Deployment

Deploy the Nitro API as a Cloudflare Worker and the Vue app as static files. Marked where Cloudflare UI steps are inferred.

The API is a Cloudflare Worker (cloudflare-module). The web app is a static Vite build. There is no first-party “one click” deploy script that ships both.

scripts/deploy/Dockerfile and .gitpod.yml are stale (nginx + playground/dist, pnpm run dev:play, port 5555). create-tamandeletesscripts/deploy/** and .gitpod.yml from generated projects. Do not follow them.

1. Build the API

pnpm nx run api:build

Nitro (cloudflare.deployConfig: true) emits Worker assets and wrangler config. Worker name in the source repo is taman-better-auth-back. Generated projects rename it to <name>-api. Renaming a deployed worker creates a new worker — keep the name stable after first deploy.

Then, from the API output directory Nitro documents for your Nitro version:

npx wrangler deploy
The exact wrangler cwd/output folder is whatever Nitro 3.0.260903-beta emits (deployConfig). Confirm against the build log; this page does not guess a path that drifts with Nitro betas.

2. Cloudflare resources

Create (names from nitro.config.ts, rewritten on generate):

ResourceSource nameGenerated
Durable Object classTodoPublisherObjectunchanged
DO bindingTODO_PUBLISHERunchanged
Queuetaman-email<name>-email
DLQtaman-email-dlq<name>-email-dlq
send_email bindingEMAILunchanged

SQLite DO migration tag: v1 / new_sqlite_classes: ['TodoPublisherObject']. Applying this twice on an existing worker can fail — treat it as one-time.

Unverified in-repo: dashboard steps to attach Hyperdrive / TCP Postgres, Email Routing destination, and queue production settings. Use Cloudflare docs for those products.

3. Secrets and vars

The API reads these values only through useRuntimeConfig(), and Nitro fills runtimeConfig from NITRO_* variables. Set the Worker secrets / vars with the same names as apps/api/.env:

  • NITRO_DATABASE_URL
  • NITRO_TRUSTED_ORIGINS — your production web origin(s), comma-separated
  • NITRO_BASE_URL — public API URL
  • NITRO_APP_URL — public web URL
  • NITRO_BETTER_AUTH_SECRET
  • NITRO_GOOGLE_CLIENT_ID / NITRO_GOOGLE_CLIENT_SECRET (optional)
  • NITRO_EMAIL_FROM / NITRO_EMAIL_FROM_NAME

An unprefixed name such as DATABASE_URL is ignored by the Worker and leaves the value empty. After the first deploy, check that /readyz returns OK.

Google Cloud Console: authorized redirect {NITRO_BASE_URL}/api/auth/callback/google.

4. Database

Do not use drizzle-kit push in production. Generate migrations (pnpm db:generate), commit them, run pnpm db-pg:migrate:prod from a trusted network (or CI). drizzle-kit reads the production URL from an unprefixed DATABASE_URL, unlike the Worker.

Postgres must be reachable from the Worker (Hyperdrive, Cloudflare Tunnel, or a publicly reachable instance with a tight allow-list). How you expose Postgres is not specified in the template.

5. Web static files

pnpm --filter @taman/web build

Set VITE_API_URL to the Worker URL, VITE_ROUTER_HISTORY (hash is the production example — useful if you cannot configure SPA fallbacks), VITE_BASE if not serving at /.

Host apps/web/dist on Cloudflare Workers Static Assets, Pages, or any static host. Configure HTTPS, and put that origin in NITRO_TRUSTED_ORIGINS.

Production builds may emit _app-config-[version]-[hash].js (window._TAMAN_ADMIN_DEV_CONFIG_) so VITE_GLOB_* can change without rebuilding — see vite-config.

6. Cookies and CORS

better-auth cookies use prefix taman. The SPA must call the API with credentials: 'include'. NITRO_TRUSTED_ORIGINS must list exact origins (https://app.example.com, not *). Empty config yields no allowed origins (resolveTrustedOrigins returns []).