truewire.toml reference
A Truewire project is a directory holding a truewire.toml. Every command finds the
nearest one above the working directory, or takes --project PATH. truewire init writes
the file with the [cores.*] and [python.cores.*] tables its --template needs; see
docs/cores.md for what each template wires.
#:schema https://truewire.dev/schemas/truewire.toml.json
[project]
name = "petstore" # required. Also the default Python package name.
[spec]
dir = "spec" # default "spec": holds endpoints/ and schemas.json
[secrets] # optional: credential variable *names*, never values.
required = ["PETSTORE_API_KEY"]
optional = ["PETSTORE_ADMIN_KEY"]
[policy] # optional: what the client may do on its own behalf.
rate = 10 # requests per second it paces itself to; unpaced when absent
retry = false # default false
refuse = ["pets.delete"] # endpoint ids it refuses to call
[cores.default] # one table per symbolic core name a router.json can declare.
meta = { type = "object", properties = { public = { type = "boolean" } }, additionalProperties = false }
[python]
package = "petstore" # default: [project].name
src = "src" # default "src": the package lives at <src>/<package>
name = "Petstore" # root class name; default: PascalCase of [project].name
backend = "backend.py" # optional: a module exporting `generator`, a Generator subclass
ruff = "ruff.toml" # optional: formatter config; default is the one shipped with truewire
pyright = false # run pyright after generate; also implied by a pyrightconfig.json
[python.cores.root] # the core the root router.json names (usually "root")
base = "petstore.core:ClientBase"
[python.cores.default] # every other symbolic core: base class + how children get the transport
base = "petstore.core:Endpoint"
[python.cores.streams] # a base built through `new(client, *, ...)` declares its keywords
base = "petstore.core:StreamsBase"
forward = ["market_client"] # passed from the composing class's own same-named field
params = { network = "petstore.core:Network" } # exposed to the caller, typed by import
children = { market_data = "market_client", private = "private_client" }
[typescript]
package = "petstore" # default: [project].name; the package lives at <src>/<package>
src = "src" # default "src"
name = "Petstore" # root class name; default: [python].name, else PascalCase of [project].name
[rust]
package = "petstore" # default: [project].name; the modules live at <src>/<package>, lib.rs at their top
src = "src" # default "src"
name = "Petstore" # root struct name; default: [python].name, else PascalCase of [project].name
[score] # optional
stranger = 2026-10-12 # the day a newcomer last completed the quickstart unaidedSections
[project]:nameis required. It names the project in output and is the default for[python].package.[spec]:dirrelocates the spec tree.truewire check,examples,surface,mockandgenerateall read<dir>/endpoints/**/endpoint.json,<dir>/**/router.jsonand<dir>/schemas.json.[secrets]:requiredandoptional, lists of environment variable names and nothing else: a value, a path or another key is an error. The values live in.env, whichtruewire initgit-ignores.truewire standardsuses the names to flag a recorded example that leaked a real credential-shaped value.[policy]:rate, a positive number of requests per second;retry,trueorfalse;refuse, endpoint ids (the dotted function pathtruewire capturetakes).truewire checkfails on arefuseid that names no endpoint in the spec.refuseis generated (W15): a refused endpoint keeps its method in Python, TypeScript and Rust, and the method fails with the package'sRefusedByPolicy(aLogicError, generated intopolicy.py,policy.tsorpolicy.rs) before any request is made.generatefails on an id that names no generated endpoint, and on a refused endpoint whose method is written by hand:surface: handwritten, an extras entry thatreplacesit, or a[python].backendskip_endpoint. Go does not render refusals yet, andgenerate gosays so for each one.rateandretryare generated onto the root (RATE/RETRYin Python and TypeScript, associated consts in Rust), and the hand-written core builds its runtimeHttpClientfrom them (cores): request starts spaced1 / rateseconds apart, and, withretry, up to three attempts for a connection that never opened or a 429/503, waiting itsRetry-Afterup to 30 s. Go renders neither yet.[score]:strangeris the datetruewire scoreprints for S12 (docs/shape/score.md): the day someone who had never seen the project completed its quickstart from the published package and the public docs alone. A TOML date or the same as a"YYYY-MM-DD"string. Absent, the row isuncheckedand the project is not done. No command writes it; the person who watched the run does.[cores.<name>]: the JSON Schema every endpoint'smetamust satisfy when its nearestrouter.jsonresolves to<name>. Omitmetafor a core that reads nothing per call.[python]: where the generated Python package goes and how it is finished.nameis the root class, and it must not be the class one of the spec's own top-level router groups renders -- a client calledWeatherover aweather/group has the root import the class it then declares itself (docs/spec/authoring.md rule 18). The same holds for[typescript].nameand[rust].namebelow.[python.cores.<name>]:baseismodule.path:Class, the hand-written class every generated endpoint under that core subclasses. Nothing is imported during generation (ADR 0011); the three optional keys below say how a composite built on this base is constructed.childrenmaps a composed child attribute to the field ofselfit receives as its transport, for a base whose children need different connections ({ spot = "spot_client", streams = "private_client" }). Unlisted children getself.client.forwardlists the keywords of this base'snew(client, *, ...)that the composing class passes from its own same-named fields:forward = ["market_client"]rendersStreams.new(self.private_client, market_client=self.market_client).paramsmaps the keywords ofnew()a caller supplies to their types. A string ismodule.path:Name(imported by the composing module) or a bare builtin (str); the table form{ type = "...", required = false }makes it optional. The composing class exposes each as a keyword-only parameter (def token(self, *, network: Network)), unless its own core is this same core, in which case it already carries the field and forwardsself.<name>.
Declaring
forward(even as[]) orparamsmeans the base is built throughnew(client, *, ...); declaring neither builds it asChild(client=self.<field>). The protocols a base satisfies are intruewire_core.contract.[[python.extras."<router node>"]]: hand-written classes folded into a generated router node (file,class, optionalreplaces).[typescript]: wheretruewire generate typescriptwrites the TypeScript package (<src>/<package>/) and what its root class is called. There is no per-core table: a generated class takes its core as a constructor argument typed by the@truewire/corecontract (HttpEndpoint<Meta>,CommandEndpoint<Meta>,DualEndpoint<Meta>,StreamEndpoint<Meta>,ReplyStreamEndpoint<Meta>), and the hand-written<package>/core/index.tssatisfies it by shape, so nothing is imported or resolved at generation. The composition keys are read from[python.cores.<name>], the one declaration serving both languages: a core declaringchildrenorforwardmakes its router take a fields object -- one property per field the declarations name, exported as the<Class>Coreinterface -- and hand each child the field itschildrenentry maps it to (clientwhen unmapped), or the whole object to a child that is itself composite.paramsneeds nothing: the hand-written core takes its parameters when it is built. See docs/typescript.md.[[typescript.extras."<router node>"]]: hand-written classes folded into a generated TypeScript router (file,class,methods, optionalreplacesandfield); what serves an endpoint whose spec declaressurface: handwritten, which the TypeScript backend does not render. See docs/typescript.md.[rust]: wheretruewire generate rustwrites the Rust modules (<src>/<package>/, whoselib.rsthe project'sCargo.tomlnames as its library path) and what its root struct is called. As for TypeScript there is no per-core table: a generated struct holds its core as anArc<dyn HttpEndpoint<Meta>>fromtruewire-core, and the hand-written<package>/coremodule, which the generatedlib.rsdeclares, implements the trait, so nothing is imported or resolved at generation. A core declaringchildrenorforwardmakes its router take one parameter per field (Kraken::from_cores(market_client, private_client, spot_client)), a field needing several traits held as the combined traitcontract.rsdeclares; see docs/rust.md.
Generated state
generate writes <src>/<package>/meta.py, one TypedDict per [cores.<name>] that
declares a meta schema (SpotMeta for spot); the hand-written core annotates its
meta parameter with it. truewire init writes the first one so the core template can
import it before the first generate.
generate typescript writes <src>/<package>/meta.ts, one interface per core with a meta
schema (DefaultMeta for default), which the hand-written core names as the Meta
parameter of HttpEndpoint. generate rust writes <src>/<package>/meta.rs, one struct
per such core, which the hand-written core implements HttpEndpoint<DefaultMeta> for.
generate writes .truewire/codegen/<language>.json (python.json, typescript.json,
rust.json, go.json), the manifest of files it owns. Files not in the manifest are
never deleted; generate --check compares the plan to it and each owned file's content
to what the plan renders (formatted the way generate writes it), and generate --delete
removes only what it owns. Commit the manifest: it is what lets --check on a fresh
clone see a file an earlier plan owned that this one does not, which the next generate
deletes. Only .truewire/cache/ is git-ignored (truewire init writes that line, and
rewrites a bare .truewire/ line an older init wrote). On a checkout without a manifest,
generate --check takes the plan as the owned file list -- it names every file generate
would write -- and checks existence and content the same way, less that one difference.
Up to 0.10 the manifest was .truewire/<language>-files.json, git-ignored. generate
moves one it finds to the new path and says so; generate --check reads it when the new
path has none and reports it as a stale manifest, so the check fails until generate has
moved it.