Deployment
apps/web deploys through GitHub Actions prebuilt deploys; docs sites deploy through Vercel's Git Integration.
The repo's active deploy paths are split by ownership:
apps/webis owned by GitHub Actions prebuilt deploys.apps/dev-docsandapps/webview-bridge-docsare owned by Vercel's Git Integration.
| Vercel project | Source path | Workflow | Repo variable | Owner |
|---|---|---|---|---|
today-web | apps/web | deploy-web.yml, deploy-web-lane-staging.yml, promote-web-production.yml | VERCEL_PROJECT_ID_WEB | Actions |
today-dev-docs (Vercel project: web-dev-docs) | apps/dev-docs | (none) | (n/a) | Vercel Git |
webview-bridge-docs | apps/webview-bridge-docs | (none) | (n/a) | Vercel Git |
The admin console (today-admin Vercel project, admin.today.ai) is deployed
from todayai-labs/today-admin.
For the underlying conceptual model, see Architecture -> Vercel deploy model.
Web Environments
| Flow | Git ref / action | GitHub environment | Vercel target | Public URLs |
|---|---|---|---|---|
| Dev / PR preview | dev push or PR | web-preview | preview | unique/contextual *.dev.preview.todayai.dev; merged dev also gets todayai.dev |
| Lane staging candidate | release/web-* push | web-lane-staging | custom staging target | unique/contextual *.staging.preview.today.ai aliases plus https://staging.today.ai |
| Production candidate review | manual promote workflow | web-production | production-mode candidate with --skip-domain | unique and workflow aliases under *.prod.preview.today.ai; active hotfix also uses hotfix.prod.preview.today.ai |
| Production promote | approved workflow step | n/a | vercel promote of the production candidate | the candidate's preview aliases plus https://today.ai |
Use the terms precisely:
- lane staging is
staging.today.ai, tied to backend lane routing. Its web build uses Vercel's customstagingtarget and injectsAPP_ENV=productionplusNEXT_PUBLIC_TRAFFIC_LANE=staging, so the app keeps production semantics while browser clients and BFF upstream fetches sendX-Traffic-Lane: staging. - production candidate is a separate production build from the same release
SHA without
NEXT_PUBLIC_TRAFFIC_LANE. It is the only artifact that may be promoted totoday.ai.
Because NEXT_PUBLIC_TRAFFIC_LANE is baked into browser JavaScript, promoting a
lane staging build to production would leak the staging lane into production
traffic. The production candidate therefore rebuilds from the release SHA.
Web Preview Aliases
Every Actions-owned web deployment receives an opaque random alias that never moves and a contextual alias that can move as its PR or workflow is updated:
- dev and PR builds use
<random>.dev.preview.todayai.devplus eitherweb-pr-<number>.dev.preview.todayai.devorweb-run-<id>.dev.preview.todayai.dev; - after a PR merges, the resulting
devpush keeps those two custom preview URLs and Vercel moves the existingtodayai.devbranch alias to that deployment; - lane-staging builds use
<random>.staging.preview.today.aiplus eitherweb-pr-<number>.staging.preview.today.aiorweb-run-<id>.staging.preview.today.ai; - production candidates use
<random>.prod.preview.today.aiplusweb-run-<id>.prod.preview.today.ai; - after immutable candidate smoke, the dedicated hotfix workflow also moves
hotfix.prod.preview.today.aito the only active hotfix candidate; - PR comments show both aliases on separate labeled lines. Both initially point to the latest successful deployment. When a newer deployment succeeds, the random snapshot alias stays unchanged while the contextual latest pointer moves forward. GitHub environment links use the contextual alias, while smoke tests use the random alias to verify wildcard DNS, certificate, and routing for that deployment.
Lane staging moves staging.today.ai only after both preview aliases exist and
the random alias passes smoke. Production keeps today.ai unchanged through
candidate review; vercel promote adds the production domain only after the
approval gate.
Ad Hoc Web Candidates
Three manual workflows can build any branch, tag, or full commit SHA from this repository without moving a stable environment domain:
Deploy Web Dev Candidatebuilds with Vercel Preview settings and assigns*.dev.preview.todayai.devaliases;Deploy Web Staging Candidatebuilds with the custom Vercelstagingtarget plusAPP_ENV=productionandNEXT_PUBLIC_TRAFFIC_LANE=stagingand assigns*.staging.preview.today.aialiases;Deploy Web Prod Candidatebuilds with production settings, removes the staging lane, and assigns*.prod.preview.today.aialiases.
Run the workflow itself from dev, then provide the requested source in its
source_ref input. This keeps the workflow definition and deployment scripts
on the trusted trunk while checking out the candidate source into a separate
directory. The input accepts a branch, tag, or full commit SHA from this
repository; pull-request refs and external repositories are not accepted.
Every manual candidate gets an immutable random URL and a moving
web-run-<run-id> URL. The candidate workflows never assign todayai.dev,
staging.today.ai, or today.ai, and never call vercel promote. Production
candidates enter the web-production GitHub Environment before source checkout
or build. Configure required reviewers on that environment to gate access to
production configuration and secrets. Candidate builds disable source-map
uploads to avoid exposing observability upload credentials to arbitrary source
revisions.
Staging candidates pull the custom Vercel staging configuration. Keep the
web-lane-staging GitHub Environment protected because arbitrary source still
runs with staging-scoped credentials and service access.
Ad hoc staging and production candidates are inspection artifacts only. They do not satisfy release ownership, frozen-SHA, CI, E2E, approval, tagging, or backmerge requirements and cannot replace the normal release workflows.
Web Release Pipeline
See Web release flow for the operator checklist. The workflow files map to these stages:
cut-web-release.ymlcreates a temporaryrelease/web-*branch fromdevor another explicit production-tag source. Weekly cuts require the currentdevtip and an idle staging lifecycle; replacing an intentionally abandoned release requires its exact branch and full SHA and creates a permanentweb-abandoned-*marker. Same-day replacement branches use.2,.3, and so on. Emergency hotfix cuts require the current productionweb-prod-*source SHA.deploy-web-lane-staging.ymlwaits for CI, builds from the custom Vercelstagingtarget withAPP_ENV=productionandNEXT_PUBLIC_TRAFFIC_LANE=staging, deploys with--target=staging, and assigns unique and contextual staging-preview aliases. It smokes the unique alias before movingstaging.today.ai. All staging deploys share one concurrency group and pass an active-release ownership guard before changing the stable staging alias.promote-web-production.ymlrequires the aggregateCI Successresult and, on the normal path, exact-SHA staging deployment, build metadata, and Full E2E evidence. It then builds the production candidate without lane env, pauses on theweb-productionapproval gate, verifies the release branch SHA did not move, promotes totoday.ai, archives the exact SHA with a protectedweb-prod-*tag, opens the required reconciliation PRs, and deletes the promoted source branch after those refs and production metadata are verified.promote-web-hotfix.ymlis the production-candidate-first emergency path. It always skips stable lane staging, assignshotfix.prod.preview.today.aionly after immutable candidate smoke, and reuses the same production approval, promote, tag, and reconciliation engine. An optional staging candidate remains isolated and never changesstaging.today.ai.merge-web-release-reconciliation.ymlis the only merge-commit exception forbackmerge/web-*andbackport/web-*PRs. It verifies approval, the immutable production tag, source-side topology, and CI on the current potential merge, then uses the Today Platform CI App to compare-and-set fast-forward the target to the exact two-parent reconciliation merge.
Weekly production promotion cannot skip stable staging evidence. Use the dedicated hotfix workflow when production must be repaired without waiting for the weekly lane.
Required Vercel Setup
For today-web:
-
Actions own CLI deploys. Keep Vercel Git auto-deploy disabled for the project.
-
today.aiis the production domain. -
staging.today.aiis an Actions-owned alias for lane staging deployments. -
*.dev.preview.todayai.devmust route to Vercel and have an active wildcard certificate for dev and PR aliases. -
*.staging.preview.today.aimust route to Vercel and have an active wildcard certificate for ad hoc staging candidates and release lane-staging aliases. -
*.prod.preview.today.aimust route to Vercel and have an active wildcard certificate for production-candidate aliases. -
When
today.aiandtodayai.devremain on external authoritative DNS, configure each wildcard route and ACME delegation in its corresponding zone:# todayai.dev zone CNAME *.dev.preview -> cname.vercel-dns-0.com NS _acme-challenge.dev.preview -> ns1.vercel-dns.com NS _acme-challenge.dev.preview -> ns2.vercel-dns.com # today.ai zone CNAME *.prod.preview -> cname.vercel-dns-0.com NS _acme-challenge.prod.preview -> ns1.vercel-dns.com NS _acme-challenge.prod.preview -> ns2.vercel-dns.com CNAME *.staging.preview -> cname.vercel-dns-0.com NS _acme-challenge.staging.preview -> ns1.vercel-dns.com NS _acme-challenge.staging.preview -> ns2.vercel-dns.comThe wildcard certificates for
*.prod.preview.today.ai,*.staging.preview.today.ai, and*.dev.preview.todayai.devare independent. -
The custom Vercel
stagingenvironment should not trackmain; lane staging is built by GitHub Actions fromrelease/web-*withvercel pull --environment=staging,vercel build --target=staging, andvercel deploy --prebuilt --target=staging. The--skip-domainoption is production-only; the workflow assigns staging aliases explicitly after the immutable candidate passes smoke. -
The custom staging environment must set
APP_ENV=production,NEXT_PUBLIC_APP_URL=https://staging.today.ai, production API/auth URLs, andNEXT_PUBLIC_TRAFFIC_LANE=staging. This keeps production application semantics while separating the Vercel deployment target and configuration. -
Do not set
X-Traffic-EnvorLANE_STAGING_TRAFFIC_ENV; the web lane contract isNEXT_PUBLIC_TRAFFIC_LANE=stagingat build time andX-Traffic-Lane: stagingon requests. -
The Web Vercel project is the source of truth for public PostHog ingestion tokens. Development/Preview provide
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN_DEV; Staging/Production provideNEXT_PUBLIC_POSTHOG_PROJECT_TOKEN_PROD. Web and Desktop builds pull the corresponding environment and fail closed when the selected value is absent. Dev/test Desktop packages pull both Preview DEV and Production PROD because those packages can switch runtime environment; fixed staging/prod packages pull only their corresponding PROD profile. Desktop CI transfers only an allowlisted public artifact from a trusted config-only job; pull-request code never receives the broader Vercel credential.
GitHub needs these shared variables/secrets:
vars.VERCEL_ORG_IDvars.VERCEL_PROJECT_ID_WEBsecrets.VERCEL_TOKENvars.TODAY_PLATFORM_CI_APP_IDsecrets.TODAY_PLATFORM_CI_APP_PRIVATE_KEY- optional source-map upload secrets:
POSTHOG_API_KEY,POSTHOG_PROJECT_ID_DEV,POSTHOG_PROJECT_ID_PROD,SENTRY_AUTH_TOKEN
PR Previews
deploy-web.yml handles only dev and PR preview deployments. It uses Vercel
preview env vars, deploys a prebuilt preview artifact, assigns immutable random
and moving contextual aliases under *.dev.preview.todayai.dev, and comments the
contextual URL on PRs. A merged PR subsequently triggers the dev push deploy;
Vercel's existing branch-domain assignment moves todayai.dev to that latest
successful dev deployment.
Docs Sites
apps/dev-docs and apps/webview-bridge-docs deploy through Vercel's Git
Integration. Do not add GitHub Actions deploy workflows for them unless the app
is explicitly migrated to Actions-owned prebuilt deploys and its vercel.json
disables Vercel Git deploys at the same time.
Rollback
Vercel keeps every deploy. Runtime rollback is a human-selected Vercel rollback or promotion of a previous deployment.
For code-level rollback, create a revert PR against dev, let it land through
the merge queue, cut a new release/web-* branch, and run the normal promote
flow. The durable production archive is the web-prod-* tag, not main.