Saltar al contenido principal

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

TierBranchDomain rootDeploys whenHosting
stagingdeveloptedos.devevery merge to develop (platform auto-deploy)Render free services from effort #1604
prodmaintuempresa.digitalevery 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-compose Postgres. *-dev.tedos.dev names 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 main since 2026-09-07 (sub #1690). The Vercel projects tedos-comprender (portal), tedos-console, and tedos-docs are linked to the GitHub repo with Production Branch = main. Before that date they had no Git integration at all — every prod deploy was a manual vercel deploy --prod from the CLI, and the three frontends had drifted days to months behind develop. Non-main pushes create no deployment: each app's vercel.json ignoreCommand exits 0 (skip) unless VERCEL_GIT_COMMIT_REF is main, then hands off to turbo-ignore so 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 whose vercel.json predates the main-only ignoreCommand (two such branches still built previews the same morning) — and gitComments off (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 ID 05c83e7bec7484ad57c69c97a2617695 — 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

ServiceStagingProduction (unchanged)
apps/apiapi-staging.tedos.devapi.tuempresa.digital
apps/portalportal-staging.tedos.devtuempresa.digital
apps/consolenone — not wantedconsole.tuempresa.digital
apps/claude-kit-docsnone — not wanteddocs.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 AAAA records for these names — Render is IPv4-only and stale AAAA records misroute.
  • Wildcard exception: if a *.tedos.dev record 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

TierInstanceDATABASE_URL / PRODUCT_DATABASE_URL
stagingRender Postgres tedos-staging-db (0.1c-256mb, pg18, 1 GB, oregon) — provisioned + migrated 2026-09-07both point at the SAME database (the api-staging env group carries both)
prodFly Managed Postgres tuempresa-pg (gru) — unchanged, live, until the cutoverboth point at the same database (verified #1622)
prod (Render standby)Render Postgres tedos-production-db (0.1c-256mb, pg18, 10 GB, oregon) — provisioned, emptywired 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-db was tedos-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 the vector/pg_trgm extensions created by hand — the migration set itself doesn't create them (packages/domain/src/provisioning.ts creates 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 create refuses 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-db and tedos-production-db are on the paid 0.1c-256mb plan, 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:

GroupEnvironmentVars
api-stagingStagingNODE_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-productionProductionNODE_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

  1. Everything lands on develop through PRs; staging deploys automatically.

  2. A release is a PR developmain, merged when staging is green. Merging it IS the prod deploy: Vercel builds portal/console/docs from main; the Fly API is deployed from a clean checkout at origin/main (apps/api/DEPLOY-FLY.md).

  3. No tags, no changelog automation, no Actions in this effort — the PR is the release record.

  4. 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 the release label (a negative match in .coderabbit.yaml) and the title Release develop to main ([DevOps] [#<effort>] Release develop to main when it belongs to an effort). Either alone skips; always set both. Human review of the release PR = confirm main equals develop (tree diff empty) and that any lockfile conflict was resolved per the lockfile rule.

  5. Merge method for a release PR is a merge commit, never squash. A squash would give main the same tree with a different history, so the next developmain PR would conflict on every file touched since. With a merge commit main contains develop's history and the next release PR is a clean fast-forward-style merge again.

  6. Update the release branch right before merging. If develop moved after the PR was opened, merge origin/develop into the release branch first — the human check is git 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 main with develop (511 commits apart on 2026-09-06; main also carried Dependabot commits not on develop, 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.yaml is 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 a databases: 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.yaml today mirrors the two apps/api services (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.