Skip to content
zap.ts
Esc
navigateopen⌘Jpreview
On this page

Going to Production

The checklist to work through before real users arrive. Demo data, test keys, placeholder ids and the zap.ts branding all need replacing.

Deployment shows you how to ship. This page shows you what you must not forget before you do.

zap.ts starts with demo data, test keys and its own branding, so it runs on your computer with no paid account. None of that belongs in production. Work through these nine steps in order.

1. Remove the demo users and the demo organisation

pnpm run db:seed fills the database with example data. It lives in packages/database/scripts/seed.ts.

It creates one organisation, Demo Organization, and three users:

Email Name Role
owner@demo.test Olivia Owner owner
admin@demo.test Adam Admin admin
member@demo.test Mia Member member

All three sign in with the password demo1234. The owner also carries the admin role, so that one account opens the back office too.

Never run pnpm run db:seed against your real database. If you already did, delete those three users and that organisation before you go live.

2. Replace the placeholder plan ids

Open packages/billing/src/plans.ts. The first line says:

// TODO: Replace these placeholder ids with the ones of your own provider.

The file ships two plans, starter and team. Their ids are fake. Stripe, for example, has price_replace_me_starter. No such price exists in your account, so checkout will fail.

Create your plans in your billing provider. Then copy each real id into the field for that provider. A plan holds the ids of every provider at once, and each provider reads only its own field.

3. Switch billing from test mode to live

zap.ts runs payments in test mode by default. No real card can be charged.

For Stripe, the key itself decides. packages/billing/src/mode.ts checks the start of the key:

const mode = isLiveKey(env.STRIPE_SECRET_KEY, "sk_live_") ? "live" : "test";

The default STRIPE_SECRET_KEY is sk_test_local_dev. Replace it with your sk_live_ key. Also replace STRIPE_WEBHOOK_SECRET with the one your live webhook gives you.

Other providers use a setting instead of the key:

Provider Setting Change it to
Polar POLAR_SERVER production
Dodo Payments DODO_PAYMENTS_ENVIRONMENT live_mode
Creem CREEM_ENVIRONMENT live

The billing page says on screen when the app is in test mode. Open it after the change and check that the message is gone.

4. Point mail at a real provider

MAIL_PROVIDER is smtp by default. That is Mailpit, the fake inbox that docker compose up starts. Emails land there and go nowhere.

Set MAIL_PROVIDER to resend, postmark, sendgrid or brevo, then set the key that provider needs: RESEND_API_KEY, POSTMARK_SERVER_TOKEN, SENDGRID_API_KEY or BREVO_API_KEY.

Also change MAIL_FROM. It ships as zap.ts <noreply@zapstudio.dev>, which is not your domain.

5. Point storage at a real bucket

The R2_* values point at MinIO, the local file store from docker compose up. The user name and password are both minioadmin.

Replace R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and R2_BUCKET with your Cloudflare R2 values.

Leave R2_ENDPOINT empty on Cloudflare. R2 builds its own address from the account id. The schema says so in a comment.

6. Point the database at a real server

DATABASE_URL in packages/database/.env.schema is the Postgres from Docker:

postgresql://postgres:postgres@127.0.0.1:5432/postgres

That server only exists on your computer. Use a hosted Postgres in production, and create a Hyperdrive for it. See Deployment for how Hyperdrive is wired.

Set BETTER_AUTH_URL too. It ships as http://localhost:3000.

7. Check the two robots.txt files

A robots.txt file tells search engines which pages they may read. Each app builds its own.

apps/web blocks every crawler:

User-agent: *
Disallow: /

This is on purpose. The web app sits behind the sign-in, so there is nothing useful for a search engine to find. Leave it as it is.

apps/marketing allows crawlers, and blocks only /api/. It also lists eleven AI crawlers by name, such as GPTBot, ClaudeBot, PerplexityBot and Google-Extended. Today they are all allowed. The file carries a TODO:

// TODO: To block these crawlers, swap their `Allow: /` below for `Disallow: /`.

Decide what you want. If you do not want AI crawlers to read your site, follow that comment before you launch.

8. Set the Sentry and PostHog keys

Sentry collects your errors. PostHog collects how people use the app. Both values are empty by default, so neither one is sending anything.

Set SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN for errors. Set POSTHOG_KEY for usage. POSTHOG_HOST already points at https://eu.i.posthog.com, so change it only if your project is elsewhere.

9. Replace the zap.ts branding with your own

Your app still says zap.ts in a lot of places. Your users will see it.

Start with the two files that hold the name. Both carry a // TODO: Replace with your informations. comment:

  • apps/web/src/lib/site.ts sets SITE_NAME.
  • apps/marketing/src/lib/site.ts sets SITE_NAME, and also SITE_TWITTER_HANDLE and SITE_AUTHOR.

Changing SITE_NAME covers a lot at once. The logo, the page titles, the sharing image and the web app manifest all read it.

Then work through the rest:

What Where
The logo mark apps/web/src/components/brand.tsx. It uses a Zap icon from lucide-react. Swap in your own
App icons icon-192.png and icon-512.png in apps/web/public and apps/marketing/public
Colours The token files in @zap-ts/ui
Worker names The name field in each apps/*/wrangler.jsonc. They all start with zap-ts-
Email templates packages/mail/src/templates. They carry a TODO about placeholder designs
Legal pages apps/marketing/content/legal. These are placeholders, not legal advice
Blog post apps/marketing/content/blog holds one example post. Delete it

When you think you are done, search the whole repository for what is left:

grep -ri "zap.ts\|zap-ts\|zap studio" apps packages --include="*.ts" --include="*.tsx" --include="*.mdx" --include="*.json*"

Ignore hits inside @zap-ts/ import paths. Those are package names, not your brand, and they do not reach your users.

Last check

Print how every value resolves, one app at a time:

pnpm run env:web
pnpm run env:marketing
pnpm run env:api
pnpm run env:admin

Read the output and look for anything that still says local_dev, replace_me, localhost or minioadmin. A missing required value stops the build and names itself, but a leftover demo value will not. You have to spot that one yourself.

Last updated on September 22, 2026

Was this page helpful?