CD model + custom-domain provisioning
:::note Contenido en inglés Esta página del wiki se sincroniza desde la base de conocimiento en inglés y todavía no está traducida. :::
reference· devops · updated 2026-09-01 · source
How tedos ships changes and how a client's custom domain goes live. Two ideas do the heavy lifting:
- Per-org config is RUNTIME, not a build — changing a client's branding/modules/features/domain is a Postgres write, never a git push or a Vercel deploy.
- One multi-tenant deployment serves every client domain —
apps/portal(the tenant app, ADR-017) is deployed once; custom domains are added to that one Vercel project via the Domains API and resolved byHostat request time.
Decided + built in effort #922 (Client Configuration Framework), sub #930; console UI + hardening in
effort #1444 (the target app moved from the retired comprender to apps/portal with ADR-017).
Related: comprender-mvp-e1.md (tenant topology) ·
plan-engine.md (the GitHub/Vercel sync that mirrors deploys into the graph).
1. Runtime config vs. code change (the split)
| Change | Where it lives | What it triggers |
|---|---|---|
| Branding tokens, enabled modules, feature flags, tier, locale, custom domain | Postgres rows (org_config / feature_flag_override / control.project) | Nothing — a DB write (custom domain also calls the Vercel Domains API; still no git push, no deploy). The client app reads it at request time. |
| App code, packages, schema, UI | git → develop → main | A Vercel deploy of the affected app only (turbo-ignore). |
- The operator console writes config through the operator-guarded API
(
PATCH /projects/:id,PATCH /console/projects/:id/features/:key,POST /console/projects/:id/domain[/verify]). These are pure DB writes + Vercel Domains API calls — nothing in the config write-path pushes git or triggers a deploy. - The multi-tenant client app reads
GET /tenant/configserver-side, resolving the tenant by the requestHostheader (no-store, DF5). So flipping a client's branding or modules is instant and build-free — no rebuild, no redeploy, no per-tenant artifact.
2. How a custom domain goes live (the console flow, effort #1444)
The backbone (sub #923/#924) stores project.domain (UNIQUE since #1446 — one tenant per
domain, NULLs free) + domainVerificationToken + domainVerifiedAt. Sub #930 makes that domain
actually route; the console screen Configurar → Dominios (/org/<slug>/configurar/dominios,
sub #1445) drives the whole flow:
- Operator saves the org's domain on the Dominios screen →
POST /console/projects/:id/domain(via the console BFFapps/console/src/app/api/console/projects/[id]/domain/route.ts) persists it unverified and adds it to the client app's Vercel project ("Add a Domain to a Project", Vercel Domains API). The response carries the DNS challenge (TXT/CNAME records), rendered with copy buttons. - Client publishes the DNS records at their authoritative DNS provider.
- Operator hits Verificar →
POST /console/projects/:id/domain/verifypolls Vercel; once ownership + SSL are confirmed it stampsdomainVerifiedAtand clears the one-time token. Vercel issues the TLS cert automatically. - The domain now points at the one portal deployment;
GET /tenant/config(apps/api/src/routes/config/config-routes.ts) resolves the tenant byHostagainstproject.domain. No new build, no new app — just a new domain on the existing deployment.
The screen derives a shared DnsStatus per domain (pending → verifying → active, error
on a failed call — @tedos/shared dnsStatusSchema) from the real API responses.
Env (names only, never hardcoded; fails closed / 503 when unset — mirrors GET /console/infra;
the console surfaces the 503 as "provisioning not configured"). Documented in
apps/api/.env.example; minting the token is the operator's MANUAL step in the Vercel dashboard:
VERCEL_TOKEN— Vercel access token (domains write scope).VERCEL_TEAM_ID— optional; scopes calls to thetuempresadigitalteam.VERCEL_CLIENT_PROJECT_ID— the Vercel project id/name of the multi-tenant client app (apps/portal) that every tenant domain is added to.
3. Selective builds — turbo-ignore
Every app's vercel.json carries:
"ignoreCommand": "npx turbo-ignore @tedos/<app>",
"git": { "deploymentEnabled": { "main": true, "develop": false } }
turbo-ignoreasks Turborepo whether the app (or anything in its dependency graph) changed since the last deploy; if not, Vercel skips the build. A code change rebuilds only the affected app, not the whole monorepo.deploymentEnabledships frommainonly;developis integration (no auto-deploy).- The apps are consistent on both settings — verified in #930.
4. One deployment, many domains
@tedos/portal is the multi-tenant tenant app (ADR-017:
operators AND students, role-split on ONE host per tenant): deployed once, it serves every
tenant host — <tenant>.tuempresa.digital and any verified client-owned domain (e.g.
tienda.comprender.mx). New tenants don't get a new deployment — their domain is added to the
portal's Vercel project (step 2 above) and resolved by Host. This is what keeps onboarding a
client a config operation, not an engineering/deploy operation. (The comprender app this doc
originally targeted is retired — ADR-017.)