---
title: Deployment
description: Stacks, release choreography, and the infra.ts order.
sidebar:
  order: 6
---

Terraform owns the declared Auth0 infrastructure and Alchemy owns the declared Cloudflare resources. Runtime agent
applications and owner-managed Auth0 registrations have [separate owners](/architecture/auth0#resource-ownership).
Start with [your own deployment](/getting-started#configure-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.

1. **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.

2. **Switch**

    Flip the consumer: the storefront code path, the backoffice client, an Action URL or the Poppy discovery document.
    Deploy and watch.

3. **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](/runbooks/do-transfer) 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](/runbooks/scheduled-jobs).
`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](/runbooks/audit-investigations)
for actual service/runtime attribution, source evidence, bounded raw demo policy and 30-day retention/restore behavior.
