Environments — domains, branches, databases
:::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. :::
canonical· devops · updated 2026-09-07 · source
Decided by José 2026-09-06 (effort #1650, spun out of effort #1604's Render/Cloudflare work).
The single source of truth for which tier exists, which branch feeds it, which domain it answers
on, and which database it uses. render.yaml (Render Blueprint) declares the same shape for
apps/api in a machine-readable form, but the Render Dashboard is the actual live source of
truth for what's deployed — render.yaml is a mirror kept in sync by hand (§ Platform decisions
below). Agent access rules for the platforms live in .claude/rules/render-conventions.md and
.claude/rules/delegation-brief.md; this doc describes what IS.
Two tiers, no previews
| Tier | Branch | Domain root | Deploys when | Hosting |
|---|---|---|---|---|
| staging | develop | tedos.dev | every merge to develop (platform auto-deploy) | Render free services from effort #1604 |
| prod | main | tuempresa.digital | every merge to main = a release (Vercel auto-deploys the three frontends; the Fly API is a manual deploy from origin/main) | Vercel (portal, console, docs) + Fly (tedos-api, Fly Managed Postgres) — unchanged |
- No hosted dev tier and no per-PR preview environments (José: "I don't care about previews. I
only want the links and the deployment process on merges"). Dev is each developer's machine with
the local
docker-composePostgres.*-dev.tedos.devnames are reserved by this convention and nothing is provisioned for them. - Merge-to-deploy needs no GitHub Actions. Render and Vercel deploy on push to their configured branch; Actions is billing-blocked in this repo and is not part of the flow.
- Prod builds from
mainsince 2026-09-07 (sub #1690). The Vercel projectstedos-comprender(portal),tedos-console, andtedos-docsare linked to the GitHub repo with Production Branch =main. Before that date they had no Git integration at all — every prod deploy was a manualvercel deploy --prodfrom the CLI, and the three frontends had drifted days to months behinddevelop. Non-mainpushes create no deployment: each app'svercel.jsonignoreCommandexits 0 (skip) unlessVERCEL_GIT_COMMIT_REFismain, then hands off toturbo-ignoreso a release only rebuilds the apps whose dependency closure changed. Two project-level settings back that up (Vercel API, 2026-09-07):previewDeploymentsDisabled: true— no preview build for any branch, including branches whosevercel.jsonpredates the main-onlyignoreCommand(two such branches still built previews the same morning) — andgitCommentsoff (no bot comments on PRs).
The domain split (hard rule)
tedos.dev— staging only. Registered because it carries zero existing traffic, so its whole DNS sits on Cloudflare (Zone ID05c83e7bec7484ad57c69c97a2617695— not secret).tuempresa.digital— production, untouched by this effort. It carries live Resend, Google Workspace MX, auth records, the Fly API A/AAAA and the Vercel ALIAS records. A migration was rejected: a dropped record silently breaks auth, email or the live API. Record list + reasoning:apps/api/docs/cloudflare-domain-automation-live-cutover.md.
Naming — flat and hyphenated, one level
<service>-<env>.tedos.dev. Never nested (api.staging.tedos.dev): a single Cloudflare wildcard
cert (*.tedos.dev) covers every flat name; a nested form would need a second-level wildcard.
Domain map
| Service | Staging | Production (unchanged) |
|---|---|---|
apps/api | api-staging.tedos.dev | api.tuempresa.digital |
apps/portal | portal-staging.tedos.dev | tuempresa.digital |
apps/console | none — not wanted | console.tuempresa.digital |
apps/claude-kit-docs | none — not wanted | docs.tuempresa.digital |
Out of scope, on purpose: console-staging and docs-staging (José, 2026-09-06 — the two
Hobby-included custom domains go to api + portal; each extra would be $0.25/mo and nobody needs
them); apps/admin (frozen, ADR-017); apps/chat and apps/clients/comprender (comprender is
being absorbed into portal — no separate staging surface). Tenant custom domains (a client's own
domain pointing at their portal) are a different mechanism on the same zone — Cloudflare for
SaaS Custom Hostnames, origin.tedos.dev fallback origin — owned by effort #1604 sub #1621.
DNS mode — proxied, SSL Full → Full (strict)
Staging records on Cloudflare are proxied (orange cloud) (José, 2026-09-06). Zone SSL mode: Full while any Render custom domain still has a pending certificate; Full (strict) as the target once every attached domain shows an active Let's Encrypt cert on Render (strict validates the origin cert, which is what stops a misrouted origin from being silently accepted). If strict cannot be met for a domain, record the exception and the reason here. Cloudflare terminates the public TLS with its wildcard cert; Render terminates the origin leg with its own Let's Encrypt cert per domain. Rules from Render's own docs:
- CNAME
<service>-staging→<render-service>.onrender.com, proxied. - No
AAAArecords for these names — Render is IPv4-only and staleAAAArecords misroute. - Wildcard exception: if a
*.tedos.devrecord is ever added on Render's side, the root must be gray-clouded (Render docs); we do not do this today. - The Render CLI has no custom-domain command. Channels: Blueprint
domains:(declares the association; DNS stays manual), the REST API (POST /v1/services/{id}/custom-domains), or the Dashboard.
State on 2026-09-07: api-staging resolves through Cloudflare, points at the tedos-api-staging
Render service, and /healthz returns 200 (allow up to ~60s on a cold start — the free-tier
service spin-down means the first probe after 15 min idle can take ~55s to answer). portal-staging
resolves and reaches tedos-portal-staging (renamed from tedos-portal-poc), but returns 404 — not
a DNS/domain-attachment problem: neither a staging tenant nor ENGINE_API_URL/
PORTAL_DEV_PROJECT_ID are set on the service yet.
Databases — one per environment
| Tier | Instance | DATABASE_URL / PRODUCT_DATABASE_URL |
|---|---|---|
| staging | Render Postgres tedos-staging-db (0.1c-256mb, pg18, 1 GB, oregon) — provisioned + migrated 2026-09-07 | both point at the SAME database (the api-staging env group carries both) |
| prod | Fly Managed Postgres tuempresa-pg (gru) — unchanged, live, until the cutover | both point at the same database (verified #1622) |
| prod (Render standby) | Render Postgres tedos-production-db (0.1c-256mb, pg18, 10 GB, oregon) — provisioned, empty | wired via the api-production group (sub #1655); schema from the first pre-deploy, data at the cutover restore (#1623) |
- One database per environment, both URLs on it (José, 2026-09-06). This mirrors how prod
actually runs; Postgres schemas keep the data apart (
plan/pgboss/product/client_*). tedos-staging-dbwastedos-pgboss-reliability-test(renamed 2026-09-07) — the pg-boss reliability test's database, kept after a PASS verdict and repurposed as staging's shared database rather than deleted (apps/api/docs/render-pg-boss-reliability-test.md). It carries the three migration journals (drizzle/plan/product) plus thevector/pg_trgmextensions created by hand — the migration set itself doesn't create them (packages/domain/src/provisioning.tscreates them per tenant instead).- Render's pre-deploy-command (a paid-plan feature) doesn't run migrations for staging today.
Two blockers, both confirmed empirically:
render jobs createrefuses on a free-plan base service (400 free tier plans are not supported for jobs, regardless of the job's own--plan-id), and staging's Postgres is only reachable from outside Render over its External URL with?sslmode=require+ an Access Control (IP allow-list) entry for the caller. Until #1761 wires an in-Render path, migrations run from a laptop against the External URL with both of those set. - Render free Postgres is not an option for anything durable: one per workspace, 1 GB, expires
30 days after creation (14-day grace). Both
tedos-staging-dbandtedos-production-dbare on the paid0.1c-256mbplan, not the free tier. - Staging data = a scrubbed copy of prod, run manually. Never a raw copy of tenant data. Design:
staging-data-pipeline.md.
Env-group layout (Render)
Environment groups are scoped to one Render environment (Render has no cross-environment shared group), so each repeats the five common vars:
| Group | Environment | Vars |
|---|---|---|
api-staging | Staging | NODE_ENV PORT HOST LOG_LEVEL WORKER_IN_WEB DATABASE_URL PRODUCT_DATABASE_URL — both URLs point at tedos-staging-db. tedos-api-staging carries no service-level vars. |
api-production | Production | NODE_ENV PORT HOST LOG_LEVEL WORKER_IN_WEB (declared in render.yaml) + Dashboard-managed since 2026-09-08 (sub #1655): DATABASE_URL/PRODUCT_DATABASE_URL (internal URL of tedos-production-db), the 16 secret names Fly's tedos-api carries, and CLOUDFLARE_API_TOKEN/CLOUDFLARE_ZONE_ID (moved in from service-level). tedos-api-production carries no service-level vars. The cutover (#1623) only restores DATA into the already-migrated database. |
Releases — how prod gets a version
-
Everything lands on
developthrough PRs; staging deploys automatically. -
A release is a PR
develop→main, merged when staging is green. Merging it IS the prod deploy: Vercel builds portal/console/docs frommain; the Fly API is deployed from a clean checkout atorigin/main(apps/api/DEPLOY-FLY.md). -
No tags, no changelog automation, no Actions in this effort — the PR is the release record.
-
A release PR is never CodeRabbit-reviewed (#1697): its content already passed review PR-by-PR on
develop, and the diff is hundreds of files. Mechanism, not luck: the PR carries thereleaselabel (a negative match in.coderabbit.yaml) and the titleRelease develop to main([DevOps] [#<effort>] Release develop to mainwhen it belongs to an effort). Either alone skips; always set both. Human review of the release PR = confirmmainequalsdevelop(tree diff empty) and that any lockfile conflict was resolved per the lockfile rule. -
Merge method for a release PR is a merge commit, never squash. A squash would give
mainthe same tree with a different history, so the nextdevelop→mainPR would conflict on every file touched since. With a merge commitmaincontainsdevelop's history and the next release PR is a clean fast-forward-style merge again. -
Update the release branch right before merging. If
developmoved after the PR was opened, mergeorigin/developinto the release branch first — the human check isgit diff --stat origin/develop <release-head>empty, and a stale branch fails it silently.
The first release (2026-09-07, sub #1690) — two separate events:
- Release: PR #1696 aligned
mainwithdevelop(511 commits apart on 2026-09-06;mainalso carried Dependabot commits not ondevelop, reconciled in the PR). It deployed nothing — at merge time the Vercel projects were not linked to the repo, so the merge produced no build. - Linking (after the merge): the three Vercel projects were linked to the repo with Production
Branch =
main(see § Two tiers). Linking triggers nothing by itself.
First merge-to-deploy verified (release PR #1733, merged 2026-09-07 08:01Z): the push to main
created one production deployment per project (source=git, ref=main, commit 69367358), all
three reached READY within four minutes, the custom domains were re-aliased to them, and
tuempresa.digital / console.tuempresa.digital / docs.tuempresa.digital answered 200.
Platform decisions and rejected options
- Render for staging (the 4 existing services). Region decided per environment — staging stays
oregon; revisited at the prod cutover (#1604). Render has no Mexico / South America region. render.yamlis a mirror of the Dashboard, not the live source of truth. The decision (José, 2026-09-07) is Auto Sync stays off — the CLI has no read command that confirms this, so verify in the Dashboard rather than assume, and never add adatabases:entry regardless: a Blueprint-declared existing instance with Auto Sync on could try to create a new paid resource (Render auto-syncs a Blueprint on every push to its tracked branch when Auto Sync is on — it never deletes resources, but it creates/updates them).render.yamltoday mirrors the twoapps/apiservices (tedos-api-staging/tedos-api-production) only — the three staging frontends (portal/console/docs) were CLI/Dashboard-created and stay outside the Blueprint; adding them is a migration step of its own, not a drive-by.- Coolify (self-hosted PaaS) evaluated, not adopted. 2–5× cheaper (Hetzner ~€4/mo, Vultr
Mexico City ~$12/mo, unlimited domains, built-in previews) but: self-hosted, still versioned
beta, 11 critical CVEs in January 2026 (five at CVSS 10), and it would put staging on a different platform than prod's likely target. - Render Preview Environments / Neon branching not adopted — previews are not wanted.
- Render official skills (github.com/render-oss/skills, 21 skills) are read directly when
needed, never installed as a plugin (they bundle an MCP server + auto-approval hooks —
.claude/rules/render-conventions.md).
Pointers
- Effort: #1650 (
[Effort] 1650 · [Infra] Environment domains and databases). - Staging data design:
staging-data-pipeline.md. - Render access + resources table:
.claude/rules/render-conventions.md. - Prod cutover Fly → Render (separate effort):
render-postgres-migration-plan.md,apps/api/docs/render-final-dns-cutover-runbook.md. - Tenant custom domains:
apps/api/docs/cloudflare-domain-automation-live-cutover.md. - Cost gate:
.claude/rules/cost-decisions.md.