Skip to content
Tarn & Timber Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

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_ISSUER on the MCP Worker is the issuer poppy.json publishes as auth.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.ts fails 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 table dpop_proofs (key thumbprint, jti, Session, expiry), deleted by the hourly clean_dpop_proofs job once expired.
  • Retry replay. conversation_message_ids stores response_status and response_body, written once with the first response to a message ID.
  • Discovery. apis[openapi].url in the Auth0 proxy’s poppy.json is <api>/poppy/openapi.json, the Poppy view of the contract, and infra/alchemy/composition.ts checks it. Poppy agents call /v1 with DPoP.

Shop API runtime

  • enable_request_signal. infra/alchemy/api.ts gives the API Worker API_COMPATIBILITY, the shop’s compatibility settings plus this flag, so request.signal aborts 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}/turn and /internal/conversations/{id}/handoff-timeout with ConversationHubSecret.

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

In a later release, remove the old path, credentials and bindings.

Deploy order in infra.ts

  1. Terraform state backend
  2. Sign-in screen assets
  3. Email sending domain
  4. Auth0 (Terraform), Auth0 email (Terraform), Okta (Terraform)
  5. Proxy Auth0
  6. Docs site
  7. 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:

  • AccountKeyRoot is created once by Alchemy and bound to both as ACCOUNT_KEY_ROOT. HKDF-SHA256 derives one key per purpose (account operation, sign-out status, account deletion, step-up), and new values use kid: v2. No legacy keys are bound.
  • Sealing keys. Alchemy derives STRIPE_CUSTOMER_SEALING_KEY from sha256("stripe-customer-creation:" + secret), using the website session secret or, without one, the Stripe secret key, and ACCOUNT_DELETION_SEALING_KEY from sha256("account-deletion:" + sessionSecret) only when the session secret exists. Both are bound to both Workers as redacted 64-character hex secrets. AUTH0_SECRET stays out of the API: it receives only the two purpose keys.
  • WEB_SESSION_CONTEXT_KEY is 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 namespace WebSessionRevocations, retained with the Worker. It holds revoked sid values and account cutoffs with a 30-day TTL (the longest session) and remembered jti values for 120 seconds.
  • AUTH0_MANAGEMENT_DOMAIN, AUTH0_MANAGEMENT_CLIENT_ID and AUTH0_MANAGEMENT_CLIENT_SECRET, the Shop Management client (auth0_client.shop_management). The storefront requests tokens with exactly read:sessions, delete:sessions and delete:refresh_tokens for 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.

Was this page helpful?