Takoserver is an open-source cloud you run on your own Linux machine. You deploy Workers, with their SQLite databases, KV namespaces, object buckets and queues, from Takoform definitions, and it serves each Worker at an HTTPS hostname on your domain. It is for people who run their own server and domain: this repository gives you the software, not a hosted account.
No released Takoform client speaks the Host API v2 this Host uses yet (the
OpenTofu provider, terraform-provider-takoform 4.1.0, is v1-only), so today you
deploy with curl, or in a browser console you serve yourself. An organization
owns its Resources, and its API keys, each with read or write permission,
manage them.
To serve Workers:
- a Linux x86-64 machine with glibc 2.38 or newer, such as Ubuntu 24.04;
- git, curl, jq, Bun 1.4.0, and the Go toolchain to build the Workflow execution guard;
- the pinned
workerd, attached to the 2.0.0-rc.1 pre-release; 1.0.0, the release marked Latest, does not have it; - a domain with a wildcard DNS name for the Workers, such as
*.apps.example.com, pointing at the machine, a wildcard certificate for it, issued through a DNS challenge that needs your DNS provider's API, and TCP 443; - an HTTPS front end such as Caddy for the API, on its own name and another
port, such as
api.example.com:8443, and TCP 80 for its certificate.
For a look without Workers, Docker with Compose is enough.
| To | Follow |
|---|---|
| Look around: the Host API, operator sign-in, organizations and API keys, without Workers | Docker and Compose self-host |
| Serve a Worker over HTTPS: install, sign in, and create the five Resources of one Worker, with curl or in the console | First install and first use, steps 1 to 9 |
| Ship new code, add SQLite, KV, a bucket or a queue, and delete Resources | Steps 10 to 13 of the same guide |
| Decide what to expose and how to harden the machine | Self-host security |
| Monitor, back up, update or roll back | Monitoring, Cold snapshot, Update and code rollback |
| Look up a v2 setting, the Forms a Bun Host supports, or how organizations own Resources | Takoform v2 architecture and operator setup |
| Change the code | Working on it: bun run check is the gate |
A Takoform Form defines one kind of resource, such as a Worker or a SQLite database, at a versioned URL. These are the Forms a Bun Host can serve:
- Artifacts:
WorkerBundle 0.2.0,StaticAssetBundle 0.2.0andSQLiteMigrationSet 0.4.0. Worker code, static files and SQL migrations, pinned by URL and SHA-256 and kept by the Host. By themselves they do not run a Worker, serve assets, apply SQL or create an endpoint. - Workers and HTTPS:
ModuleWorker 0.3.0,WorkerVersion 0.5.0,WorkerDeployment 0.4.0,WorkerCronTrigger 0.3.0andWorkerEndpoint 0.3.0. A Worker, its versions, how its traffic is split across up to eight of them, a schedule, and an HTTPS hostname. - Data:
SQLiteDatabase 0.2.0,SQLiteMigrationApplication 0.4.0,EdgeKVNamespace 0.2.0andObjectBucket 0.2.0. SQLite databases, KV namespaces and object buckets, which a Worker reaches through Bindings, and the application of a migration set to a database. - Queues, actors and workflows:
AtLeastOnceQueue 0.2.0,QueueConsumer 0.3.0,ActorNamespace 0.3.0andDurableWorkflow 0.3.0. At-least-once queues and the consumers that deliver their messages to a Worker, actors that keep state, and durable workflows. A Worker reaches the queues, actors and workflows through Bindings.
A Host serves exactly the Forms its configuration composes:
- An artifact Form needs only Bun and its block in
TAKOSERVER_TAKOFORM_V2_CONFIG. It validates and keeps bytes the operator has seeded or that are reachable over public HTTPS. - Everything else needs the complete local Worker profile: on Linux x86-64, the
pinned
workerd, the Workflow execution guard and the settings listed for that profile under Settings. A partial Worker profile advertises none of these Forms. - Within that profile,
SQLiteMigrationApplication 0.4.0also needs asqliteMigrationSetblock, andWorkerEndpoint 0.3.0needs the listener on TCP 443; without one of them, only that Form is absent. SQLiteMigrationSetandSQLiteMigrationApplication0.2.0 and 0.3.0 are retained, not offered: Resources a Host already accepted under them stay readable, recoverable and deletable, but the support endpoint reports them unsupported and a new create or update is refused withunsupported_form.
Before relying on a Form, ask a running Host with an API key, passing the
URL-encoded Form URL, for example
https://edge.forms.takoform.com/forms/ModuleWorker/0.3.0/:
GET /apis/forms.takoform.com/v2/support?form=<Form URL>.
Takoform v2 architecture and operator setup has the exact gates, Form map and limits.
A Takoform Form is identified by a versioned HTTPS URL, such as
https://edge.forms.takoform.com/forms/SQLiteDatabase/0.2.0/, that fixes what
a Resource's spec, observed state and output mean without naming a provider.
Takoserver is a Host: it implements the Forms its configuration composes and
serves them from this machine.
- Declare a Resource with its exact Form URL, a name, the Space and a spec.
Host API v2 accepts each create, update or delete as an Operation and applies
it in the background; poll
/apis/forms.takoform.com/v2/operations/{id}until it issucceededorfailed. Every create, update and delete carries anIdempotency-Keyof 16 to 128 characters, and an update or delete also names the generation it read intakoform-expected-generation. A Form this Host does not compose is refused, never approximated, so check it first as in What you can deploy. Steps 8, 10 and 13 of the install path show the exact create, update and delete requests. - Own Resources through an organization. Its ID is the v2 Space, and its API
keys and its owner's session act as one principal, so replacing a key does
not strand a Resource. A
resources:readkey reads; aresources:writekey reads and changes. A key cannot choose another organization; a signed-in owner names one with thetakoform-organizationheader on every request. - Ship code as a WorkerBundle, which names an exact HTTPS URL and SHA-256 rather than accepting an upload (see Held artifacts). A ModuleWorker is the Worker's identity, with an empty spec. A WorkerVersion names its ModuleWorker, at most one WorkerBundle (a static-site Version names only a StaticAssetBundle), its handlers, vars and Bindings, and cannot be changed afterwards.
- Bind Resources into a Worker by UID. A WorkerVersion lists them in
sqliteBindings,kvBindings,bucketBindings,queueProducerBindings,serviceBindings,actorBindingsandworkflowBindings, each entry{name, resource: {resourceUid}}, and the Worker reaches each one by its name, such asenv.DB. The Host serves the SQLite, KV, bucket and queue Bindings from private loopback services, not from its public listener. A Resource that another still names cannot be deleted, so delete in reference order. - Serve a Worker with a WorkerDeployment, which splits its traffic across up to eight WorkerVersions by weights that add up to 10000, and a WorkerEndpoint, which publishes the Worker at an HTTPS hostname on the Host's TCP 443 listener. Neither a bundle nor a WorkerVersion can be changed, so to ship new code, create a new WorkerBundle and WorkerVersion, then update the Deployment's versions.
- This repository is the Host and its self-host plugin: everything needed to build and run a Takoserver on your own machine.
- It serves Takoform Host API v2 only. Host-side code implements each Form
it composes; the Host does not install Form packages, check their
signatures, or translate requests into Takoform Host API v1. Its own
/v1account, wallet and AI APIs are separate product APIs. - takoserver.com's managed backend is not here. Its non-public Workers for Platforms backend is supplied separately by its operator and is not needed to build or run a self-host. Where the control plane runs is a separate deployment choice.
Docker and Compose self-host packages the artifact/control API profile with persistent local state and a loopback-only host port. Build it from a reviewed checkout; the complete Worker runtime requires the additional native setup below.
Self-host operations is the install path, step by step, from a reviewed checkout to a Worker served over HTTPS. This section summarises it and lists the settings.
The Bun entry (src/entry-bun.ts) runs one of two profiles, selected by
configuration:
- Artifact profile. The v2 Host API, operator sign-in, organizations, API
keys, and the artifact Forms
WorkerBundleandStaticAssetBundle, plusSQLiteMigrationSetonce a migration set is seeded, which validate and retain bytes without running them. It needs Bun and the three required settings below. - Complete local Worker profile. Adds the Worker lifecycle Forms and their
SQLite, KV, ObjectBucket and Queue Bindings, run on Linux x86-64 by the
pinned closed-graph workerd (a release attaches the
binary with its notices; see release assets). The
Host advertises them only when every setting in the second table is present
and the existing Worker owners restore at startup; a partial setup advertises
no reduced Worker profile.
WorkerEndpointalso needs the HTTPS listener on TCP 443 described below.
Neither profile needs publisher-set admission: the v2 Host serves the Forms
its configuration composes. scripts/selfhost-form-admission.ts and
Self-host admission belong to the
retained v1 repair path and are not v2 setup.
A Bun Host keeps its control state in local SQLite and its artifacts in a
local content-addressed store, by default under the data root
(Data root and backups), and it refuses
TAKOSERVER_D1_DATABASE_ID and TAKOSERVER_R2_BUCKET before opening that
state; D1- and R2-bound execution belongs to the Worker entry
(Running it on Cloudflare). In the complete Worker
profile it runs Workers on this machine with the pinned
workerd, the runtime Cloudflare runs at
the edge.
With the settings below, the first start creates the local schema and starts
serving. With no identity provider configured, it generates the operator key and
prints a ten-minute, single-use sign-in assertion until the operator has signed
in (Operator sign-in and funding). If
TAKOSERVER_CONSOLE_ORIGIN is set, sign in at that exact console origin;
otherwise the Host prints its origin, API documentation URL and how to sign in
over the API. The Bun Host does not serve a console page itself. With an
identity provider configured, use its sign-in flow instead. Then create the
organization that will own Resources; its ID is the v2 Space.
Three settings are required; startup stops without them:
| Variable | What it does |
|---|---|
TAKOSERVER_PUBLIC_ORIGIN |
Canonical bare https:// origin of the control API, used for Host identity, operator audience, and the session cookie's Secure policy. The Bun listener speaks HTTP, so this is the origin of the TLS front end in front of it. |
TAKOSERVER_TAKOFORM_V2_CONFIG |
Strict, non-secret JSON with documentation and authenticationDocumentation URLs and the optional sqliteMigrationSet, workerBundle and staticAssetBundle blocks of held artifacts. See v2 operator setup. |
TAKOSERVER_TAKOFORM_V2_CURSOR_KEY |
At least 32 random bytes as canonical, unpadded base64url. An operator secret; keep it stable across restarts. |
The complete local Worker profile needs all of these, plus workerBundle and
staticAssetBundle blocks that both use targetKey
selfhost-v2-worker-primary:
| Variable | What it does |
|---|---|
TAKOSERVER_WORKERD_BINARY |
Absolute path to the exact pinned closed-graph artifact. An absent, substituted, or behaviorally incompatible binary disables Worker execution without silently selecting the npm workerd. |
TAKOSERVER_WORKFLOW_EXECUTION_GUARD_BINARY |
Absolute path, free of symbolic links, to services/workflow-execution-guard built from this checkout. |
TAKOSERVER_V2_WORKER_RUNTIME_BOOT |
Actor and Workflow boot, for example {"actor":true,"workflow":{"maximumRegistrations":64}}. |
TAKOSERVER_V2_WORKER_PRIVATE_PLANES |
The five private planes (SQLite, KV, ObjectBucket, Queue settlement and Queue Producer), each with its own fixed port and an operator-generated signing key file. |
TAKOSERVER_RUNTIME_INPUT_SEAL_KEYRING is not part of the profile and is
optional; with it, WorkerVersion also accepts privateInputs.
WorkerEndpoint additionally needs these:
| Variable | What it does |
|---|---|
TAKOSERVER_V2_WORKER_ENDPOINT_HTTPS |
Exactly 1. The Host itself listens for Worker Endpoints on 0.0.0.0:443. |
TAKOSERVER_WORKER_ENDPOINT_SUFFIX |
DNS suffix (at least two labels, not localhost or an IP address). Each Endpoint gets a one-label hostname under it. |
TAKOSERVER_WORKERD_TLS_CERT_FILE / TAKOSERVER_WORKERD_TLS_KEY_FILE |
PEM certificate covering *.<suffix>, and its key. They also terminate TLS on the retained pre-v2 Worker socket. |
TAKOSERVER_WORKERD_TLS_CERT / TAKOSERVER_WORKERD_TLS_KEY |
The same two halves as PEM text, for a deployment that has no file to point at. |
Everything else is optional, and what is absent is absent rather than faked: a
deployment with no Stripe key does not serve the route that would begin a
payment, and its console offers the way it does take money instead.
TAKOSERVER_DB can put the control database outside the data root, and
configured service dependencies may use protected files outside it.
| Variable | What it does |
|---|---|
TAKOSERVER_DATA_ROOT |
Objects, databases, published Workers, and the signing key. .takoserver in the working directory by default. |
TAKOSERVER_DB |
Control database. A file under the data root by default. |
PORT |
Where the API and console API listen, over HTTP on TAKOSERVER_LISTEN_HOST (default 8787). |
TAKOSERVER_LISTEN_HOST |
Interface address the API listener binds, as one canonical IP literal; blank reads as unset. 127.0.0.1 by default. In a container or Pod, 127.0.0.1 is the container's own loopback, so published ports, other containers, Services and probes are refused: set 0.0.0.0 (or :: for IPv4 and IPv6), or the container's private address, and restrict access with a network policy or firewall. Before this default the listener bound every interface, like ::. See Self-host security. |
TAKOSERVER_PRINT_OPERATOR_ASSERTION |
1 prints a fresh operator sign-in assertion on every boot; 0 or blank keeps the default, which prints one only until the operator has first signed in. |
TAKOSERVER_SELFHOST_TENANT_RUN_CREDENTIALS |
Set to exactly 1 to mount the self-host runner-credential route. Absent or any other value leaves it at 404. |
TAKOSERVER_SELFHOST_TENANT_RUN_CREDENTIAL_KEY_ID |
Optional dedicated key identity for self-host runner credentials. Changing it creates a separate private key file under the data root; it must differ from TAKOSERVER_SIGNING_KEY_ID. |
TAKOSERVER_WORKERD_PORT |
Where the retained pre-v2 runtime serves published Workers (default 8788). |
TAKOSERVER_WORKER_ENDPOINT_PORT |
Port a published Worker address carries. The workerd port by default; set it when something else terminates in front of workerd. The scheme's own default (443, 80) publishes a portless address. |
TAKOSERVER_SUFFIXES |
Hostname suffixes this deployment will serve. Empty means any. |
TAKOSERVER_OPERATOR_PUBLIC_JWK |
Public half of the operator key. Generated under the data root if unset. |
TAKOSERVER_OPERATOR_IDENTITY_PUBLIC_JWK |
Optional identity-only operator key. It overrides the login key without granting wallet-funding authority. |
GOOGLE_CLIENT_ID |
Turns on Google sign-in. Its absence leaves the operator path. |
STRIPE_SECRET_KEY |
Turns on card payment. Its absence leaves operator-signed funding. |
STRIPE_WEBHOOK_SECRET |
Signing secret (whsec_…) of this deployment's Stripe webhook endpoint. With card payment on, it serves POST /v1/payments/stripe/webhook; without it the route is 404 and only the reconcile pass credits payments nobody presented. |
TAKOSERVER_STRIPE_CHECKOUT_MINIMUM_MINOR / TAKOSERVER_STRIPE_CHECKOUT_MAXIMUM_MINOR |
Smallest and largest single Checkout payment, in cents (defaults 500 and 1000000; bounded by Stripe's 50 to 99999999). |
TAKOSERVER_STRIPE_WEBHOOK_TOLERANCE_SECONDS |
How far a webhook's signed timestamp may be from this Host's clock (default 300, at most 3600). |
TAKOSERVER_STRIPE_RECONCILE_LOOKBACK_SECONDS |
How far back the reconcile pass reads completed Checkout Sessions (default 604800, seven days; 3600 to 2592000). |
TAKOSERVER_AI_BASE_URL |
HTTPS base path of an OpenAI-compatible upstream. |
TAKOSERVER_AI_MODELS |
JSON allowlist mapping public model IDs to upstream IDs, limits, and retail token prices. |
TAKOSERVER_AI_TOKEN_FILE |
Preferred rotatable upstream bearer secret file. |
TAKOSERVER_AI_TOKEN |
Direct upstream bearer secret when a file is not used. |
When explicitly enabled, POST /v1/selfhost/tenant-run-credentials is a
Host-specific authenticated HTTP route, not a route-less service or a
private-network API. It accepts only a resources:write organization API key
(never a browser session or another tenant-run token) and the exact JSON body
{"spaceRef":"…","runRef":"…","workerEndpointOriginReservationId":"…"};
the reservation member is optional. The organization is always taken from the
key, and a supplied reservation must still be live under that organization.
The returned bearer lasts exactly 300 seconds. Its dedicated Ed25519 key is
persisted as a 0600 file under TAKOSERVER_DATA_ROOT; revoking that key in
runtime_grant_keys takes effect after the existing verifier cache (10 seconds
by default), while expiry bounds every already-issued bearer to five minutes.
At boot and before each issuance, the Host proves that private key against the
exact active public half in the registry; a stale file, reused id, or revoked
row fails closed and is never overwritten or revived automatically.
The Cloudflare public Worker does not compose this route or import its signer.
The v2 artifact Forms never accept uploaded bytes. A create names an exact
HTTPS URL and SHA-256. For an unheld URL, the Bun Host fetches the manifest and
each file over anonymous public HTTPS and keeps the verified bytes. A held URL
is exclusive: the Host serves it only from bytes already in its object store,
to the organizations its configuration grants, and never falls back to public
HTTPS. It reads those grants at startup. scripts/selfhost-artifact.ts puts
held bytes there while the Host is stopped:
set -a; . /etc/takoserver/takoserver.env; set +a # the Host's environment
bun --no-env-file scripts/selfhost-artifact.ts seed worker-bundle ./dist \
--base-url https://artifacts.example.com/hello/v1/ \
--organization <organizationId> --entrypoint worker.js \
--config /etc/takoserver/v2/takoform-v2.json > hello-v1.jsonIt validates the files with the Form's own validator and stores them under
content addresses in the data root's object store. It prints the artifact
for the Resource spec and the exact config fragment that grants it, and with
--config merges that fragment into the config file. It holds the control
database's write lock while it runs, and refuses, before writing, a data root
that another process has open or that it cannot prove unused, one that is not
private to this user, an organization the control database does not know,
dotfiles unless --include-dotfiles, and a config that would no longer fit in
one environment variable. Run it as the Host's user in the Host's PID
namespace. Restart
the Host with the updated config, then create the ModuleWorker,
WorkerBundle, WorkerVersion, WorkerDeployment and WorkerEndpoint
Resources through /apis/forms.takoform.com/v2/resources. Steps 6 to 10 of the
install path give
the complete commands; steps 11 and 12 add SQLite, KV, bucket and queue
Bindings, and step 13 deletes everything in reference order.
scripts/selfhost-artifact.ts verify rechecks every configured entry
read-only, including while the Host runs.
The Bun API listener speaks HTTP, including when a front end terminates TLS.
The configured public origin still owns operator sign-in and existing-owner
proof audiences, and HTTPS deployments still issue and clear Secure session
cookies. Backend URLs and Forwarded / X-Forwarded-* headers do not select
that authority. Cookie authentication continues to require the exact configured
console Origin; TLS termination does not relax that browser boundary.
Put the control API behind a TLS front end. The HTTP listener binds
TAKOSERVER_LISTEN_HOST, 127.0.0.1 by default, so a front end on the same
machine reaches it over loopback and nothing else can. In a container or Pod
set 0.0.0.0 (or ::) and limit who can reach the port with a network policy
or firewall; see Self-host security. A
WorkerEndpoint 0.3.0 publication is served by the Host itself:
with TAKOSERVER_V2_WORKER_ENDPOINT_HTTPS=1 it binds TCP 443 on every IPv4
address, which needs root or CAP_NET_BIND_SERVICE, and DNS must resolve
*.<suffix> to the machine. A control API front end on the same machine
therefore uses another port, for example
TAKOSERVER_PUBLIC_ORIGIN=https://api.example.com:8443, or runs on another
machine with TAKOSERVER_LISTEN_HOST set to an address it can reach. Startup
refuses the selection without the certificate pair and suffix, or when another
self-host listener is already configured on 443.
The settings and rules in this subsection belong to the retained pre-v2
runtime (TAKOSERVER_WORKERD_PORT, TAKOSERVER_WORKER_ENDPOINT_PORT and its
WorkerEndpoint@0.1.0 publication). They do not apply to WorkerEndpoint 0.3.0, which uses the listener above.
Without a certificate the Worker socket speaks plain HTTP and the origin this
Host hands a Worker is http://, truthfully — an https:// address the runtime
does not serve is one nothing answers on. On the default localhost suffix that
is fine for the Worker's own identity. On any other suffix it is not: a Worker
that derives its public identity from the request URL establishes no origin over
plain HTTP on a name that is not loopback, so federation, signing, and
self-addressing cannot work there. The process says so at boot rather than
leaving it to be discovered.
A WorkerEndpoint is a published, portable address, and the released
WorkerEndpoint Form states what one may look like: https:// plus a dotted
name plus /. There is no plaintext address and no port. So this deployment can
create a WorkerEndpoint only when the address it would publish is that shape,
which means terminating TLS on the default port in one of two ways:
- In workerd. Set
TAKOSERVER_WORKERD_TLS_CERT_FILEandTAKOSERVER_WORKERD_TLS_KEY_FILE(or the two_CERT/_KEYPEM-text variables) andTAKOSERVER_WORKERD_PORT=443. Binding 443 needs the capability to do so. - Behind a front end. Terminate TLS on 443 in front of this machine, leave
workerd on whatever port it has, and say so with
TAKOSERVER_WORKER_ENDPOINT_PORT=443. The port then normalizes away and the published address is the portless one the front end really answers on.
For Caddy in front of the HTTPS workerd listener, disable upstream connection reuse. A Worker can reject a POST before reading its body; with the current workerd runtime and Caddy's pooled HTTP/1.1 upstream, the following request can fail with EOF / 502 before reaching the Worker. The compatible transport keeps each request independent, without replaying mutations:
reverse_proxy https://127.0.0.1:8788 {
header_up Host {http.request.host}
transport http {
keepalive off
tls_server_name runtime.apps.example.com
}
}The port and TLS server name are examples: use this deployment's actual workerd listener and a name covered by its certificate. Keep certificate verification enabled; configure the proxy's trust pool when using a private CA. Preserve the original request Host so workerd can route the assigned WorkerEndpoint, including on proxy versions that rewrite HTTPS upstream Host headers by default. This changes only proxy-to-workerd pooling, not browser keep-alive or the Bun API listener. Do not compensate with automatic POST retries or by draining attacker-controlled bodies in application authorization. See Caddy's HTTP transport reference.
Any other configuration — plain HTTP, or a port that is not the scheme's
default — still runs Workers, KV, SQL, queues, cron and buckets, and still
serves them on its own socket. It simply cannot mint a WorkerEndpoint, and it
says which of the two remedies to apply: at boot, and again in the refusal a
takoform_worker_endpoint create answers with. The loopback development default
is included: http://<script>.localhost is exactly as unpublishable as any
other plain-HTTP address. The boot line is not printed when
TAKOSERVER_V2_WORKER_ENDPOINT_HTTPS=1 selects the v2 listener: both remedies
put 443 on a port that startup then refuses beside it, and WorkerEndpoint 0.3.0 does not use this socket.
For a self-host QueueConsumer, changing the source queue or receiving Worker
requires replacing the attachment, as its Form declares. In-place updates may
change delivery settings or the dead-letter destination, but must retain the
same native source queue and Worker. A target change is refused before any
attachment is written; it must not leave both the old and new consumers active.
A restarted deployment brings its published Workers back by itself. The runtime is started at boot for whatever this machine had already published, before the API begins answering; a machine that has published nothing starts no runtime.
An accepted workerd child that crashes is restarted automatically with backoff from 100 ms up to 5 seconds. If a replacement fails readiness, or cannot be recorded because the crashed child is gone but the replacement is not proven to hold its listener (another process holds it, or the replacement's own process state cannot be read because it is already exiting), recovery waits for that exact child to exit and its listener to become vacant before spawning again. A delayed exit, temporary port occupancy, or a failed listener observation while waiting for vacancy keeps the retry pending; recovery never stops or replaces a foreign process. Serving is reported only after recovery passes readiness, and health requests only observe recovery. Deliberate shutdown cancels it. A failed first start, an unconfirmable child exit, or child-identity persistence that fails for any other reason, such as a crashed child that is not proven gone, an unreadable listener table, or a failed write, requires operator intervention rather than automatic retries.
SIGINT, SIGTERM and SIGHUP fence new work and drain accepted requests and background passes before stopping owned resources in order. A proved clean stop exits with status 0. A reported shutdown-stage failure prints its stage and exits with status 1, even when dependent listeners remain open. Shutdown does not close dependents below an unproved owner or discard its durable custody; the next Host must recover that retained ownership before serving.
The Host can also serve AI inference through /v1/ai when the operator
configures an OpenAI-compatible upstream (Settings). An API key
with ai:invoke sees only the configured public model IDs. Every chat
completion needs an Idempotency-Key, and streaming is refused. A settled
result is replayed from durable state without calling the upstream or charging
again. A model priced at zero never touches the wallet. For a priced model, the
Host holds the most the request can cost from the organization's prepaid
wallet, refuses with 402 insufficient_quota when the wallet cannot cover
it, captures the reported use and releases the rest; a failed upstream call
charges nothing. See
qualifying one public AI inference for the
explicit opt-in completion/replay check and its limits.
Creating, running or deleting a v2 Resource places no charge on the wallet. Operator sign-in and funding covers how a wallet is funded.
Sign-in and money are the two facts a server cannot determine for itself. With
no identity provider configured, the operator answers the first by signature —
so a fresh machine mints an operator key, keeps it beside its database, and
prints a single-use assertion good for ten minutes. The first session exchange
spends it. A boot prints a new one until the operator has signed in, or every
time with TAKOSERVER_PRINT_OPERATOR_ASSERTION=1
(Self-host security).
Later ones:
TAKOSERVER_OPERATOR_KEY=.takoserver/operator-key.jwk \
TAKOSERVER_PUBLIC_ORIGIN=https://api.example.com \
bun --no-env-file scripts/operator-key.ts sign-in google operator operator@localhost OperatorThe key is under the data root (.takoserver by default; the install guide
uses /var/lib/takoserver).
TAKOSERVER_PUBLIC_ORIGIN must be the Host's own origin: without it the
assertion is signed for http://localhost:8787, and the Host refuses it.
The legacy/generated key also credits the wallet
(operator-key.ts funding <org> <ref> <minor>) until Stripe is configured. An
explicit TAKOSERVER_OPERATOR_IDENTITY_PUBLIC_JWK is login-only: it takes
precedence for operator sign-in but cannot verify funding assertions. This is
the operator vouching in a form the server can check and nobody else can forge
— not an identity provider, and it stops being the way in the moment a real one
is configured.
A hosted Cloudflare deployment enables customer card funding only when its
private deploy target explicitly sets "stripeCheckout": true and the
Worker already has a STRIPE_SECRET_KEY secret. The target contains no secret
value: the owner preflight fails if the named capability has no secret, while a
lingering secret alone cannot expose Checkout when the target capability is
absent. The webhook is gated the same way: "stripeWebhook": true (only with
stripeCheckout) requires a STRIPE_WEBHOOK_SECRET secret, and
"stripeSettings" carries the four amounts and windows above as
checkoutMinimumMinor, checkoutMaximumMinor, webhookToleranceSeconds and
reconcileLookbackSeconds, validated before deploy.
The Hosted billing API proposal describes planned idempotent top-ups, next-period subscription changes, personal Organizations and Workspace usage attribution. It distinguishes existing contracts from the extensions awaiting implementation and staging acceptance.
Starting a Checkout needs a console to send the payer back to: without a console origin the Checkout route is not served, while the key still verifies a presented session and runs the webhook (where configured) and the reconcile pass.
A payment is credited however the Host learns of it, and only once. The
console's return presents the Checkout Session id; the Stripe webhook
(checkout.session.completed and checkout.session.async_payment_succeeded)
names it; and a reconcile pass, every five minutes on both the Bun Host and the
Worker cron, lists completed sessions inside the lookback window. Each path
re-reads the session from the Stripe API — whether it is paid, the amount, the
currency, and the organization written into its metadata at creation — and
credits through the same stripe:<session id> funding reference, so the
ledger's uniqueness makes every path and every redelivery credit once. A
session naming an organization this deployment does not have credits nothing.
Webhook events are deduplicated by Stripe event id in stripe_events; types
reserved for subscriptions, invoices and refunds (invoice.paid,
invoice.payment_failed, customer.subscription.updated,
customer.subscription.deleted, charge.refunded) and any other type are
recorded and acknowledged without acting on them. Point the Stripe endpoint at
https://<public origin>/v1/payments/stripe/webhook; one Stripe account and
mode should serve one deployment.
No path credits a payment that is no longer there: the session is read with its
PaymentIntent and latest charge, and a refunded (in any part) or disputed
charge, or one that cannot be read, credits nothing. The webhook and the
reconcile pass credit only one-off (mode=payment) sessions this Host created,
which carry metadata[takoserverPurpose]=wallet_funding; sessions created
before that marker existed are credited only when the console presents them.
Paid sessions that could not be credited (another currency, an organization
this Host lacks, an unreadable payment) are logged by funding reference as
takoserver.stripe.uncredited_payments.
The operator can also run the reconcile pass on demand, for example where the
schedule has not fired: POST /v1/operator/payments/reconcile with
{"assertion":"…"} runs the same pass and returns its report (sessions
examined, credited and already credited, skips by reason, the funding
references it credited, and the paid ones it could not credit with their
reasons; never an amount or an organization). It is served only where the
reconcile pass is (a Stripe key is configured) and an operator identity key
is present, and is 404 otherwise. The credential is a payments-reconcile
assertion signed by the key that verifies operator sign-in
(OPERATOR_IDENTITY_PUBLIC_JWK on a Worker,
TAKOSERVER_OPERATOR_IDENTITY_PUBLIC_JWK on a Bun Host, else the
legacy/generated operator key); an identity-only key suffices, because the
pass credits only what Stripe reports as settled. It names this Host's
origin, lives a minute and runs one pass; no session or API key can start it.
A repeat credits nothing twice, and on a Bun Host a pass already running is
joined rather than repeated (a Worker runs one pass per request). If Stripe
cannot be read the answer is 503 unavailable; sign a fresh assertion to
retry:
assertion=$(TAKOSERVER_OPERATOR_KEY=… TAKOSERVER_PUBLIC_ORIGIN=https://api.example.com \
bun scripts/operator-key.ts payments-reconcile)
curl -sS -X POST https://api.example.com/v1/operator/payments/reconcile \
-H 'content-type: application/json' -d "{\"assertion\":\"$assertion\"}"
The webhook records every delivery in stripe_events, so it needs migration
0091_stripe_events.sql applied first. A Bun Host migrates its own SQLite. On
Cloudflare, the current USD-micros Worker requires the complete audited 0098
schema, 0094 money-unit policy, and selected-mode immutable Stripe realm pin
even when the webhook and billing options are disabled. The
staging-only --fresh-lineage=v2-0091 and v2-0092 profiles prepare fixed
historical D1 payloads; neither can serve this Worker. The staging-only
--fresh-lineage=v2-0100 profile creates a new plain 0001–0100 D1 and claims
the test realm in the same import, so this Worker starts on it; routine
publication still needs the separate Auth103 step (deploy guide).
The staging-only --fresh-lineage=v2-0100-money-preserved profile creates a new
Money-preserved 0001–0100 D1 with that claim and one fresh Writer epoch row in
the same import; its Worker bootstrap is not yet qualified.
Source audit does not authorize applying 0098 to a protected D1; existing-data
cutover and old-writer quiescence require separate qualification.
By default, durable state is rooted under .takoserver, but that directory is
not always the complete backup set. Stop Takoserver, its child workerd, and
every other configured writer before copying the complete root. Include an
external TAKOSERVER_DB and any existing SQLite -wal, -shm, or
rollback-journal sidecars from the same quiescent window. Preserve protected
external configuration and key material needed to restore the same identity
separately: the exact TAKOSERVER_TAKOFORM_V2_CONFIG, the
TAKOSERVER_TAKOFORM_V2_CURSOR_KEY, the private-plane key files and the native
binaries, among others (the full list is in
What belongs to one installation).
Do not store secrets in this repository, and do not copy the runtime-only
socket directories such as /tmp/tw-*. See the
self-host backup and restore preparation for
the procedure and its limits.
.takoserver/
control.sqlite organizations, keys, the ledger, resources
signing-key.jwk signs data-plane tokens
operator-key.jwk signs operator assertions
objects/ seeded held artifacts under operator-held/; pre-v2 objects and uploaded bundles
databases/ pre-v2 SQL databases
v2-sqlite-databases/ v2 SQLiteDatabase files
v2-kv-namespaces/ v2 EdgeKVNamespace data
v2-object-buckets/ v2 ObjectBucket objects
v2-worker-owners/ v2 Worker runtime owners
v2-runtime/ v2 Actor and Workflow runtime state
s/ per-start private sockets; not needed in a backup
runtime-probes/ verified workerd snapshot and transient resolver probes
workers/ pre-v2 published Workers and their workerd config
The tree is a map, not a backup list: a backup copies the whole root.
Published Workers are served by a workerd process Takoserver starts on the first
publish and leaves watching its configuration — a deploy rewrites the config,
and no other tenant's in-flight requests are dropped for it. A machine without
the exact artifact, digest, and executable closed-graph resolver capability
does not advertise or activate Worker serving, rather than recording a false
serving state; storage and databases remain independently available. At boot,
Takoserver copies the verified bytes into its private data root and executes
that snapshot for both inspection and serving. Replacing the configured input
path therefore cannot silently change the runtime identity of an already
running process.
On the supported Linux/x64 Worker path, /usr/bin/setpriv, /bin/sh, and
Linux /proc are also required: the child is bound to the Bun Host's lifetime
and its TCP listener must belong to that exact child. If these prerequisites
are missing, Worker serving fails closed; the Host's storage paths do not
require them.
A Worker that declares static assets gets them through Host-private asset
services; no native ASSETS binding is exposed to tenant code.
runWorkerFirst and notFoundHandling decide routing and fallback, so a
single-page application survives reload without adding an undeclared tenant
capability.
The public control-plane Worker never receives a Cloudflare parent credential.
Reviewed Cloudflare Edge or ObjectBucket supplies require one exact route-less
provider-executor topology; the public Worker holds only its typed service
binding and non-secret supply projection. The executor alone receives
CLOUDFLARE_API_TOKEN and the runtime-input sealing keyring from its canonical
operator-private secrets file. Apply, import/adoption, and their recovery paths
are bound to the exact active Host saga lease and executor-owned pre-effect
claim. Observation, artifact-consumption readback, and upstream usage meters
are bound to the exact tenant, Offering, provider installation, and recorded
Deployment before the executor uses its parent credential. Historical released
provider adapters may remain installed to observe and delete recorded
Deployments, but their beta Forms are not republished as a sale catalog.
bun run deploy -- --contract prints the
side-effect-free split deploy contract. Every operation then names one surface,
one action (--status or --apply, and on the credential surfaces their own
--issue, --mint and --revoke), an exact environment, and an exact 40-hex
commit; there is no mixed controller, plan, ledger, journal, or target override.
See docs/deploy.md for the surface list, dependency order,
the clean-checkout integration target realization path, private inputs, and
failure rules. In particular, do not copy the retired
.deploy/target.staging.json shape into a current checkout: the current v2
target joins the Cloudflare supplies, executor, gateway, and receipt-authority
identities atomically. The landing-page details are in
docs/deploy-site.md.
The one-time public parent-credential handoff is owned by the private
composition's takoserver-public-parent-token-retirement surface, exported
through @takoserver/core/deploy-extension; it is deliberately absent from the
public contract above, so this checkout's bun run deploy refuses it. It
qualifies the exact route-less executor and public Worker closure, releases the
exact service binding if needed, then removes only the public
CLOUDFLARE_API_TOKEN; its value-free status is the sole
lost-acknowledgement/adoption path. It does not read or alter the executor's
owner-private credential file. Run integration first; rehearsal and production
remain separate target/source-qualified lanes.
Wasabi has no equivalent private executor. Every Wasabi supply or recovery offering therefore fails target parsing/public composition closed; its access key, secret key, provider, and parent-backed meter are never public Worker bindings. Self-host storage remains independent of that hosted restriction.
The official operator-private deploy target may declare aiModels,
objectBucketSupplies, and whether hosted sponsorship is enabled.
Sponsorship adds only the product-owned bearer secret required by that API; it
does not add a service binding or an external entrypoint to the Takoserver
Worker.
objectBucketSupplies is a closed, non-secret operator composition tying one
exact current ObjectBucket Form to a Provider Installation, Supply Contract,
price plan, and embedded-binding delivery. It accepts only operator-internal
provider access; native-credential retail is not inferred from that document.
Realization writes it to TAKOSERVER_OBJECT_BUCKET_SUPPLIES and separately
requires the provider's provisioning secrets before publication. aiModels is
the exact public-model to upstream-model mapping, limits, and retail token
prices. Deploy realization and
immutable Worker Version readback require the exact D1, R2, and secret-name
closure and reject unexpected bindings.
The read-only surface-specific --status path proves the applicable exact D1,
R2, secret-name, domain-owner, deployment-history, and binding closure, so a
stale operator target or an incompletely wired live Version cannot masquerade
as healthy state.
Takoserver's public protocol and standalone path do not depend on Takosumi.
Stable Worker Forms with an omitted or empty requiredSensitiveVars
declaration provision normally. A configured Cloudflare provider can also
accept a non-empty declaration through Takoserver's RuntimeInputAuthority, up
to 64 names when TAKOSERVER_RUNTIME_INPUT_SEAL_KEYRING is configured. Before
the Resource graph exists, an organization key reserves the future canonical
HTTPS origin at
/v1/worker-endpoint-origin-reservations/{reservationId}. Takoserver selects
and records the same exact sold ModuleWorker Offering and provider placement as
ordinary Host mutation; an omitted Offering is accepted only when that
selection has exactly one eligible result. The reservation is value-free and
does not create a Takoform Resource or call the provider. A key that reserves
nothing still gets a WorkerEndpoint where the selected installation derives
its own endpoint address. The managed Workers-for-Platforms provider derives
an opaque default label from the organization, Space and Worker name beneath
its configured managed base domain. Self-host and the ordinary-workers
adapter use their own endpoint suffixes. The Host reserves that derived origin
on the caller's behalf, in an id namespace the public routes refuse every
write to, and releases an old endpoint or advances a moved Worker revision
only under the existing ownership and absence fences.
A supplied chosen-name reservation still takes precedence; default-name allocation never takes over a caller's live reservation. No extra Form field, Host API header or provider configuration is needed to obtain a default URL. Choosing a custom label is a separate control-plane flow: the current Cloudflare public Host accepts that reservation through an admitted tenant-run credential, not an ordinary organization key's Resource request. Preparing a chosen-name reservation with an organization key alone does not attach it to that key's subsequent WorkerEndpoint create.
The sensitive half of a Worker Version travels separately, over
PUT|GET /v1/takoform/worker-runtime-input-preparations/{operationKey} speaking
takoserver.worker-runtime-input-preparation@v2. The operation key is the exact
Idempotency-Key the ordinary public apply will carry, so one key names both
halves of the same mutation. The private request states this Host's own
canonical public origin and commits to the exact public apply it authorizes —
method, path, If-None-Match: *, and body — and Takoserver recomputes that
commitment and echoes it back, so a caller can prove the Host bound the values
to the request it meant. A resources:write organization API key retains the
organization-wide preparation surface. The self-host runner instead reuses its
already-admitted tenant-run credential for PUT|GET: Takoserver opens the exact
create-only WorkerVersion apply, pre-binds the handoff to its Space, and returns
operation_not_found for an unknown or different Space. DELETE remains an
organization-key operation. Replaying the key with a different apply or
different values is a conflict, not an overwrite. The provider lease claims
that exact identity before any asset or Worker Version mutation, erases
ciphertext in the dispatch CAS, and settles only after provider readback. GET
is the value-free recovery read: it never returns a value and never asks the
caller to send the secrets a second time.
Activation later proves the released, Ready WorkerEndpoint, its exact worker
relation, provider placement, and canonical output. Deactivation retains the
endpoint UID as a deletion witness: the endpoint and its provider deployment
must be closed before the reservation can be released and its origin reused.
TTL expiry does not unlock an origin while that deletion witness is retained.
A self-hosted machine takes the same path when its operator configures
TAKOSERVER_RUNTIME_INPUT_SEAL_KEYRING and serves the deployment at an
https bare TAKOSERVER_PUBLIC_ORIGIN, and only then: the capability is
derived from the lease port's presence, so a machine with nowhere to seal a
value advertises a ceiling of zero and admission refuses the declaration with
unsupported_capability before anything is provisioned. The canonical public
origin is HTTPS-only on both sides — the released Takoform provider refuses any
other scheme before it sends a value, and openapi/takoserver.openapi.json
documents the same — so the default http://localhost:8787 development origin
carries no sensitive runtime inputs; the entry point says so on startup and
composes no preparation route. The retired-ObjectBucket drain mode composes no
lease port either, and therefore also serves no preparation route. Nothing is
auto-generated in its place — a key kept beside the ciphertext it protects is
not encryption at rest. workerd has no secret binding type, so a delivered value
is projected as an ordinary environment binding into a 0600 configuration
under a 0700 directory, recorded in a 0600 file beside — never inside — the
immutable version directory, and never written anywhere else.
The declaration's names remain in the Worker Version spec; values do not enter
portable state. Realization places the other non-secret values in Worker vars.
AI uses the native Workers AI binding, so it does not copy an account API token
into inference requests. CLOUDFLARE_API_TOKEN remains provider-executor
authority, not tenant runtime input. Managed R2 S3 credentials and the receipt
proof secret enter only the route-less receipt-authority Worker's atomic
--secrets-file publication; there is no public S3 credential issuer. Deploy
preflight refuses an enabled capability whose required secret is absent. With
no such target fields, /v1/ai and ObjectBucket retail stay absent rather than
using a demo backend.
See docs/adr/0001-provision-from-the-worker.md for the superseded public-credential decision and the current route-less provider-executor boundary. The reservation and RuntimeInputAuthority boundary is recorded in docs/adr/0004-runtime-input-authority.md, and the runtime-input wire contract this Host now speaks in docs/adr/0006-runtime-input-wire-contract-v2.md. All recorded decisions are indexed in docs/adr/.
The implementation notes below describe retained pre-v2 provider, data-plane and recovery code. They are not the v2 support catalog or instructions for creating v2 resources. Package admission and old native Host tests must not be used as v2 readiness evidence. Existing v1 records are preserved and pending repair work continues internally; the old public Takoform HTTP handler is not mounted.
Current managed object storage is one exact portable chain: the versionless
edge.forms.takoform.com/ObjectBucket Resource provides edge.objects, and a
Worker Version consumes it only through the exact
module-worker.object-bucket Binding declared in bucketBindings. That Form is
supported and activated by the code-owned implementation catalog
(ADR 0007);
a Host may execute it only where its deploy target realizes an ObjectBucket
supply. Provider bucket names, regions, endpoints, credentials, and supply
documents remain inside the selected Provider Pack and Deployment; none is
Resource desired, observed, output, discovery, or Worker binding state.
Both Cloudflare Worker backends carry that Binding. When explicitly enabled for
development, the OSS ordinary-workers backend uploads the tenant's exact bundle
bytes with no wrapper, so the declared name carries Cloudflare's native R2
binding. The separately supplied managed backend exposes the same nine-method
edge.objects facade while
keeping provider-native capabilities out of the tenant environment. Its
operator owns multipart recovery, retention, and destruction reconciliation;
uncertain provider effects are not reported as success or retried blindly.
No bucket name, region, endpoint, credential, or provider authority enters the
Takoform contract. ADR 0007 records the original implementation decision and its divergence from
ADR 0005.
Takoserver serves no public S3-credential or managed standard-service retail
route. Separate S3 retail is not composed by default, and provider credentials
alone never authorize it. Private R2 and S3 transports are implementation
adapters behind edge.objects, not alternate public contracts.
The released provider-v2.1.1 Edge Family remains immutable historical input so
Takoserver can observe and delete Deployments already recorded under it. Its
v1beta1 ObjectBucket identity is recovery-only and is never installed as a
current sale, authoring, or /provision/v1 authority.
Ordinary Bun always keeps the stable self-host Provider3 execution pack.
CLOUDFLARE_ACCOUNT_ID may separately back an explicitly reviewed ObjectBucket
supply, but it is neither provider-selection nor resale authority and does not
switch stable Forms off.
The released Cloudflare ObjectBucket provider survives only as an explicit
recovery lane for observing and deleting its already-recorded beta
Deployments. An operator enters it with
TAKOSERVER_RETIRED_PROVIDER_MODE=cloudflare-object-bucket-drain plus the
Cloudflare account credential and TAKOSERVER_PROVISIONER_TOKEN. That lane
publishes zero current Offerings and cannot be mixed with self-host provider
settings. TAKOSERVER_ZONES is rejected because an ObjectBucket drain owns no
DNS or Worker-route authority; the old implicit TAKOSERVER_EDGE_FORMS switch
is rejected too. Recovery-mode credentials are validated before local state is
opened.
For a disposable Provider v3 integration run against the production Cloudflare adapter and an in-process account, use the loopback-only stable Host launcher:
TAKOFORM_STABLE_CATALOG_ROOT=/path/to/exact/takoform-v3.0.0 \
TAKOSERVER_STABLE_LOCAL_TOKEN=local-test-token-at-least-16-characters \
bun run debug:stable-local-cloudflare-hostThe catalog loader verifies the frozen 31-Form input before listening. The
launcher installs the historical 13-Form union used by the Road to Me and
Yurucommu local graph fixtures. Its StaticAssetBundle path
uses the production Cloudflare Provider upload protocol and serves the realized
asset manifest through the disposable workerd runtime, including Worker-first
and single-page fallback behavior.
This diagnostic profile does not support ObjectBucket and accepts only Worker
Versions whose requiredSensitiveVars declaration is omitted or empty. It is
therefore insufficient for the current Yurucommu Provider 4 graph, which needs
both capabilities; a passing older fixture is not a full current-app E2E.
This command is not a deploy path. It binds 127.0.0.1
on an ephemeral port by default and prints one sanitized ready JSON line
without the token. TAKOSERVER_STABLE_LOCAL_SPACE and
TAKOSERVER_STABLE_LOCAL_PORT may override the local Space and port.
The reseller run bearer, Attachments and Migrations below act only on pre-v2
inventory records. Their /v1 routes are still served, but the run bearer
authenticates only to the pre-v2 Host API, whose handler is not mounted, so a
minted bearer opens nothing. Host API v2 has none of them.
- Run the ordinary Takoform provider without handing a hosted runner the reseller's organization API key. A reseller reservation can mint a short-lived bearer pinned to one opaque tenant, exact Form, and exact Resource name. Before capture it can only validate, prepare, and create that paid address. After capture, a new bearer additionally pins the immutable Resource UID before it may read, observe, update, or delete that incarnation. The first create atomically marks the reservation consumed before provider side effects, so a release can never leave an unpaid Resource.
- Attach independent resources by exact Interface reference. An Attachment stores only resource/deployment identity and an opaque grant, endpoint, secret, or native-binding reference; it never embeds a provider credential.
- Migrate by selecting another Offering for the same exact Form. Takoserver provisions a candidate Deployment, transfers and verifies the data, then atomically switches the active Deployment and Attachment resolutions. The source stays retained for the bounded rollback window.
Takoform owns the portable words: Form, Interface, Binding, Attachment, and Migration semantics. Takoserver owns the supply decisions: Offerings, provider installations, Deployments, commercial authority, placement, credentials, metering, and cost.
One exact Form may have many Offerings. A logical Resource keeps one stable UID
while one or more provider-backed Deployments coexist during migration. Provider
IDs exist only on Deployments; provider names and prices never enter a Form.
Deleting either side of a live Attachment fails with dependency_in_use until
the Attachment is removed.
Migration planning accepts no caller-invented payment claim. The target Offering must be backed by one active, exact-digest reservation of quantity one; the reservation is unique to the Migration and is captured only after cutover. Cancelling before cutover first deletes any authoritative candidate Deployment and only then releases the hold. An acknowledgement gap is left open for operator reconciliation rather than being reported as a successful cancellation.
Seven provider-neutral ports; the SQL port is shown at both capability levels below, and everything above them remains provider-neutral.
| Port | Implementations |
|---|---|
Sql |
SQLite, D1 binding (including atomic batch) |
SqlAccess |
D1 over HTTP (query/run only; no atomic batch) |
ObjectStore |
filesystem, R2 binding, R2 over HTTP, memory |
Provider Pack |
provisioning, Attachment, transfer, credential, meter, and cost capabilities |
ExternalIdentityVerifier |
Google ID tokens, operator signature |
FundingSettlementVerifier |
Stripe, operator signature |
PaymentEvents |
Stripe webhook and reconcile pass |
AiGateway |
any OpenAI-compatible upstream; the Worker entry uses its native Workers AI binding |
scripts/check-imports.ts enforces the layering as a gate rather than a
convention: core, adapters, domain, routes, composition, entries — and a
per-entry ban, so the Workers entry cannot reach a filesystem it does not have.
Provider embedders use the curated @takoserver/core/provider-extension source
export at their selected exact package/source pin. The shared Cloudflare adapter
does not construct the managed WfP implementation. Its managed workerBackend
selection supplies a synchronous create(context) factory, called once at
construction with the adapter's resolved identity, credential callback, artifact
reader and an independent cloned Offering snapshot. Namespace, gateway and
installation configuration stay in the operator's factory closure. A missing,
asynchronous, incomplete or wrong-kind backend is a startup error, never an
ordinary-Workers fallback; the deprecated ordinary endpoint suffix cannot be
combined with that factory.
Ordinary Cloudflare Worker writes are disabled by default. A development harness
must explicitly select workerBackend: { kind: "ordinary-workers", allowDevelopmentWorkerWrites: true } to create or update Workers through this
adapter; a workerEndpointSuffix alone does not enable writes. Existing
WorkerVersion operations may still recover by readback without that opt-in, and
legacy Workers remain observable and deletable. Managed customer ModuleWorker
supply on Cloudflare requires the separately supplied Workers for Platforms
backend, not ordinary account Worker scripts or workers.dev. This boundary does
not itself qualify that managed backend or admit its Forms.
Mutation failure codes do not prove that nothing changed. A Provider may return
failedWithoutProviderMutation(operationId, code, message) from this extension
entry only when its exact initial call accepted no native mutation. The proof
is bound to that returned ticket object and operation id; cloning, serialization
and later polling do not carry it. Other failures retain the operation for
reconciliation, including non-retryable failures and exceptions after entry.
An earlier runtime-binding callback or SQLite migration also prevents a later
provider-only refusal from proving the whole attempt idle. When the attempt is
provably idle, its wallet hold and failed-operation cleanup commit together;
otherwise the hold stays with the recoverable operation.
An embedding that implements TakoformResourceDriver directly can use
ProviderMutationDefinitiveRefusalError from @takoserver/core for its own
initial pre-effect rejection. Provider Pack methods use the returned-ticket
helper instead; throwing that class from a Provider method is not proof.
Neither helper adds a field or version to the public Takoform API.
The separate @takoserver/core/provider-extension/selfhost entry exports the
experimental createDockerHttpRevisionRuntime and createSelfhostContainerRuntime.
Node/filesystem adapters stay out of the portable provider-extension, which
exports the runtime-neutral createHttpRevisionServing coordinator.
The Docker primitive reconciles an application-selected image
pinned by SHA-256 on an operator-selected Docker socket and isolated network,
with explicit memory, CPU and PID ceilings. Exact revision names and labels
allow recovery without creating a second container; old and new revisions may
coexist, and deletion addresses only the selected native revision. Its caller
must still own resource admission, desired revision, traffic cutover and
retirement. The opt-in self-host composition connects these responsibilities
to the existing Host lifecycle for a caller-selected, verified exact
ContainerService@0.1.0 Form. The current released Form catalog contains no
ContainerService: configuring Docker alone does not publish, install, activate
or offer one. The local Form-only qualification fixture is unpublished and
does not provide a portable HTTP Interface or Binding. See
local Container backend
for configuration and the remaining publication boundary.
This is an in-process composition seam, not another Host API or serialized
credential format. The public Worker imports only the credential-free executor
RPC port and pure wire validation; the execution authority stays behind the
operator's route-less executor. Concrete WfP runtime and its dedicated deploy
surfaces belong to the non-public implementation, not this package. The
@takoserver/core/deploy-extension export shares deploy primitives without
copying the Host's lifecycle code. The @takoserver/core/testing export supplies
shared composition fixtures to downstream conformance tests; neither export
defines another network API or Form version.
Existing shared D1 migration history remains here, with one schema owner. A managed extension may bind that same operator-selected database, but it does not copy the migration lineage or become a second schema writer. Source separation alone is not proof of a deployed managed installation.
Workflow Host embedders have three separate source-library entries:
@takoserver/core/workflow-runtime exposes the platform-neutral coordinator and
ports; @takoserver/core/workflow-runtime/transport exposes an in-memory,
one-use frame journal, HTTP turn controller, and existing bundled trusted
bootstrap source/digest to Host adapters;
@takoserver/core/workflow-runtime/workerd exposes trusted Bun/Linux guarded
execution, resolved-Version graph compilation and selected-Version preparation.
None is a provider extension or an app-facing Binding.
See Workflow runtime
for selection and lifecycle responsibilities. These entries do not ship a
managed companion image, enable Workflow support, or change any API/Form version.
Three properties are worth knowing before reading the code:
Shipped Form definitions come from exact Takoform bytes. Takoserver cannot
author a name in the Takoform namespace. bun run check:form-corpora pins
both the canonical source commit and the generated catalog bytes, while still
pinning immutable provider-v2.1.1 history used to drain old records. A current
sale additionally needs an implemented backend and explicit operator supply;
pinning it here does not mint or promote a Takoform release. AI and S3 protocol
operations do not become Forms just because Takoserver offers them.
Guarded writes and atomic batches. The control database has no interactive
transaction, so invariants live in WHERE clauses and are verified by counting
the rows a write actually changed. SQLite and the Worker D1 binding also expose
an all-or-none Sql.batch for multi-statement commits. The D1 HTTP maintenance
adapter intentionally exposes only SqlAccess (query/run); it cannot claim
that atomic capability. A hold that cannot be covered is not written at all.
Failure is a value. A provider returns a classified ticket rather than throwing, so the engine, the ledger, and the operation record all see the same outcome. A credential we misconfigured is never reported to a customer as their permission problem.
bun run check # format, lint, layering, types, tests, builds
bun run fmt # the only thing that rewrites source
bun run check is the gate. It is read-only and it does not skip.
Its test step (bun run test, scripts/run-tests.ts) runs every test file in
parallel worker processes. Each file gets its own global scope, module registry
and TMPDIR, so a module mock or a host-wide lock in one file cannot reach
another, and the step fails unless Bun's summary covers every test file. The
worker count is half the CPUs the process may use (affinity and cgroup quota),
leaving room for the workerd and Miniflare processes the tests start;
TAKOSERVER_TEST_WORKERS overrides it. tests/test-timings.json only orders
the files, slowest first.
While developing, bun run test:changed runs only the files Bun's import graph
links to your changes against origin/main (bun scripts/run-tests.ts --changed=<ref> picks another base). It is a fast check, not the gate.
After each run the step prints the slowest files, the summed per-file time and
how busy the workers were, flags any file far slower than its recorded time,
and writes the run's per-file durations to a path outside the repository;
copying that file over tests/test-timings.json refreshes the ordering.
Fixtures that need a migrated SQLite database copy one built from the real
migration chain (tests/helpers/migrated-database.ts) instead of replaying the
chain for every test; built images are shared for the length of one run only.
A green gate is not a rollout. bun run check and local native evidence do not
show that any deployment works: not a self-host's public TLS and DNS, not the
hosted service's D1 or Workers for Platforms, and not adoption by downstream
Providers. Each is qualified separately.
tests/takoform-v2-host-api-conformance.test.ts runs Takoform's own Host API
v2 HTTP baseline probe against a real bun src/entry-bun.ts process on
loopback, once each for SQLiteMigrationSet, WorkerBundle and
StaticAssetBundle. It is part of bun run check. The probe is vendored
byte-exact from Takoform and pinned by size and SHA-256 in
vendor/takoform/host-api-v2-conformance/;
drifted bytes are refused before it runs.
The opt-in companion runs the same probe against the complete local Worker
profile's Forms that need no other Resource (ModuleWorker, SQLiteDatabase,
EdgeKVNamespace, ObjectBucket, AtLeastOnceQueue and the three artifact
Forms). It needs the pinned workerd, the Workflow guard and permission to bind
443, so run it in a disposable network namespace:
unshare --net sh -c 'ip link set lo up && \
TAKOSERVER_V2_ENTRY_NATIVE=1 \
TAKOSERVER_WORKERD_BINARY=/absolute/path/to/closed-graph-workerd \
TAKOSERVER_WORKFLOW_EXECUTION_GUARD_BINARY=/absolute/path/to/workflow-execution-guard \
bun test tests/takoform-v2-host-api-conformance-native.test.ts'A pass means only that the probe's mandatory sequence passed for those fixtures. The probe reports restart durability, fault injection, concurrency, cross-principal authority, optional features and Form-specific behavior as not tested, and neither lane qualifies Hosted D1, Cloudflare or a deployment.
These commands target the retired Host API v1 and do not work against the current entry, which serves only
forms.takoform.com/v2. They are kept as a record of earlier evidence, not as current checks. No releasedterraform-provider-takoformspeaks Host API v2 yet (v4.1.0 is v1-only); v2 provider support is that repository's work, not Takoserver's.
bun run test:selfhost-opentofu (scripts/selfhost-opentofu-e2e.ts) was a
native journey for OpenTofu 1.12.5 and provider 4.0.0 against the v1 Bun Host:
Core-verifier Form admission, EdgeKVNamespace and SQLiteDatabase CRUD with a
no-change plan, and optionally a nine-resource workerd Worker journey. It waits
for /.well-known/takoform/v1 on a plain-HTTP origin, both of which the current
entry refuses. bun run conformance:takoform-v1
(scripts/conformance-takoform-v1.ts) runs the frozen stable-v1 suite against
the separate v1 conformance entry and says nothing about the v2 Host.
Report a vulnerability privately, as SECURITY.md describes, not in a public issue. Hardening a self-host is in Self-host security.
GNU Affero General Public License v3.0.
Run it, host it, change it, sell it. The one obligation is the one that matters for something people reach over a network: if you offer a modified takoserver as a service, its users are entitled to your modifications. Self-hosting for yourself carries no such duty.