Skip to main content

Self-hosting Transit

Advanced and optional. You do not need this page if you use the hosted service at transit.orangecountyai.com.

This path operates your own Cloudflare Worker, Durable Objects, D1 database, browser control plane, and host enrollment authority. It is for operators who accept responsibility for their own Worker account, credentials, retention, and incident response. For component boundaries rather than deployment steps, see Transit architecture.

Prerequisites

  • A Cloudflare account on Workers Paid for Durable Objects and, when you choose to configure it, Workers Logpush.
  • Cloudflare authentication through bunx wrangler login, or a CLOUDFLARE_API_TOKEN for a headless environment.
  • mise, Bun, and Go. mise installs the repository-pinned Bun and Node tools; Go is needed to build the host daemon from daemon/.
  • A domain and an email address you control for EMAIL_FROM if you will send password-reset mail.

Install the project tools and dependencies from the repository root:

mise trust && mise install
bun install
bun install --cwd web

Configure secrets and variables

The Worker requires these secrets:

NamePurpose
BETTER_AUTH_SECRETSigns Better Auth state.
TRANSIT_MASTER_KEYEncrypts integration credentials and custom-source secrets before storage.

It also requires these nonsecret variables in wrangler.jsonc:

VariablePurpose
BETTER_AUTH_URLCanonical public origin for authentication. Set it to your deployed Transit URL.
EMAIL_FROMSender address for password-reset email.

mise run secret:put creates BETTER_AUTH_SECRET if it is missing. It does not create TRANSIT_MASTER_KEY; generate and store a high-entropy key in your own secret manager, then provide it with Wrangler. Never put either secret in wrangler.jsonc, source control, or a shell command line.

mise run secret:put
printf %s "$TRANSIT_MASTER_KEY" | bunx wrangler secret put TRANSIT_MASTER_KEY

Billing is optional, plan limits are not

Leave STRIPE_SECRET_KEY unset and the deployment runs with billing switched off. The Better Auth Stripe plugin is not registered, so /api/auth/subscription/upgrade and /api/auth/subscription/billing-portal do not exist, GET /api/billing reports configured: false, and the /billing page degrades to read-only: it shows the current plan, limits, and usage with a "Stripe not configured" notice and no upgrade controls.

Plan enforcement is independent of that switch, and this is the part that surprises people. currentPlan reads the subscription table, an account with no subscription row resolves to free, and the free limits are then applied by the API exactly as they are on the hosted service: 1 host, 5 agents, 2,000 messages per month, 0 integrations, and a 7-day ledger. On a self-hosted Worker with no Stripe, enrolling a second host answers HTTP 402 plan_limit.

To run your own deployment without those caps, give the account an active subscription row for the plan you want. resolvePlan accepts operator or fleet with status active or trialing:

bunx wrangler d1 execute transit-db --remote --command \
"INSERT INTO subscription (id, plan, reference_id, status, billing_interval, period_end)
VALUES ('sub_local_1', 'fleet', '<better-auth-user-id>', 'active', 'year', 9999999999)"

Use the account's Better Auth user id as reference_id; it is the same value every host, room, and integration is scoped by. This is operator-owned surgery on your own database, so treat it as configuration you must reapply for each account you create. If you would rather not carry that, edit PLANS in src/lib/billing/plans.ts and deploy your own limits.

The Stripe price and webhook variables (STRIPE_PRICE_*, STRIPE_WEBHOOK_SECRET) matter only when billing is on.

Before enabling password-reset email, onboard the domain of EMAIL_FROM with Cloudflare Email Sending. The repository binding expects the domain to be enabled with:

bunx wrangler email sending enable <your-domain>

You may complete the same onboarding in the Cloudflare dashboard.

Create and deploy production

Create the remote database, apply migrations, ensure the auth secret exists, and deploy:

mise run d1:create
mise run migrate:remote
mise run secret:put
mise run deploy

mise run deploy builds the SPA, deploys the Worker, and can patch the production BETTER_AUTH_URL placeholder with the deployed workers.dev URL. If you use a custom domain, set BETTER_AUTH_URL to that origin and deploy again. Configure your custom domain in your own Cloudflare Worker route configuration.

Preview environment

The repository defines a separate transit-preview Worker and transit-db-preview database. Provision and deploy it with:

mise run d1:create-preview
mise run migrate:remote-preview
mise run deploy:preview
mise run secret:put-preview

secret:put-preview supplies BETTER_AUTH_SECRET only after the preview Worker exists. Set a distinct preview TRANSIT_MASTER_KEY with Wrangler as well:

printf %s "$TRANSIT_PREVIEW_MASTER_KEY" | bunx wrangler secret put TRANSIT_MASTER_KEY --env preview

The preview environment has no cron trigger. Do not treat it as a production retention environment.

Operational ownership

A self-hosted operator owns all of the following:

  • The daily retention sweep. Production configuration includes the 0 4 * * * cron; monitor that it remains deployed and functioning. Messages and message deliveries retain for seven days, while settled/dead integration deliveries and replies retain for 30 days.
  • Custody, rotation, backup, and incident response for TRANSIT_MASTER_KEY, BETTER_AUTH_SECRET, device tokens, connector credentials, and custom-source secrets.
  • Host enrollment and revocation. Your dashboard issues the one-time code; you decide who may enroll and revoke hosts.
  • Cloudflare billing, Durable Object capacity, D1, domains, Email Sending, observability, and optional Logpush. mise run logpush:setup is an opt-in task.

Enroll a host against your Worker

Install Transit on the host the usual way, herdr plugin install Orange-County-AI/transit/daemon/plugin; the plugin is origin-agnostic and the --url you pass at enrollment decides which control plane the host talks to. After signing in to your self-hosted dashboard, use Hosts -> Enroll host to issue a code, then enroll against your public Worker origin:

transit enroll --url https://transit.example.com --code XXXX-XXXX

The daemon stores the issued device token locally and then maintains its outbound connection to the URL you supplied. Continue with Hosts and the daemon, Agents, and Transit security model for operational behavior.