Deployment
Stacks, release choreography, and the infra.ts order.
Terraform owns the declared Auth0 infrastructure and Alchemy owns the declared Cloudflare resources. Runtime agent
applications and owner-managed Auth0 registrations have separate owners.
Start with your own deployment; the checkout’s SETUP.md contains
the full provisioning procedure and docs/deployment.md inventories the stacks and settings.
Hostnames such as api.tarnandtimber.com below identify the existing personal deployment. Replace them with your
origins when adapting the project; they are not neutral defaults or a production-service promise.
Stacks
| Resource | Where | Notes |
|---|---|---|
| API Worker | infra/alchemy/api.ts |
Custom domain api.tarnandtimber.com; declares AccountWriter and ConversationHub and the hourly cron; binds the shop database and its secrets |
| MCP Worker | infra/alchemy/mcp.ts |
Custom domain mcp.tarnandtimber.com; binds the API service |
| Storefront Worker | infra/alchemy/shop.ts |
Binds the shop database, the API’s two Durable Object classes by script name, the API service and the secrets its own capabilities need; never the hub callback secret or a cron |
| Backoffice | infra/alchemy/backoffice.ts |
Its own BackofficeDatabase and the API service; declared only when BACKOFFICE_ORIGIN is set |
| Docs site | infra/alchemy/docs.ts |
Static assets from apps/docs/dist on docs.tarnandtimber.com |
The database, Durable Object and Worker logical IDs never change. Renaming one creates a new resource, and a replaced Durable Object namespace loses its data.
Poppy settings
POPPY_AUTH_ISSUERon the MCP Worker is the issuerpoppy.jsonpublishes asauth.issuer: the canonical MCP resource names it in its protected resource metadata. The issuer fronts both standard OAuth and Poppy flows: ordinary MCP OAuth clients receive an Auth0 token without a Poppy Session; Poppy flows establish Sessions. It is also the exact issuer of the Auth0 proxy Session Tokens the server accepts.company.run.tsfails the plan when the MCP Worker’s value differs from the Auth0 proxy’s configured issuer.- DPoP. The API gets a generated
DpopNonceKey(DPOP_NONCE_KEY). Accepted proofs are rows in the D1 tabledpop_proofs(key thumbprint,jti, Session, expiry), deleted by the hourlyclean_dpop_proofsjob once expired. - Retry replay.
conversation_message_idsstoresresponse_statusandresponse_body, written once with the first response to a message ID. - Discovery.
apis[openapi].urlin the Auth0 proxy’spoppy.jsonis<api>/poppy/openapi.json, the Poppy view of the contract, andinfra/alchemy/composition.tschecks it. Poppy agents call/v1with DPoP.
Shop API runtime
enable_request_signal.infra/alchemy/api.tsgives the API WorkerAPI_COMPATIBILITY, the shop’s compatibility settings plus this flag, sorequest.signalaborts when a client disconnects and conversation streams stop. The other Workers do not get it.- Hub callbacks. The hub calls back at
/internal/conversations/{id}/turnand/internal/conversations/{id}/handoff-timeoutwithConversationHubSecret.
Release choreography
One deploy can’t safely switch a consumer to something deployed later in the same run, so a cutover between Workers uses three releases.
Expand
Add the receiving side, such as an API route, an Auth0 grant or an Action secret, while every consumer still uses the old path. Deploy and verify.
Switch
Flip the consumer: the storefront code path, the backoffice client, an Action URL or the Poppy discovery document. Deploy and watch.
Contract
Deploy order in infra.ts
- Terraform state backend
- Sign-in screen assets
- Email sending domain
- Auth0 (Terraform), Auth0 email (Terraform), Okta (Terraform)
- Proxy Auth0
- Docs site
- Shop, including the API and MCP Workers, and the backoffice
Terraform values flow into the later steps as outputs, never by copying.
This docs site
The site is a static build of apps/docs served as Worker static assets with trailing-slash dropping, on
docs.tarnandtimber.com. A check in pnpm test builds it, runs Blume’s doctor and renders every Mermaid diagram in
headless Chromium, failing on diagram errors or broken internal links.
Keys shared by the API and the storefront
Both Workers run account and payment operations against the same rows, so they open each other’s sealed values:
AccountKeyRootis created once by Alchemy and bound to both asACCOUNT_KEY_ROOT. HKDF-SHA256 derives one key per purpose (account operation, sign-out status, account deletion, step-up), and new values usekid: v2. No legacy keys are bound.- Sealing keys. Alchemy derives
STRIPE_CUSTOMER_SEALING_KEYfromsha256("stripe-customer-creation:" + secret), using the website session secret or, without one, the Stripe secret key, andACCOUNT_DELETION_SEALING_KEYfromsha256("account-deletion:" + sessionSecret)only when the session secret exists. Both are bound to both Workers as redacted 64-character hex secrets.AUTH0_SECRETstays out of the API: it receives only the two purpose keys. WEB_SESSION_CONTEXT_KEYis shared by both and signs only invited-agent contexts (invited-agent+jwt).GUEST_SESSION_KEY(and, during a rotation, the previous key with its deadline) is bound to both.
Only the API receives STRIPE_WEBHOOK_SECRET; Stripe’s endpoint is https://api.tarnandtimber.com/webhooks/stripe.
Storefront session bindings
infra/alchemy/shop.ts (shopSessionBindings) gives the storefront what it needs to own browser sessions:
WEB_SESSION_REVOCATIONS, the Workers KV namespaceWebSessionRevocations, retained with the Worker. It holds revokedsidvalues and account cutoffs with a 30-day TTL (the longest session) and rememberedjtivalues for 120 seconds.AUTH0_MANAGEMENT_DOMAIN,AUTH0_MANAGEMENT_CLIENT_IDandAUTH0_MANAGEMENT_CLIENT_SECRET, the Shop Management client (auth0_client.shop_management). The storefront requests tokens with exactlyread:sessions,delete:sessionsanddelete:refresh_tokensfor session listing and sign out everywhere.
The Terraform variable website_backchannel_logout_url must match ^https://[^/?#]+/api/auth0/backchannel-logout$, so
Auth0 posts back-channel logout to the storefront. The account Actions call the API’s
/internal/account/security-intents; shop_account_security_api_origin defaults to shop_api_origin.
The My Account provider token lifetime must be at most 300 seconds before activating sensitive confirmations; the
storefront refuses longer-lived tokens. AUTH0_JWKS_URL carries the canonical tenant directory into the shop; it
remains distinct from the custom login issuer.
Durable Objects and scheduled work
The API declares AccountWriter and ConversationHub with transferredFrom: ['Shop'], because both classes used to
live in the storefront Worker; the transfer runbook covers the move for an instance that still
has them there. The API’s hourly cron owns commerce recovery and independent diagnostic cleanup; backoffice separately cleans its local
operator ledger. See the scheduled-jobs runbook.
BackofficeDatabase stays with the backoffice for operator sessions and audit records.
Diagnostic ownership
ShopDatabase also retains five diagnostic tables; it is not a new database or collector. BackofficeDatabase continues to own only operator sessions and the separate access ledger. “No shop database access” means no direct binding: auditors can investigate authorized diagnostics through API procedures. Storefront, MCP and backoffice emit through distinct named private API producer capabilities; the Auth0 proxy emits only synthetic HTTP streams, without app recorder or database access. Genuine direct/forwarded Auth0 events remain visibly separate from simulation. See diagnostic investigations for actual service/runtime attribution, source evidence, bounded raw demo policy and 30-day retention/restore behavior.