Docs/Deployment
Deployment

05. Bind Domains and Environment Variables

Bind production domains, switch frontend variables to real online addresses, and understand how copied web apps should be wired.

Last updated Mar 26, 2026

This chapter is where your deployment starts looking like a real online product instead of a set of temporary project URLs.

0. Why bind domains?

The system may run without binding custom domains. But:

  • URLs will be ugly: default *.workers.dev or *.pages.dev URLs are hard to remember and share.
  • Payment callbacks need public URLs: Stripe and Creem must reach your backend from the public internet.
  • SEO metadata should not point at preview domains: sitemap, canonical tags, and robots should use your real production domain.

node3-pay-service must be bound first because it receives payment provider callbacks. Without a custom domain, Stripe and Creem webhooks are usually unreliable.

1. Bind custom domains

Web (Astro 6)

web currently deploys to a Worker, not Pages. Bind your custom domain in Cloudflare Dashboard -> Workers & Pages -> your web Worker -> Settings -> Domains & Routes.

Recommended workflow: Copy apps/web to a new project and customize there. Do not modify apps/web directly. Treat it as the official template.

If you copy apps/web to create a new site

Use this order in the current system:

  1. Create the tenant in Admin first

    • Open Admin -> Projects
    • Create a new project row with a unique app_key
    • This registers the tenant in t_project
  2. Copy apps/web to a new app directory

    • Example: apps/my-brand
  3. Edit apps/my-brand/zship.app.json

    • appKey: must match the tenant you created in Admin
    • domain: your production domain
    • siteUrl: your canonical production URL
    • pagesProject: the deployment target name for this frontend app
    • Brand fields such as siteName, brand.name, and brand.logo
  4. Keep runtime wiring aligned

    • zship.app.json owns appKey, domain, and siteUrl
    • Env variables are for backend service URLs, secrets, and legacy/env-based overrides
  5. Change wrangler.toml

    • Update the name field
    • If you keep name = "web" in the copied app, deployment may overwrite the original web Worker
  6. Bind the new custom domain

    • Give the copied frontend its own domain
    • Do not reuse the original web domain unless you intentionally want to replace it
  7. Redeploy the copied frontend

    • Treat it as its own frontend Worker and deploy it independently

Mental model:

  • Admin creates the tenant
  • zship.app.json maps a frontend app to that tenant
  • wrangler.toml decides which Worker receives the deployment

Admin

admin usually deploys to Pages. Bind the domain in Workers & Pages -> admin -> Custom domains.

Backend Workers

Priority: node3-pay-service. Bind it first so Stripe and Creem webhooks can reach your payment callback endpoint. Other Workers can follow.

CDN and R2 (CDN_PUBLIC_URL)

If you use node6-cdn-service for uploads, the Worker's CDN_PUBLIC_URL must match the R2 bucket custom domain. Otherwise uploads may succeed while public URLs return 404. Full steps are in Troubleshooting.

2. How zship.app.json, site.ts, and env work together

Frontend configuration is layered:

  1. zship.app.json

    • Recommended source of truth for frontend app identity
    • Manages appKey, siteUrl, domain, brand metadata, and deployment target names
  2. site.ts

    • Runtime adapter that reads zship.app.json and turns it into typed siteConfig
    • Regenerated by Dev Console only when scaffold/brand tooling needs to refresh the adapter
  3. Environment variables

    • Local: .env
    • Production: Cloudflare Variables and Secrets
    • Service URLs and secrets come from env; app identity and canonical URL come from the manifest unless a legacy app explicitly enables env-based tenant selection

So the practical rule today is:

  • Edit zship.app.json
  • Let Dev Console keep robots and generated adapters aligned when you use its tooling
  • Configure env for backend URLs and secrets only

3. Canonical URL vs latest deployed URL

Do not treat these as the same thing:

  • Canonical URL: your real production site URL, for example https://my-brand.com
  • Latest deployed URL: the latest preview or temporary URL discovered during deploy, for example https://xxxx.my-brand.pages.dev

Current rule:

  • siteUrl in zship.app.json is the canonical URL
  • the latest deployed pages.dev URL is only a deploy artifact and should not replace sitemap, robots, or canonical metadata

For a copied web project, put identity in zship.app.json and configure service endpoints with env values like:

AUTH_SERVICE_URL=https://n1.example.com
PAY_SERVICE_URL=https://n3.example.com
BLOG_API_URL=https://n5.example.com
SITE_SERVICE_URL=https://n7.example.com
CHECKIN_SERVICE_URL=https://n9.example.com
AI_SERVICE_URL=https://n10.example.com
SUPPORT_SERVICE_URL=https://n2.example.com

Important:

  • appKey in zship.app.json must match the tenant created in Admin
  • siteUrl in zship.app.json should be the real production URL, not a temporary pages.dev preview URL

5. Redeploy after env or domain changes

After changing env, custom domains, or frontend identity, redeploy the frontend. Otherwise old values may still be baked into the build output.

6. Final sanity check

Before initialization, make sure:

  • The public site opens under the correct production domain
  • Admin opens under the correct production domain
  • Login and registration target the correct tenant
  • Frontend requests hit the real production backend URLs
  • Sitemap, robots, and canonical metadata point at the production domain rather than pages.dev