Your API

The contract is the source: the validation, the specs, the SDKs and the MCP tools follow from it. Add your operations beside the example’s, then take the example’s out.

charter spec     # after every change to the contract, migrations/ or the queries
charter check    # fails on a stale spec, a failing test, or (in Go) a TinyGo gap

The files

  TypeScript (oRPC, Zod) Go (Huma)
The contract api/contract.ts api/contract.go
The handlers api/handlers.ts api/handlers.go
The storage api/store.ts (D1, with Drizzle on api/tables.ts) api/store.go (Store, and MemStore for go test and -native), api/store_d1.go (D1, through api/queries.go)
The schema and its typed code migrations/*.sql; api/tables.ts, written from them migrations/*.sql and api/queries.sql; api/queries.go, written from them (below)
The specs’ document api/spec.ts, whose specs charter spec writes api/spec.go: API(Env), which charter spec writes the specs from
The Worker worker.ts, bundled into build/worker.mjs; its bindings are env from cloudflare:workers, typed from charter.toml (.charter/env.d.ts) main.go, with the bindings in platform_js.go; worker.mjs, the Worker’s entry. Its module (go.mod) is the project’s name, on the Go library github.com/joeblew999/charter
The tests test/, and unit tests beside the code (api/*.test.ts) test/, the TypeScript twin’s tests, with the schemas they check answers against (test/schemas.ts); unit tests in api/*_test.go
charter’s charter.toml (below): the Worker, its bindings, its tests the same

Add an operation

  1. In the contract: its method, path and operationId (also the MCP tool’s name), its input with the rules each value follows, its output, and who may call it (Auth). In Go, struct tags say where a value comes from and its rules:

    type GetInput struct {
    	ID int64 `path:"id" minimum:"1" example:"42" doc:"The note's id"`
    }
    
  2. The handler gets its input already checked. To refuse, return Huma’s error in Go (huma.Error404NotFound("...")), throw oRPC’s in TypeScript; every refusal goes out as problem details (The standard API).
  3. The storage: a table change is the next numbered file in migrations/; never edit one that was applied. Then charter spec writes the typed code again (The database).
  4. The SDK’s names: x-fern-sdk-group-name and x-fern-sdk-method-name on the operation name client.<group>.<method>() (the example’s sdk(...) helper). Every constrained field needs a valid example.

The database

The schema is migrations/*.sql, the one place the tables are said: a deploy applies them to D1, charter dev to its local database. From them charter spec writes the code that reads and writes the tables, typed, as db in charter.toml says, and charter check fails when it is stale.

db You write charter spec writes
"drizzle" (TypeScript) The queries with Drizzle, on drizzle(env.DB) api/tables.ts: each table, as SQLite has it once the migrations ran
"sqlc" (Go) The queries in api/queries.sql, each named (-- name: CreateNote :one, :many or :exec), with a comment that starts with its name as its doc api/queries.go: a function for each, over charter/d1, its arguments and rows typed by sqlc against the schema. A row of a whole table is that table’s type (NotesRow); a column that may be NULL is a pointer

The Go is written over charter/d1, not database/sql, which D1 does not need: a statement is one call, and its rows come back as one string. The notes examples keep their notes this way.

Write for Workers

  • No memory between requests. State goes in D1 or a Durable Object.
  • No background work after the answer; little work at start-up, which runs for every new instance.
  • In Go, TinyGo lacks parts of reflect, the disk and some of net: charter check runs the API under workerd, which finds it; go test cannot.

MCP

Every operation that answers once is also a tool at /api/mcp, with the same checks and the same sign-in: a call is let in, refused and limited as the route is. A stream or a WebSocket is not a tool. To hide one: mcp.NotATool(op) in Go, .meta(notATool()) in TypeScript.

claude mcp add --transport http billing-api https://billing-api.<your-subdomain>.workers.dev/api/mcp

charter.toml

Everything charter knows about the project: its Worker’s name and URL, its port, its bindings (D1, Durable Objects; its rate limits are the contract’s), its secrets (and throwaway local values in [local]), its SDKs, and its tests: [test] run against the API on workerd (charter check), live against the deployed one (charter live-test). A TypeScript Worker names its entry: [build] entry = "worker.ts". Every key: go doc github.com/joeblew999/charter/project.

Take the example out

What What to do
The contract, handlers and storage Replace the notes; keep hello and the wiring
migrations/0001_init.sql Replace it if never deployed; otherwise add a file
test/ Rewrite the notes checks
charter.toml Your SDKs’ name: [sdks] name, then charter spec

Without a stream, remove the hub too: the Hub export, the HUB binding and the stream’s code. grep -rni note api test migrations charter.toml shows what is left.