neuron-explorer
Steering-first neuron explorer for Qwen3.5-4B: push every MLP neuron, keep every rollout, browse the results
翻译准备中
⚒️ Guardrails for your AI coding agents — a full-stack TypeScript starter on Bun
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Server-rendered JSX, light-touch JS, custom CSS — one codebase, one deploy target.
Deterministic templates with strong types that AI agents can reason about and test with confidence.查看完整 README ↗TLDR: Full-stack TypeScript starter built for AI coding agents. Server-rendered JSX (not React), custom CSS, PostgreSQL via Bun — one process, one test runner, one deploy target. Ships with auth (magic-link or email+password, your pick), CSRF protection, rate limiting, auto-migrations, and 1,100+ tests. The architecture is deliberately simple (services → controllers → templates) so AI agents get fast, unambiguous feedback from strict types, zero-warning linting, and a test suite that runs in seconds. Deploy anywhere you can run
bun run start.— Claude Opus 4.6
Billet (noun): A semi-finished piece of steel, shaped and ready to be worked into something specific. Named for Sheffield — the Steel City, where crucible steel was invented.
Left to their own choices, AI coding agents will reach for what they know best: React with Next.js. The result is a thick-frontend app split across client and server, locked into a specific ecosystem, requiring multiple test systems to simulate browser state, and unnecessarily complex and costly to deploy.
Billet takes the opposite approach. It's a single-instance server-rendered app with light-touch client-side JavaScript. Templates are deterministic functions of their props — given the same input, they produce the same HTML. This makes them trivial to test in a single unified system without browser simulation. One codebase, one test runner, one deploy target.
This isn't a limitation. It's a deliberate architectural choice that plays to AI's strengths: strong type information to reason about, functional input/output patterns, and a feedback loop (write code → run tests → see results) that works in seconds, not minutes.
📝 For the longer story behind why Billet works this way — the fat front-end, RLS, and the case for a real server — read Boring on purpose.
AI agents work best when they get told they're wrong immediately. Not by you — by the toolchain. Type errors, failing tests, lint warnings, broken builds — that's backpressure, and it's the single most important thing you can invest in when working with agents. Every automated check that catches a mistake is one less time you have to context-switch back in to fix something a machine should have caught.
When you do have to step in and rescue an agent, don't just fix the output and move on. Ask why it went wrong and close that gap. Add a type. Write a test. Tighten a schema. The goal isn't zero failures — it's zero repeat failures. Every rescue you engineer away is time you get back.
Billet is built around this idea: strict TypeScript, zero-warning linting, deterministic templates, and a test suite that runs in seconds. The architecture itself is the feedback loop.
Billet is built for server-rendered apps with light client-side interactivity. If your project needs a highly reactive, state-driven UI — real-time collaborative editing, complex drag-and-drop interfaces, rich data visualisations — you're better off with a full client-side framework like React or Svelte from the start. Billet lets you opt in to client-side frameworks per page, but if most of your pages need one, the thin-frontend approach is working against you rather than for you.
Auth, security, database, testing, linting — these are the rails. They're solved so the agent can focus on your custom logic instead of bugging you with questions about tech choices and security fundamentals.
Two complete auth flows, chosen with one environment variable. AUTH_MODE defaults to magic-link;
set it to password for conventional email-and-password credentials. The two are mutually exclusive —
offering both at once would mean every account has two ways in, and the weaker one sets the ceiling.
Bun.password, no new dependency), a full reset-by-email flow, and change-password on /accountusers.email_verified_at records when an address was proven, with a fixed reminder banner until it is. Nothing is gated on it; forks add their own gating/admin route namespaceTEAMS_ENABLED (off by default) — a /team page to invite people by email, see the member list, change org roles, and remove people. organization_members.org_role is a separate axis from users.role, so an org owner is not a platform admin; migration 008 adds three tables and alters none, so a fork that won't use it can drop it without touching account dataCAPTCHA_ENABLED)CRYPTO_PEPPER rotation stays recoverable; password reset gives identical answers for known and unknown addresses, and so does sign-in except for accounts carried over from magic-link mode, which are told they have no password yet (see SECURITY.md for that trade-off)/.well-known/security.txt, with every script served from our own origin — see runbooks/SECURITY.mdPostgreSQL through Bun's built-in Bun.SQL — no ORM, no driver dependency.
migrate:up, migrate:status, migrate:create)bun run seed)bun run fixtures) and a session for any persona (scripts/persona-session.ts) — see runbooks/FIXTURES.mdThe "deterministic templates, test with confidence" tagline isn't a marketing claim — it's backed by infrastructure.
renderToString() and string assertions.env.test carries DATABASE_URL and nothing else; every other variable is pinned by a preload (src/server/test-utils/test-env.ts), so your dev .env can't change what the suite sees no matter which command starts itRun the full suite: bun run test — around 15 seconds on four cores, since files run in
parallel with a database per worker.
--max-warnings 0) — no any types, no console statements, no unused variablesnoUnusedLocals and noUnusedParameters.github/workflows/ci.yml); gate merges behind them with required status checks (see runbooks/CI.md).bun-version and engines.bun fix the Bun version, and CI installs from the same file, so the toolchain changes on a commit rather than on Bun's release schedulebun run audit fails on high-severity advisories, run on every PR and weekly on a schedule (.github/workflows/audit.yml), with Dependabot opening the routine bumpssrc/server/services/logger.ts — replaces console.* with levelled output (info, warn, error)@import resolution, CSS nesting, and minification — no external CSS tooling neededsrc/client/reactive/ is a few hundred lines: signals, computeds and effects, plus data-* bindings (data-text, data-show, data-value, data-on…) that name a component's state by name, never by expression — with ui.perElement for the case where the elements are a list the server built from data and there is no name to give each one. No virtual DOM, nothing rendered twice, no 'unsafe-eval' in the CSP, and the binding names in a template are typechecked against the component<form> that works without JavaScript. With it, server.submit(form) posts the same form with the CSRF token as a header and asks for a fragment; the controller answers with the server-rendered row instead of a redirect, and a stale token is refreshed and retried once. The classic todo list is the worked example — add, toggle, delete and filter, no page loadsregisterPage() / PageController pattern with init() and cleanup() for per-page JSThe "designed for AI agents" tagline is the reason Billet exists, so here's what that means in practice.
The repo ships a CLAUDE.md that holds only what an agent can't learn by reading the repo, plus path-scoped rules in .claude/rules/ and a set of skills in .claude/skills/. CLAUDE.md is the gotchas: JSX with no hydration, why service tests mock the database module before importing, why bun test isn't the test command, where security headers actually come from. Guidance that only matters in one part of the tree lives in path-scoped rules under .claude/rules/, loaded when those files are opened, and everything procedural lives in skills that load on demand:
| Skill | Loads when |
|---|---|
adding-a-feature | Wiring a page, endpoint, or migration through every layer |
writing-tests | Adding or fixing a test — one reference per module type |
verifying-changes | Running lint, typecheck, and the test suite, and reading their failures |
This split follows Anthropic's context engineering guidance for Claude 5 models: keep the always-loaded context small and specific to your codebase, and use progressive disclosure for the rest.
AI agents are good at following patterns with fast, unambiguous feedback. Billet is designed around that:
renderToString(), and assert on the output — no browser, no async rendering, no timing issues.bun run test completes in seconds. bun run check (lint + typecheck) catches issues before they compound. Agents can write-test-fix in tight cycles.Every layer catches a different class of error before a human has to:
| Layer | What it catches |
|---|---|
| TypeScript strict mode | Type mismatches, missing properties, unused code |
| Biome linting | Style violations, unsafe patterns, console usage |
| Pre-commit hooks | Anything that slipped past the editor |
| Test suite | Behavioural regressions, broken templates, bad responses |
This is the backpressure that keeps agents inside the guardrails.
The usual way to parallelise agents is one git worktree each, and it has a sharp edge: worktrees share a single .git directory, and refs/stash lives there. The stash is one global stack across every worktree, so an agent that pops in its own checkout can silently walk off with work another agent pushed a moment ago.
Billet closes that hole. scripts/wip snapshots to refs/worktree/wip — the one ref namespace git keeps per-worktree — so a snapshot is invisible to every other agent:
bun run wip save "trying the other approach" # snapshot, working tree untouched
bun run wip stash # snapshot, then revert tracked changes
bun run wip list # this worktree's snapshots
bun run wip restore # apply the newest, and keep it
bun run wip drop # forget the newest
A PreToolUse hook in .claude/settings.json denies git stash and points the agent at the table above, so this isn't a convention an agent can forget — it's enforced.
The other thing worktrees share is everything in .env. .conductor/settings.toml runs scripts/workspace.ts provision on setup, which gives each workspace its own port (CONDUCTOR_PORT), its own dev and test databases (<base>-<workspace>), and its own session cookie name — cookies aren't scoped by port, so two apps on localhost otherwise share one. The test database matters most: cleanupTestData truncates every table, so two agents on one billet-test fail each other's suites at random. scripts/workspace.ts destroy runs on archive and drops the pair, refusing any name that doesn't carry that workspace's slug. Both are no-ops in cloud workspaces. If you don't use Conductor, run bun run scripts/workspace.ts provision with CONDUCTOR_WORKSPACE_NAME and CONDUCTOR_PORT set, or ignore it — the defaults are unchanged.
specification.website is an open, platform-agnostic checklist of the technical features a good website should have — 168 specs across foundations, SEO, accessibility, security, performance, and more. Billet works through it section by section, so the boring-but-critical baseline is in place before you write a line of product code.
| Section | What Billet covers |
|---|---|
| Foundations | Doctype, charset, viewport, canonical URLs, Open Graph, favicons, theme-color |
| SEO | robots.txt, XML sitemap, JSON-LD, per-page metadata, an explicit indexing policy |
| Accessibility | Semantic landmarks, labelled forms, focus rings, reduced motion, captioned tables, AA-checked contrast, disabled styling |
| Security | Security headers on every response, a Content Security Policy, HSTS, security.txt, Clear-Site-Data |
| Agent readiness | An llms.txt index, AI-crawler rules and Content-Signal in robots.txt, machine-readable Link headers, structured data, stable URLs |
| Performance | Brotli and gzip compression, fingerprinted immutable assets, ETag revalidation with 304s, preconnect resource hints, minified bundles |
You'll need Bun and a local PostgreSQL instance running. If you need help getting PostgreSQL set up, ask Claude — it'll walk you through the install for your OS.
Click Use this template on GitHub to create your own repo, then:
git clone <your-new-repo-url>
cd <your-project>
bun install
Open START_PROMPT.md with your AI coding agent. It will walk through creating your .env files, renaming the project, stripping the Billet starter content, and verifying everything works.
Once setup is complete you can delete START_PROMPT.md.
If you'd prefer to set up manually:
cp .env.example .env
bun run generate:pepper # copy the output into CRYPTO_PEPPER in .env
Add your DATABASE_URL to .env (e.g. postgresql://localhost/myapp), then:
bun run dev
Visit http://localhost:3000 — migrations run automatically on startup.
src/
├── client/ # Browser-side code
│ ├── main.ts # Entry point — registers pages and components
│ ├── page-lifecycle.ts # Page init/cleanup system
│ ├── reactive/ # Signals, components, bind() and the form submit helper
│ ├── style.css # CSS entry point — @import manifest, no rules of its own
│ ├── base.css # Tokens, element defaults, shared utilities
│ ├── components/ # Shared components + CSS (nav, layout, todo list)
│ └── pages/ # Page-specific JS + CSS (co-located)
│
├── server/ # Server-side code
│ ├── main.ts # Server entry point
│ ├── routes/
│ │ ├── app.tsx # View routes (HTML)
│ │ ├── api.ts # API routes (JSON)
│ │ └── admin.tsx # Admin routes (protected)
│ ├── controllers/ # Route handlers
│ │ ├── app/ # View controllers — return HTML
│ │ ├── api/ # API controllers — return JSON
│ │ ├── auth/ # Auth controllers — login/signup/logout/account/reset/verify
│ │ └── team/ # Team controllers (TEAMS_ENABLED) — dashboard/invites/members/accept
│ ├── templates/ # Full-page JSX templates
│ ├── components/ # Reusable server JSX components
│ ├── services/ # Business logic & data access
│ ├── middleware/ # Auth, CSRF, rate limiting, admin, org roles
│ ├── utils/ # Response helpers, crypto, env validation
│ └── database/
│ ├── cli.ts / migrate.ts # Migration tooling
│ ├── seed.ts # Development seed data
│ └── migrations/ # Numbered migration files
│
└── shared/ # The seam both sides import: data-* vocabulary, header names, shared copy
scripts/
├── wip # Per-worktree WIP snapshots (safe `git stash` replacement)
├── qa-session.ts # Mints a QA session cookie so an agent never borrows a human's
├── fixtures.ts # `bun run fixtures` — loads the persona fixture set (runbooks/FIXTURES.md)
├── persona-session.ts # Mints a session cookie for a fixture persona
├── benchmark.ts # Times the suite, checks and build; saves/compares records
├── browser-smoke.test.ts # Real-browser smoke test — `bun run test:browser`, its own CI job
└── workspace.ts # Per-workspace port + dev/test databases (provision/destroy)
.claude/
├── settings.json # Hooks shared with every agent on the repo
├── rules/ # Path-scoped instructions, loaded when matching files are opened
├── hooks/
│ └── no-shared-stash.ts # Denies `git stash`, points at `bun run wip`
└── skills/ # Progressive-disclosure guides for agents
.railway/
├── railway.ts # Railway Infrastructure as Code — build, deploy and env, applied by CLI
└── railway.test.ts # Guards those settings against drift
The starter ships one example resource plus a read-only stats endpoint. They exist to be
copied or deleted — the
conventions below are what a new endpoint should follow, and
.claude/skills/adding-a-feature/references/api-endpoint.md
is the checklist for adding one.
These endpoints are unauthenticated, like the
/todospage they mirror — the demo lets guests create and toggle todos. Anything exposing real data needsrequireAuthfromsrc/server/middleware/auth.ts(or a token check for machine callers) before it ships.
data. A bare array or object leaves nowhere to add metadata
later without breaking clients.{ "error": { "code": "invalid_body", "message": "A non-empty title is required.",
"fields": { "title": "Required." } } }
Branch on code — it is stable. message is for a human reading the response, and fields is
present only on validation failures.application/json (or a +json media type). Anything else is a 415;
a body that isn't valid JSON, or that is JSON but not an object, is a 400.Retry-After in seconds.| Method | Path | Returns |
|---|---|---|
GET | /api/todos | { data: Todo[], pagination: { total, limit, offset } } |
POST | /api/todos | 201 + { data: Todo }, with a Location header |
GET | /api/todos/:id | { data: Todo } |
PUT | /api/todos/:id | { data: Todo } — title required, completed optional boolean |
DELETE | /api/todos/:id | 204, no body |
GET | /api/stats | { data: VisitorStats } |
GET /api/todos accepts ?limit= (1–100, default 25) and ?offset= (default 0). Values
outside those bounds are rejected with a 400 rather than clamped — a client silently handed
100 rows when it asked for 5000 has no way to tell it received a page.
:id must be a positive integer within Postgres serial range; anything else is a 400
(invalid_id) and never reaches the database.
curl -X POST http://localhost:3000/api/todos \
-H 'Content-Type: application/json' \
-d '{"title":"Buy milk"}'
# 201 Created
# Location: /api/todos/7
# {"data":{"id":7,"title":"Buy milk","completed_at":null,"created_by":null}}
| Code | error.code | When |
|---|---|---|
400 | invalid_id / invalid_json / invalid_body / invalid_limit / invalid_offset | Malformed path, query, or body |
404 | not_found | No such row |
405 | method_not_allowed | Wrong verb — the Allow header names the right ones |
415 | unsupported_media_type | Content-Type wasn't JSON |
429 | rate_limited | Per-IP limit exceeded; see Retry-After |
.conductor/settings.toml that gives every workspace its own port, dev database, test database, and session cookie name on setup, and drops the databases again on archive, so parallel agents can't truncate each other's test data or fight over port 3000. None of it is required: the scripts no-op when CONDUCTOR_PORT is unset, and scripts/wip is plain git worktree, so the same protections work under any agent harness.A few small packages built alongside Billet that drop straight in:
role="alertdialog".error / warn / info entries and hands them off on an interval to email, Slack, or any custom sink — a lightweight alternative to a full observability stack for a single Bun process.Billet is used by cloning or forking, so there's no package manager to warn you about a breaking change. Releases are tagged, and CHANGELOG.md records what a fork has to change in its own code before merging a new major version. Read it before pulling upstream.
Which release you're on is recorded in .billet-version, separately from the
version in package.json — that field is your app's, to bump as you like. Keep the file when you
rename the project; it's the only thing that answers "is this fix already in my tree?" without
diffing source against a repo you may not have as a remote. Update it when you merge an upstream
release. The file itself carries the two curl commands for listing upstream's tags and reading
its CHANGELOG, so comparing needs no remote and no tooling.
Contributions are welcome! Please open issues or PRs.
Billet is a single Bun process — no containers, no serverless adapters, no platform-specific runtime. Anywhere you can run bun run start, it'll work: Railway, Fly.io, Render, a VPS, or your own machine.
Four commands, from nothing to a running app. Deployments typically go live in under 60 seconds.
railway init --name my-app # new project, linked here
railway config apply # service + Postgres + env defaults
railway domain # generates the public URL
railway variables --set CRYPTO_PEPPER=$(bun run generate:pepper) # the one secret you set yourself
Then connect your repo in the service's Settings → Source, and it deploys on every push.
All of it comes from .railway/railway.ts — the build and deploy settings,
the Postgres link, and every environment variable the server reads. Needs Railway CLI
5.42.1 or newer. For a project you already have, swap the first line for railway link and
read railway config plan before applying. Nothing applies on git push.
Three things the plan won't warn you about:
source, so the
service has nothing to build until you connect the repo. That is deliberate — a starter can't
know your GitHub slug — and it means a later apply leaves your connection alone.railway config apply is
what created it, as above. It won't if you connected a repo first, because Railway names a
service after the repo — then edit export const partial to match. The service takes its name
from the partial, so that one line is the whole rename.partial is also what stops an apply deleting things it doesn't know about. Without
it, a project holding several apps and one shared PostgreSQL reads every service the file omits
as one to delete.Secrets stay out of the file: everything you own is preserve(), which means this name exists,
its value lives on Railway. The defaults that are there — EMAIL_PROVIDER=console, an APP_URL
built from ${{RAILWAY_PUBLIC_DOMAIN}} — exist so a fresh apply boots instead of crash-looping.
railway.ts comments the reasoning behind each choice, including the two that look obvious and
aren't.
Prefer the dashboard? These are the same values, to set by hand in the service's Settings tab:
| Setting | Value |
|---|---|
| Builder | Railpack |
| Build command | bun install && bun run build |
| Start command | bun run start |
| Healthcheck path | /health |
| Healthcheck timeout | 30 |
| Restart policy | On failure (Railway's default), max 3 retries |
Tip: If you're using Claude Code with the Railway MCP server, you can ask Claude to set up the project, add PostgreSQL, and configure both the environment variables and the settings above for you.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | PostgreSQL connection string — auto-set when you link Railway's PostgreSQL plugin to your service |
CRYPTO_PEPPER | Yes | Secret key for session tokens — run bun run generate:pepper to get one (see below) |
APP_URL | Yes | Your app's public URL — you'll get this from Railway after your first deploy (e.g. https://my-app.up.railway.app) |
SITE_URL | No | Canonical origin for canonicals, Open Graph tags, the sitemap, and JSON-LD. Defaults to APP_URL's origin — set it only when the canonical domain differs from the app domain |
ALLOW_INDEXING | No | Set to true to let search engines index the site. Unset, every response and page says noindex and robots.txt drops its Sitemap: line, so a preview or staging host can't be indexed by accident. Set it on production only |
PORT | No | Server port — auto-set by Railway, defaults to 3000 locally |
TRUST_PROXY | No | Set to true behind a proxy that rewrites x-forwarded-for — Railway does. Without it the rate limiter sees one shared address for every visitor. Set by .railway/railway.ts |
AUTH_MODE | No | magic-link (default) or password. Mutually exclusive; any other value stops the server at boot |
CAPTCHA_ENABLED | No | Set to true to add a proof-of-work captcha to the login form. Off by default; /login is unchanged when unset |
CAPTCHA_DIFFICULTY | No | Tunes the captcha's client-side work (search-space size). Defaults to 100000 (~sub-second for a real browser) |
TEAMS_ENABLED | No | Set to true for org-level user management — invite by email, member list, org roles. Off by default; /team and /invites/accept 404 when unset. Any value other than / stops the server at boot |
Generating
CRYPTO_PEPPER: This is a secret key used to secure session tokens. Runbun run generate:pepperto get a value. Use a different value for each environment (development, production, etc).
Email deliverability: When sending real mail via Resend, follow runbooks/EMAIL.md to set up SPF, DKIM, and DMARC — without it, magic links and password-reset mail land in spam.
SEO: Canonicals, Open Graph tags, the sitemap,
robots.txt, and JSON-LD all followAPP_URL's origin, so pointing that at your production domain points them too. SetSITE_URLonly if your canonical domain differs from your app domain (marketing site on the apex, app on a subdomain). One thing you must set on production:ALLOW_INDEXING=true. Without it every response and page saysnoindexandrobots.txtdrops its Sitemap line, so a preview or staging host can't be indexed by accident — set it on production only. See runbooks/SEO.md for both, plus the sitemap, indexing policy, and verification steps.
Security: The HTTP hardening (security headers, CSP, HSTS) works out of the box, but set
SECURITY_CONTACT(thesecurity.txtreporting address) and add the registrar-level records before launch — see runbooks/SECURITY.md for that plus the TLS, HSTS-preload, CAA, and DNSSEC steps.
Auth mode: Leave
AUTH_MODEunset for magic-link auth. SetAUTH_MODE=passwordfor email-and-password instead — that enables/forgot-password,/reset-password, and the change-password form on/account, which all 404 in magic-link mode. Switching an existing app topasswordleaves current users with no password:/accountoffers those accounts a set-password form, and/forgot-passwordcovers anyone already signed out — a failed sign-in tells them so and links them there, rather than leaving them to guess at "Invalid email or password" forever./signupand/accountexist in both modes.
Team management: Leave
TEAMS_ENABLEDunset and nothing changes —/teamand/invites/accept404, no org is ever created, and the three tables migration008adds stay empty. Set it totrueand a signed-in user can create a team, invite people by email, change their org role, and remove them. Three things are worth knowing before you turn it on. The org role is a separate axis fromusers.role— that one still means platform operator and still gates/admin, so making someone an org owner does not give them your admin console. One user belongs to one org; there is no switcher, and accepting a second invite is refused. And core does not scope your own data by org — theprojecttable is untouched, deliberately;requireOrgRolehands you the resolved membership so you can scope your own queries. See runbooks/TEAMS.md for the authorisation model, the invite lifecycle, and what is deliberately not shipped. Two behaviours are worth expecting:/teamis in no navigation, so signing in without a membership lands on/teaminstead of/(and/accountlinks to it once you're in a team), and neither self-action is yours to take — you can't change your own org role or remove yourself, because the only self-change the roles allow is a demotion out of the page you'd need to undo it. Turning the flag on over an existing database leaves every current user with no team, so they'll see the "create a team" page first.
Signup spam: The login form always carries a honeypot and per-IP rate limit. If bots still create accounts with random emails, set
CAPTCHA_ENABLED=trueto add a first-party proof-of-work captcha — it signs challenges with your existingCRYPTO_PEPPER, so there's no third party, account, or extra secret to configure.
Privacy: The default site needs no cookie banner. The only thing it stores client-side is the reader's own theme choice (
billet-theme), written only when they pick one — strictly necessary, and nothing at all is written on an ordinary page view. The moment you add analytics, ads, or embeds, you must add an opt-in consent banner and a privacy policy — see runbooks/PRIVACY.md for wiring up@alexpricedev/billet-cookie-consent, the required policy disclosures, and GPC handling.
CI & merge protection: CI runs on every PR, but check results are advisory until you require them. A fork doesn't inherit branch protection, so nothing stops an auto-merge (GitHub's or Conductor's) from merging a red build — enable required status checks once per repo. See runbooks/CI.md.
Billet uses PostgreSQL through Bun's built-in Bun.SQL — no ORM, no driver dependency. Migrations run automatically on server startup — pending migrations are applied before the server accepts requests. If a migration fails, the server won't start (fail-safe).
A lightweight CLI is also available for manual operations:
bun run migrate:up # Run pending migrations
bun run migrate:status # Show migration state
bun run migrate:create # Create a new migration file
If you don't need a database, remove the src/server/database/ and src/server/services/ directories and strip the DB-related routes. There's no framework coupling to undo.
MIT — free for personal and commercial use.
Cache-Control headerstruefalse