Desarrollo en laptop
http://localhost:3000truedocker-compose up. Sin acceso externo.Cómo FARO Connect pasa de correr en una laptop a correr en staging, demo y producción sin improvisar. Stack Vercel + Supabase + Upstash, SemVer adaptado, ops.releases/deployments/release_backups, GitHub Actions y rollback con backup pre-release.
Este documento define cómo FARO Connect pasa de correr en la máquina del desarrollador a correr en ambientes controlados mostrables a un socio técnico, un piloto cliente o un inversor. Sin sobreactuar infraestructura, pero sin improvisar.
La regla central es directa: si FARO solo corre en una laptop, todavía no existe como plataforma. Puede existir como idea. Puede existir como demo. Pero no como producto serio que aguante una conversación profesional con un CTO o una decisión de inversión.
El objetivo del MVP no es infraestructura perfecta. Es infraestructura suficiente, reproducible, segura y no improvisada. Eso implica:
local, test, staging, demo, production. Cada uno con dominio, base de datos, storage, email e IA configurados explícitamente.0.1.0-demo → 0.3.0-mvp → 1.0.0. Cada deploy registra versión, commit, ambiente y metadata en ops.releases.ops: releases, deployments, release_backups. Permiten trazabilidad completa sin necesitar herramientas externas.El dataset demo vive en Empresa Demo Cuyo S.A. y solo se carga en staging y demo (nunca en producción, bloqueado por guard FARO_DEMO_MODE). Los catálogos canónicos (tensiones, acciones, evidencias, KPIs, prompts IA, Score model) son release-sensitive: cambian con versión, no a mano en consola.
"Local demuestra que se puede construir. Staging demuestra que se puede compartir. Producción demuestra que se puede operar. Release management demuestra que se puede sostener."
Principio rector. Todo deploy debe ser repetible (sin pasos manuales ocultos), auditable (saber qué versión, cuándo, quién), reversible (poder volver atrás), validable (smoke tests post-deploy), seguro (secrets fuera del repo), observado (logs, errores, health checks) y separado por ambiente (local no es staging, staging no es producción).
Cada ambiente tiene un propósito explícito, un dominio sugerido y un set de configuraciones por defecto. Mezclar ambientes es la forma clásica de romper producción con un seed demo o de mandar emails reales desde staging.
http://localhost:3000truedocker-compose up. Sin acceso externo.truemain. Vida útil: minutos.staging.faroconnect.apptruemain. Acceso con login obligatorio + robots: noindex.demo.farodireccion.comtrueapp.faroconnect.appfalse · forzado| Variable | local | test | staging | demo | production |
|---|---|---|---|---|---|
| Datos reales | No | No | No | No | Sí |
| Dataset Empresa Demo | Sí | Sí | Sí | Sí | No |
| Debug logs | Alto | Alto | Controlado | Controlado | Bajo |
| IA real | Opcional | Mock | Limitada | Limitada | Según plan |
| Email real | No | No | Sandbox | Whitelist | Sí |
| Storage | Fake local | Fake | faro-staging-* | faro-demo-* | faro-prod-* |
| Backups obligatorios | No | No | Diarios | Diarios | Continuos |
| RLS activo | Sí | Sí | Sí | Sí | Sí |
| Sentry | Opcional | No | Sí | Sí | Sí |
| Deploy manual | Sí | N/A | Auto + manual | Manual | Approval |
RLS desde el día uno. Row Level Security está activo en test, staging, demo y production. No es "para después". Activarlo tarde suele ser cirugía sin anestesia: rompe queries en cadena. Ver seguridad-rls-mvp.html para policies por tabla.
El MVP no necesita microservicios ni Kubernetes. Necesita un stack simple, gestionado, reproducible y con costos predecibles. La recomendación primaria balancea best-in-class por componente con costo bajo para etapa de validación.
Next.js App + APIs + server actions. Edge functions, preview deploys por PR, dominios incluidos. Deploy a staging desde main en segundos.
PostgreSQL 15+ gestionado, RLS nativo, storage privado integrado, dashboards SQL. Permite separar proyectos por ambiente (staging, demo, production).
USD 0-25/mes por proyectoRedis serverless. Usado para queue de jobs (workers), cache de queries pesadas, locks de score recalculation, rate limiting.
USD 0-10/mes por ambienteProcesos separados para jobs asincrónicos: workflow checker, score recalculation, notification dispatcher, weekly report generator, AI cache cleaner.
USD 7-15/mes por workerBuckets privados por ambiente. Guarda evidencia (PDFs, screenshots), reportes generados, exports XLSX.
USD 0-10/mes inicialProvider transaccional. API simple, dominio propio, templates HTML. Modo sandbox para staging/demo, real para producción.
USD 0-20/mesError tracking + performance + release health. Tags obligatorios: company_id, user_id, environment, release, module.
Workflows YAML versionados en repo. CI en cada PR, deploy staging automático desde main, deploy production con approval manual.
USD 0 (2000 min/mes free)| Aspecto | Vercel + Supabase + Upstash (primario) | Render o Railway (unificado) |
|---|---|---|
| Calidad por componente | Best-in-class · cada vendor especializado | Bueno · pero generalista |
| Operación | Más dashboards a mirar (3-4 paneles) | Un solo panel |
| Setup inicial | ~2 horas (3 cuentas + DNS) | ~45 min (una cuenta) |
| Costos primer año | USD 0-60/mes con tier free | USD 20-80/mes |
| Escalabilidad | Alta · cada componente escala solo | Media · escala todo junto |
| Vendor lock-in | Bajo · estándares abiertos (Postgres, Redis) | Medio · workflow propio |
| Recomendado para | Pre-piloto, MVP serio, pre-inversión | Proof of concept, demo único |
FARO no necesita microservicios todavía. Estrategia: una aplicación Next.js + workers separados + base PostgreSQL + Redis + storage. Separar en 12 servicios ahora es construir una fábrica antes de vender el primer producto.
| Proceso | Función | Deploy |
|---|---|---|
web | UI + APIs + server actions | Vercel |
worker | Jobs asincrónicos (score, reportes, alertas) | Render worker / Railway worker |
scheduler | Disparar jobs programados (cron) | Render cron / GitHub Actions schedule |
db | PostgreSQL gestionado | Supabase |
redis | Queue + cache + locks | Upstash |
Convención PREFIJO_NOMBRE con prefijos por dominio (APP_*, DATABASE_*, REDIS_*, STORAGE_*, EMAIL_*, AI_*, SENTRY_*, FARO_*). Toda variable sensible vive en provider secrets, nunca en Git.
Regla dura. Ningún secret debe vivir en Git. Archivos prohibidos en repo: .env, .env.local, .env.production, .env.staging.real, service-account.json, private-key.pem. El .gitignore los bloquea explícitamente y solo permite los .env.*.example.
| Variable | Tipo | local | staging | demo | production |
|---|---|---|---|---|---|
APP_ENV | config | local | staging | demo | production |
APP_URL | public | localhost:3000 | staging.faroconnect.app | demo.farodireccion.com | app.faroconnect.app |
NODE_ENV | config | development | production | production | production |
DATABASE_URL | secret | Docker local | Supabase staging | Supabase demo | Supabase prod |
REDIS_URL | secret | Docker local | Upstash staging | Upstash demo | Upstash prod |
AUTH_SECRET | secret | change-me | random 64 chars | random 64 chars | random 64 chars |
SESSION_COOKIE_NAME | config | faro_session | faro_session | faro_session | faro_session |
FARO_DEMO_MODE | config | true | true | true | false |
FARO_DEFAULT_TIMEZONE | config | America/Argentina/Mendoza | |||
FARO_TEST_NOW | config | Fecha congelada | Fecha congelada | Fecha congelada | — |
STORAGE_PROVIDER | config | fake | supabase | supabase | supabase |
STORAGE_BUCKET_EVIDENCE | public | fake | faro-staging-evidence | faro-demo-evidence | faro-prod-evidence |
STORAGE_BUCKET_REPORTS | public | fake | faro-staging-reports | faro-demo-reports | faro-prod-reports |
EMAIL_PROVIDER | config | fake | resend_sandbox | resend_sandbox | resend |
EMAIL_API_KEY | secret | — | Resend test key | Resend test key | Resend prod key |
EMAIL_FROM | public | FARO Connect <no-reply@farodireccion.com> | |||
EMAIL_ALLOWED_DOMAINS | config | — | whitelist demo | whitelist demo | * (todos) |
AI_PROVIDER | config | fallback | fallback | openai_limited | openai |
AI_API_KEY | secret | — | — | limited key | prod key |
AI_MODEL_DEFAULT | config | fallback | fallback | gpt-4o-mini | gpt-4o |
AI_DAILY_BUDGET_USD | config | 0 | 0 | 5 | 50 |
SENTRY_DSN | secret | — | staging DSN | demo DSN | prod DSN |
SENTRY_ENVIRONMENT | config | — | staging | demo | production |
SENTRY_RELEASE | config | — | Versión SemVer | Versión SemVer | Versión SemVer |
ENCRYPTION_KEY | secret | random | random 32 | random 32 | random 32 |
API_KEY_SALT | secret | random | random 32 | random 32 | random 32 |
LOG_LEVEL | config | debug | info | info | warn |
| Ambiente | Dónde se guardan los secrets | Rotación recomendada |
|---|---|---|
local | .env.local no commiteado · cada developer maneja el suyo | Al cambiar laptop |
test | GitHub Actions Secrets · scope repo | Cuando rota un team member |
staging | Vercel Environment Variables (encrypted at rest) | Cada 90 días |
demo | Vercel Environment Variables (encrypted at rest) | Cada 90 días |
production | Vercel + Supabase + Doppler/1Password como secret manager | Cada 60 días o post-incidente |
.env.exampleArchivo versionado en repo que documenta toda variable esperada con valores placeholder. Sirve de contrato entre developers y de checklist al setup en cada ambiente.
# ============================================================ # FARO Connect · .env.example # Copiar a .env.local para desarrollo. NO commitear .env.local. # ============================================================ # Application APP_ENV=local APP_URL=http://localhost:3000 NODE_ENV=development # Database / Cache DATABASE_URL=postgresql://faro:faro@localhost:54329/faro_demo REDIS_URL=redis://localhost:63799 # FARO config FARO_DEFAULT_TIMEZONE=America/Argentina/Mendoza FARO_DEMO_MODE=true FARO_TEST_NOW=2026-05-31T18:00:00-03:00 # Auth AUTH_SECRET=change-me-in-real-env SESSION_COOKIE_NAME=faro_session ENCRYPTION_KEY=change-me-32-chars-random API_KEY_SALT=change-me-32-chars-random # Storage STORAGE_PROVIDER=fake STORAGE_BUCKET_EVIDENCE=faro-evidence-local STORAGE_BUCKET_REPORTS=faro-reports-local # Email EMAIL_PROVIDER=fake EMAIL_FROM=FARO Connect <no-reply@farodireccion.com> EMAIL_ALLOWED_DOMAINS=farodireccion.com EMAIL_API_KEY= # AI AI_PROVIDER=fallback AI_MODEL_DEFAULT=fallback AI_DAILY_BUDGET_USD=0 AI_API_KEY= # Observability SENTRY_DSN= SENTRY_ENVIRONMENT=local SENTRY_RELEASE= LOG_LEVEL=debug
.gitignore mínimo# Environment files .env .env.* !.env.example !.env.demo.example !.env.staging.example !.env.production.example # Build artifacts node_modules .next dist build coverage playwright-report test-results # Logs *.log .DS_Store # Credentials / keys service-account*.json *.pem *.key *.p12
La app valida que toda variable requerida exista al arrancar. Si falta una, falla rápido con mensaje claro. Evita errores diferidos del estilo "todo OK hasta que llega el primer request".
const requiredEnv = [ "APP_ENV", "APP_URL", "DATABASE_URL", "REDIS_URL", "AUTH_SECRET" ]; export function validateEnv() { const missing = requiredEnv.filter((key) => !process.env[key]); if (missing.length > 0) { throw new Error(`MISSING_ENV_VARS: ${missing.join(", ")}`); } // Guard: producción jamás puede correr en modo demo if ( process.env.APP_ENV === "production" && process.env.FARO_DEMO_MODE === "true" ) { throw new Error("INVALID_ENV: FARO_DEMO_MODE cannot be true in production"); } }
FARO usa SemVer estándar MAJOR.MINOR.PATCH + sufijo de etapa (demo, mvp, staging, pilot). El sufijo comunica madurez sin tener que explicar el roadmap cada vez.
| Parte | Uso | Ejemplo bump |
|---|---|---|
MAJOR | Cambios incompatibles o release comercial importante | 0.x.x → 1.0.0 |
MINOR | Nueva funcionalidad relevante hacia atrás compatible | 0.2.0 → 0.3.0 |
PATCH | Correcciones, ajustes menores, hotfix | 0.2.0 → 0.2.1 |
| Sufijo | Etapa: -demo, -mvp, -staging, -pilot, -prod | 0.2.0 → 0.2.0-staging |
Demo local ejecutable. Corre en laptop con dataset Empresa Demo. No mostrable fuera del equipo.
Demo staging privada. Accesible vía URL con login. Catálogos canónicos + demo dataset.
MVP técnico completo: motor evaluador + score + workflows + reportes + alertas + IA fallback.
Piloto con empresa controlada. Dataset real, RLS productiva, monitoreo activo.
Primera versión comercial. Tres pilotos exitosos completos + onboarding self-service.
Cada release se taguea con prefijo v. El tag es la fuente de verdad: si no hay tag, no hay release.
# Crear tag anotado con mensaje git tag -a v0.2.0-staging -m "FARO staging demo release · dataset Empresa Demo Cuyo S.A." # Publicar tag a remoto git push origin v0.2.0-staging # Listar tags git tag -l "v*" --sort=-version:refname
Cada deploy genera metadata estructurada que se guarda en ops.releases y se inyecta en headers HTTP + logs + Sentry. Permite responder "¿qué versión está corriendo ahora mismo en staging?" sin abrir el dashboard del provider.
{
"version": "0.2.0-staging",
"commit_sha": "abc123def456",
"branch": "main",
"environment": "staging",
"deployed_by": "github-actions",
"deployed_at": "2026-05-31T18:00:00-03:00",
"migration_version": "V001-V113",
"score_model": "FARO_SCORE_MVP v1",
"ai_prompts": ["AI-EXPLAIN-SCORE v1"],
"demo_dataset": "EMP-DEMO v1",
"catalogs": {
"tensions": "TNS_CANONICAL v1",
"actions": "ACT_CANONICAL v1",
"evidence": "EVD_CANONICAL v1",
"rules": "rules_mvp_v1"
}
}
FARO no solo despliega código. Despliega reglas de dirección. Por eso versiona explícitamente los catálogos canónicos como parte del release metadata. Cambiar el Score model o un prompt sin bump de versión es la receta para no poder reproducir comportamiento histórico.
| Activo | Versión actual | Documento NDA |
|---|---|---|
| Score model | FARO_SCORE_MVP v1 | motor-score-mvp.html |
| AI prompts | AI-EXPLAIN-SCORE v1 | Pendiente FARO-AI-001 |
| YAML rules | rules_mvp_v1 | reglas-mvp-yaml.html |
| Catálogo tensiones | TNS_CANONICAL v1 | catalogo-tensiones-mvp.html |
| Catálogo acciones | ACT_CANONICAL v1 | catalogo-acciones-mvp.html |
| Catálogo evidencias | EVD_CANONICAL v1 | catalogo-evidencias-mvp.html |
| Demo dataset | EMP-DEMO v1 | Pendiente FARO-DEMO-001 |
Schema ops aislado del schema faro de dominio. Tres tablas que registran qué se desplegó, cuándo, dónde, cómo terminó y qué backup quedó disponible. Migraciones V110, V111, V112.
ops.releasesCatálogo de releases creados (no necesariamente desplegados con éxito). Una release es la intención de subir una versión a un ambiente. El estado se actualiza según el ciclo de vida.
-- ============================================================ -- FARO-DEPLOY-001 · V110__create_ops_releases.sql -- Catálogo de releases FARO -- ============================================================ CREATE SCHEMA IF NOT EXISTS ops; CREATE TABLE IF NOT EXISTS ops.releases ( release_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), version text NOT NULL, environment text NOT NULL CHECK ( environment IN ('local', 'dev', 'staging', 'demo', 'production') ), commit_sha text NOT NULL, branch_name text NOT NULL, release_title text NOT NULL, release_notes text NULL, migration_from text NULL, migration_to text NULL, score_model_version text NULL, ai_prompt_versions jsonb NOT NULL DEFAULT '[]'::jsonb, demo_dataset_version text NULL, status text NOT NULL DEFAULT 'created' CHECK ( status IN ('created', 'deployed', 'failed', 'rolled_back', 'cancelled') ), created_by text NULL, deployed_by text NULL, created_at timestamptz NOT NULL DEFAULT now(), deployed_at timestamptz NULL, rolled_back_at timestamptz NULL, metadata jsonb NOT NULL DEFAULT '{}'::jsonb, CONSTRAINT uq_releases_version_env UNIQUE (version, environment) ); CREATE INDEX IF NOT EXISTS idx_releases_env_time ON ops.releases (environment, created_at DESC); CREATE INDEX IF NOT EXISTS idx_releases_status ON ops.releases (environment, status, created_at DESC); COMMENT ON TABLE ops.releases IS 'Catálogo de releases FARO. Una release = intención de desplegar versión X en ambiente Y.';
ops.deploymentsCada release puede generar múltiples deployments (web, worker, scheduler). Esta tabla guarda el detalle por proceso: estado, duración, health post-deploy, smoke test, errores y URL de logs.
-- ============================================================ -- FARO-DEPLOY-001 · V111__create_ops_deployments.sql -- Deployments individuales por proceso -- ============================================================ CREATE TABLE IF NOT EXISTS ops.deployments ( deployment_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), release_id uuid NULL REFERENCES ops.releases(release_id), environment text NOT NULL CHECK ( environment IN ('dev', 'staging', 'demo', 'production') ), provider text NOT NULL, -- vercel, render, railway service_name text NOT NULL, -- web, worker, scheduler status text NOT NULL CHECK ( status IN ( 'pending', 'building', 'migrating', 'deploying', 'smoke_testing', 'success', 'failed', 'rolled_back' ) ), commit_sha text NOT NULL, image_tag text NULL, started_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz NULL, duration_ms integer NULL, health_status text NULL, -- healthy, degraded, unhealthy smoke_status text NULL, -- passed, failed, skipped error_code text NULL, error_message text NULL, logs_url text NULL, metadata jsonb NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_deployments_env_time ON ops.deployments (environment, started_at DESC); CREATE INDEX IF NOT EXISTS idx_deployments_status ON ops.deployments (environment, status, started_at DESC); CREATE INDEX IF NOT EXISTS idx_deployments_release ON ops.deployments (release_id); COMMENT ON TABLE ops.deployments IS 'Deployment individual por proceso (web/worker/scheduler) de una release.';
ops.release_backupsPara cada release a producción se exige backup pre-deploy. Esta tabla registra qué se respaldó, dónde quedó la referencia (URL/snapshot ID) y si la validación post-backup fue exitosa.
-- ============================================================ -- FARO-DEPLOY-001 · V112__create_ops_release_backups.sql -- Backups pre-release para auditoría y rollback -- ============================================================ CREATE TABLE IF NOT EXISTS ops.release_backups ( release_backup_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), release_id uuid NULL REFERENCES ops.releases(release_id), environment text NOT NULL, backup_type text NOT NULL CHECK ( backup_type IN ('database', 'storage', 'config') ), provider text NOT NULL, -- supabase, s3, manual backup_reference text NOT NULL, -- snapshot ID, URL, archivo status text NOT NULL CHECK ( status IN ('pending', 'success', 'failed') ), created_at timestamptz NOT NULL DEFAULT now(), metadata jsonb NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_release_backups_env_time ON ops.release_backups (environment, created_at DESC); CREATE INDEX IF NOT EXISTS idx_release_backups_release ON ops.release_backups (release_id); COMMENT ON TABLE ops.release_backups IS 'Backups pre-release con referencia al provider y status de validación.';
Dos helpers TypeScript que envuelven INSERT y UPDATE para que el código de aplicación no escriba SQL crudo a la tabla.
export async function createRelease(params: { client: any; version: string; environment: "dev" | "staging" | "demo" | "production"; commitSha: string; branchName: string; releaseTitle: string; releaseNotes?: string; }) { const result = await params.client.query( ` INSERT INTO ops.releases ( version, environment, commit_sha, branch_name, release_title, release_notes, status ) VALUES ($1,$2,$3,$4,$5,$6,'created') RETURNING release_id `, [ params.version, params.environment, params.commitSha, params.branchName, params.releaseTitle, params.releaseNotes ?? null ] ); return result.rows[0].release_id; }
export async function markReleaseDeployed(params: { client: any; releaseId: string; deployedBy: string; }) { await params.client.query( ` UPDATE ops.releases SET status = 'deployed', deployed_by = $2, deployed_at = now() WHERE release_id = $1 `, [params.releaseId, params.deployedBy] ); }
Build reproducible con pnpm + Node 20, Dockerfile separado para web y worker, scripts npm consolidados y health checks obligatorios. Sin esto, "funciona en mi máquina" se vuelve política empresarial.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"worker:start": "node dist/workers/runWorker.js",
"scheduler:start": "node dist/workers/runScheduler.js",
"db:migrate": "node scripts/db/migrate.ts",
"db:migrate:test": "APP_ENV=test node scripts/db/migrate.ts",
"db:seed:base": "node scripts/db/seed-base.ts",
"db:seed:demo": "node scripts/db/seed-demo.ts",
"db:seed:test": "node scripts/db/seed-test.ts",
"test:unit": "vitest run",
"test:sql": "node scripts/tests/run-sql-tests.ts",
"test:api": "vitest run --config vitest.api.config.ts",
"test:ui": "playwright test",
"test:ci": "pnpm test:unit && pnpm test:sql && pnpm test:api",
"qa:demo-check": "node scripts/qa/demo-check.ts",
"lint": "eslint . --ext .ts,.tsx",
"typecheck": "tsc --noEmit",
"deploy:staging": "bash scripts/deploy/deploy-staging.sh",
"deploy:demo": "bash scripts/deploy/deploy-demo.sh",
"release:create": "node scripts/release/create-release.ts",
"release:smoke": "node scripts/release/smoke-test.ts",
"ops:backup:pre-release": "node scripts/ops/backup-pre-release.ts",
"ops:workers:pause": "node scripts/ops/workers-pause.ts",
"ops:workers:resume": "node scripts/ops/workers-resume.ts"
}
}
FROM node:20-alpine AS base WORKDIR /app RUN corepack enable COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 CMD ["pnpm", "start"]
FROM node:20-alpine AS worker WORKDIR /app RUN corepack enable COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build CMD ["pnpm", "worker:start"]
Cinco endpoints que la app expone para que el provider (Vercel/Render), el load balancer y los scripts de smoke puedan validar estado.
| Endpoint | Responde | Uso |
|---|---|---|
GET /api/health | 200 si app arrancó | Liveness probe · provider lo pollea |
GET /api/health/deep | 200 si app + DB + Redis OK | Readiness · pre-routing |
GET /api/v1/score/current | Score actual visible | Smoke test post-deploy |
GET /api/v1/reports/weekly/latest | Último reporte semanal | Smoke test reportes |
GET /api/v1/ops/job-runs | Últimos jobs ejecutados | Smoke test workers |
Diez chequeos básicos que se corren contra el ambiente recién desplegado. Si alguno falla, el deploy se marca failed y se considera rollback automático.
export async function runSmokeTest(params: { baseUrl: string; email: string; password: string; }) { // 1. Liveness const health = await fetch(`${params.baseUrl}/api/health`); if (!health.ok) { throw new Error("HEALTH_CHECK_FAILED"); } // 2. Readiness (DB + Redis) const deep = await fetch(`${params.baseUrl}/api/health/deep`); if (!deep.ok) { throw new Error("DEEP_HEALTH_CHECK_FAILED"); } // 3-10. Login demo + dashboard + score + reporte + worker job // (implementación completa: ver scripts/release/smoke-test.ts) return { ok: true, checks_passed: 10, duration_ms: 4200 }; }
Tres workflows YAML versionados en .github/workflows/. CI corre en cada PR. Staging se despliega automáticamente al merge a main. Production requiere approval manual de un reviewer con permiso explícito.
Levanta Postgres 16 + Redis 7 como service containers. Corre lint, typecheck, migraciones, seeds test, unit/SQL/API/UI tests, demo-check y build. Bloquea merge si algo falla.
name: FARO CI on: pull_request: branches: [main] push: branches: [main] jobs: quality: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_USER: faro POSTGRES_PASSWORD: faro POSTGRES_DB: faro_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: redis:7 ports: - 6379:6379 env: DATABASE_URL: postgresql://faro:faro@localhost:5432/faro_test REDIS_URL: redis://localhost:6379 APP_ENV: test FARO_DEMO_MODE: "true" AI_PROVIDER: fallback EMAIL_PROVIDER: fake STORAGE_PROVIDER: fake steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: corepack enable - run: pnpm install --frozen-lockfile - name: Lint run: pnpm lint - name: Typecheck run: pnpm typecheck - name: Migrate test DB run: pnpm db:migrate:test - name: Seed test DB run: pnpm db:seed:test - name: Unit tests run: pnpm test:unit - name: SQL / RLS tests run: pnpm test:sql - name: API tests run: pnpm test:api - name: UI tests run: pnpm test:ui - name: Demo check run: pnpm qa:demo-check - name: Build run: pnpm build
Tras merge a main y CI verde, corre tests + build + migraciones staging + seed catálogos base + deploy app + smoke test. Reutiliza secrets de GitHub environment staging.
name: Deploy Staging on: workflow_dispatch: push: branches: [main] jobs: deploy-staging: runs-on: ubuntu-latest environment: staging steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: corepack enable - run: pnpm install --frozen-lockfile - name: Run CI checks run: pnpm test:ci - name: Build run: pnpm build - name: Run migrations staging env: DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }} APP_ENV: staging run: pnpm db:migrate - name: Seed base catalogs env: DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }} APP_ENV: staging run: pnpm db:seed:base - name: Deploy app to Vercel env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: npx vercel deploy --prod --token=$VERCEL_TOKEN - name: Smoke test staging env: STAGING_URL: ${{ secrets.STAGING_URL }} run: pnpm release:smoke -- --url $STAGING_URL
Disparado manualmente con input version (tag git). Valida tag, corre full test suite, backup pre-release, migraciones producción y deploy. Requiere approval de reviewer en GitHub environment production.
name: Deploy Production on: workflow_dispatch: inputs: version: description: "Release version (sin prefijo v)" required: true jobs: deploy-production: runs-on: ubuntu-latest environment: production # requiere approval steps: - uses: actions/checkout@v4 - name: Validate release tag run: | git fetch --tags git rev-parse "refs/tags/v${{ github.event.inputs.version }}" - uses: actions/setup-node@v4 with: node-version: 20 - run: corepack enable - run: pnpm install --frozen-lockfile - name: Full test suite run: pnpm test:ci - name: Backup pre-release env: DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }} run: pnpm ops:backup:pre-release - name: Run migrations production env: DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }} APP_ENV: production FARO_DEMO_MODE: "false" run: pnpm db:migrate - name: Deploy production env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: npx vercel deploy --prod --token=$VERCEL_TOKEN - name: Smoke production env: PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }} run: pnpm release:smoke -- --url $PRODUCTION_URL - name: Mark release deployed run: pnpm release:mark-deployed -- --version ${{ github.event.inputs.version }}
GitHub Environments con approval. Los workflows staging, demo y production usan environments configurados en GitHub. production requiere approval manual de un reviewer designado (con permiso explícito en Settings → Environments). El reviewer ve qué versión se despliega, qué migraciones aplica y aprueba o rechaza.
El rollback de código es trivial. El rollback de datos es delicado. Por eso FARO evita migraciones destructivas en el MVP y exige backup antes de cada release a producción. El restore drill mensual valida que el backup efectivamente se puede restaurar.
| Elemento | Rollback | Mecanismo |
|---|---|---|
| App code (web) | Sí | Versión anterior en Vercel (1 click) |
| Worker code | Sí | Deploy versión anterior + restart workers |
| Env vars | Sí | Provider dashboard (revert) |
| Migración aditiva | No hace falta | Columna nueva nullable no rompe versión vieja |
| Migración destructiva | Difícil | Restore de backup pre-release |
| Seeds catálogo | Sí | Script de re-seed con versión anterior |
| Datos cliente | No improvisar | Restore selectivo + validación manual |
| Storage | Versionado | Bucket versioning + restore por archivo |
| Score snapshots | No borrar | Recalcular nueva versión, mantener histórico |
ops.releases.status = rolled_back + entry en incident log.#!/usr/bin/env bash # Rollback FARO a versión previa con pausa de workers + smoke test set -euo pipefail VERSION_PREVIOUS="$1" ENVIRONMENT="$2" echo "Rolling back FARO ${ENVIRONMENT} to ${VERSION_PREVIOUS}" # 1. Pausar workers para evitar efectos duplicados echo "Pausing workers..." pnpm ops:workers:pause --env "$ENVIRONMENT" # 2. Deployar versión anterior echo "Deploying previous version..." pnpm provider:deploy --version "$VERSION_PREVIOUS" --env "$ENVIRONMENT" # 3. Smoke test contra versión recuperada echo "Running smoke test..." pnpm release:smoke --env "$ENVIRONMENT" # 4. Reanudar workers echo "Resuming workers..." pnpm ops:workers:resume --env "$ENVIRONMENT" # 5. Marcar release como rolled_back en ops.releases pnpm release:mark-rolled-back --version "$VERSION_PREVIOUS" --env "$ENVIRONMENT" echo "Rollback completed"
Antes de cada deploy a producción se ejecutan estos pasos. Sin backup verificado, el deploy se aborta.
Supabase snapshot completo + dump SQL como fallback. Referencia guardada en ops.release_backups.
Bucket versioning activo. Snapshot de manifests de archivos para validar integridad post-deploy.
Snapshot de env vars no secretas + feature flags actuales. Permite revertir configuración sin tocar provider.
Confirmar que el último backup automático corrió OK (no usar un backup viejo si los recientes están rotos).
Insertar referencia (snapshot ID + provider + status) antes de marcar release como deploying.
Una vez al mes, restaurar el último backup en ambiente sandbox y validar que la app levanta. Backup no validado = backup que no existe.
Toda migración está versionada, corre en CI, pasa por staging antes de producción y exige backup si es destructiva. Los seeds demo viven detrás de un guard que bloquea su ejecución en producción accidentalmente.
Prefijo numérico ordenado (V001, V002, ...). No reutilizar números. No editar migraciones ya aplicadas.
Cada migración nueva se aplica en el job de CI contra base test. Si falla, bloquea merge.
Toda migración corre en staging primero. Si tarda 30 min en staging, es un dato real, no un susto en producción.
DROP COLUMN, DROP TABLE, UPDATE masivo: bloqueados sin backup pre-release validado.
Seeds de demo no corren en producción salvo bandera explícita. Guard SQL + script de assertion.
Tensiones, acciones, evidencias y KPIs canónicos cargan en producción. Son contenido, no datos demo.
| Tipo | Ejemplo | Local / Test | Staging | Production |
|---|---|---|---|---|
| Schema (DDL) | Crear tabla | Sí | Sí | Sí |
| Patch aditivo | Agregar columna nullable | Sí | Sí | Sí |
| Seed catálogo | Tensiones canónicas v1 | Sí | Sí | Sí |
| Seed demo | Empresa Demo Cuyo S.A. | Sí | Sí | No |
| Data migration | Recalcular score histórico | Sí | Sí | Con control |
| Destructiva | DROP columna | Evitar | Evitar | Backup obligatorio |
ops.releases + monitorear 30-60 min post-deploy.Para evitar romper la versión anterior durante el deploy (cuando conviven temporalmente código viejo + nuevo), FARO usa el patrón expand → deploy → migrate data → contract.
Ejemplo · renombrar columna old_name a new_name. Mal: hacer RENAME y deploy. Bien: (1) Migración aditiva agrega new_name nullable. (2) Deploy app que escribe en ambos campos. (3) Migración de datos: copiar old_name → new_name. (4) Deploy app que solo lee new_name. (5) Migración contract: drop old_name. Cinco releases pequeñas en vez de uno grande con riesgo de downtime.
El seed demo valida explícitamente el ambiente antes de correr. Si está en producción, aborta con mensaje claro. Si FARO_DEMO_MODE no es true, también aborta. Cero margen para "ups, cargué la Empresa Demo en producción".
export function assertDemoSeedAllowed() { const appEnv = process.env.APP_ENV; const demoMode = process.env.FARO_DEMO_MODE; if (appEnv === "production" || demoMode !== "true") { throw new Error( "DEMO_SEED_NOT_ALLOWED: demo seeds require FARO_DEMO_MODE=true and non-production environment" ); } } // Uso en scripts/db/seed-demo.ts import { assertDemoSeedAllowed } from "./seed-demo-guard"; assertDemoSeedAllowed(); // aborta si no corresponde // ... resto del seed Empresa Demo Cuyo S.A.
| Seed | local | test | staging | demo | production |
|---|---|---|---|---|---|
| Catálogos base (TNS/ACT/EVD/KPI) | Sí | Sí | Sí | Sí | Sí |
| Roles + permisos base | Sí | Sí | Sí | Sí | Sí |
| Empresa Demo Cuyo S.A. | Sí | Sí | Opcional | Sí | No |
| Usuarios demo | Sí | Sí | Opcional | Sí | No |
| Dataset demo 6 meses | Sí | Sí | Opcional | Sí | No |
| Prompts IA | Sí | Sí | Sí | Sí | Sí |
| Score model + pesos | Sí | Sí | Sí | Sí | Sí |
Cada subsistema externo (storage, email, IA) tiene configuración explícita por ambiente. Mezclar buckets staging y producción es la forma clásica de exponer reportes confidenciales por accidente.
| Ambiente | Bucket evidencias | Bucket reportes | Public/private |
|---|---|---|---|
local | fake (filesystem temp) | fake (filesystem temp) | Local solo |
staging | faro-staging-evidence | faro-staging-reports | Privado · signed URLs |
demo | faro-demo-evidence | faro-demo-reports | Privado · signed URLs |
production | faro-prod-evidence | faro-prod-reports | Privado · signed URLs + encryption at rest |
Nunca mezclar buckets staging y producción. Parece obvio. También parecía obvio no borrar producción, y sin embargo la historia del software está llena de "ups". El código de aplicación lee el bucket desde STORAGE_BUCKET_*, jamás hardcoded. Tests RLS validan que el cliente staging no puede acceder a recursos production.
Todos los endpoints públicos usan prefijo /api/v1/. No romper contratos sin cambiar versión o mantener compatibilidad. Cuando se introduzca /api/v2/, v1 seguirá vivo al menos un release ciclo para dar tiempo a clientes integrados.
| Endpoint | Propósito | Versionado |
|---|---|---|
GET /api/v1/score/current | FARO Score actual + breakdown | Estable |
POST /api/v1/reports/weekly/generate | Disparar generación reporte semanal | Estable |
POST /api/v1/ai/explain-score | Explicación ejecutiva controlada con IA | Estable · prompt versionado |
GET /api/v1/tensions/active | Tensiones disparadas activas | Estable |
GET /api/v1/actions/open | Acciones abiertas asignadas | Estable |
GET /api/health | Liveness probe | No versionado · contrato fijo |
GET /api/health/deep | Readiness (DB + Redis) | No versionado · contrato fijo |
Configurable por EMAIL_PROVIDER. En staging/demo se usa sandbox o whitelist estricto para evitar enviar emails reales a clientes por error.
| Ambiente | Provider | Modo | Whitelist activa |
|---|---|---|---|
local | Fake (logs a consola) | Off | N/A |
test | Mock determinista | Off | N/A |
staging | Resend sandbox | Sandbox | farodireccion.com |
demo | Resend sandbox | Sandbox o whitelist | Lista cerrada por demo |
production | Resend / Postmark | Real | Off (envía a todos) |
Configurable por AI_PROVIDER. La demo no debe depender de que el provider IA esté vivo. Siempre hay fallback (template explicativo estático) si la IA falla o el budget diario se acabó.
| Ambiente | Provider | Model default | Budget diario USD | Fallback |
|---|---|---|---|---|
local | Fallback (estático) | fallback | 0 | Templates fijos |
test | Mock determinista | mock | 0 | Respuestas fijas |
staging | Fallback o real limitado | fallback | 0-2 | Templates fijos |
demo | Real limitado | gpt-4o-mini | 5 | Templates fijos |
production | Real según plan cliente | gpt-4o | 50+ | Templates fijos · retry exponencial |
Tres checklists imprimibles para usar antes y después de cada release. Son la diferencia entre "creo que está bien" y "verifiqué que está bien".
GET /api/health = 200.pnpm qa:demo-check OK · dataset coherente.ops.releases · status deployed.ops.release_backups.FARO_DEMO_MODE=false validado.GET /api/health/deep = 200.FARO-DEPLOY-001 queda aceptado cuando se cumplen los 17 criterios funcionales y 15 técnicos. Se rechaza ante cualquiera de los 12 casos críticos. El roadmap define 6 fases de implementación progresiva.
.env.example versionado.env.demo.example versionadoDockerfile + Dockerfile.workerscripts/deploy/*.shci.ymldeploy-staging.ymldeploy-production.ymlops.releasesops.deploymentsops.release_backupsvalidate-env.tssmoke-test.tsRELEASE_NOTES.mdrollback.shops.feature_flags integrado| Riesgo | Impacto | Severidad | Mitigación |
|---|---|---|---|
| "Funciona en mi máquina" | Bug en staging/prod que no se reproduce local | Alta | Docker + staging + CI con mismo Postgres/Redis |
| Migración rompe staging/prod | Downtime + data loss | Crítica | CI + staging primero + backup pre-release |
| Seed demo en producción | Datos sintéticos contaminan datos reales | Crítica | Guard assertDemoSeedAllowed() |
| Secrets filtrados a Git | Compromiso de DB/IA/email | Crítica | .gitignore + secret manager + rotación |
| Worker viejo escribe esquema nuevo | Corrupción de datos | Alta | Pausar workers en migraciones críticas + idempotencia |
| Env var faltante en boot | App no arranca o falla diferida | Media | validateEnv() al boot |
| AI provider cae | Demo se ve "rota" en reunión | Media | Fallback estático siempre disponible |
| Email manda a clientes reales por error | Spam o info confidencial filtrada | Alta | Sandbox + whitelist en staging/demo |
| Storage bucket público por mistake | Evidencia confidencial expuesta | Crítica | Buckets privados por default + audit periódico |
| RLS rompe queries en prod | App degradada o caída | Alta | Tests RLS obligatorios en CI |
| Rollback imposible por migración destructiva | Recovery via backup (downtime) | Alta | Evitar destructivas + backup obligatorio |
validate-envci.ymltest:ci consolidadodemo-checkops.releasesops.deploymentscreate-release.tsmark-deployed.tsdemo.farodireccion.comAtajo de uso diario · todos los scripts arrancan desde la raíz del repo.
El plan de deploy no vive solo. Estos son los puntos donde se conecta, depende o complementa con otros documentos NDA del pack.
DDL completo del sistema. Aquí se agregan las 3 tablas del schema ops (releases, deployments, release_backups) como migraciones V110-V112.
Matriz de estado de las 46 tareas FARO. FARO-DEPLOY-001 figura como pieza P1 crítica para pasar de demo local a staging mostrable.
Arquitectura modular que define qué se despliega a producción por industria. El versionado de catálogos atraviesa este roadmap.
RLS activa en todos los ambientes desde día uno. Tests RLS bloquean merge en CI. Sin esto, deploy a production no debe ocurrir.
Catálogo canónico release-sensitive. Cambios al catálogo viajan por el mismo pipeline que el código (PR + CI + staging + release).
Catálogo de acciones release-sensitive. Idem tensiones: pipeline completo, no edición a mano en consola productiva.
Catálogo de evidencias release-sensitive. Storage buckets configurados por ambiente reciben los archivos cargados según contrato.
Score model con versión propia (FARO_SCORE_MVP v1). Cambios al modelo se reflejan en release metadata + recálculo de snapshots históricos.
Observabilidad, jobs, errores y operación técnica. Define los health checks, Sentry config y monitoreo post-release que este documento exige.
Testing, calidad y validación MVP. Define los tests que CI corre antes de cada deploy (unit + SQL + API + UI + demo-check).
Gobierno, seguridad, auditoría y permisos. Define los approvals de GitHub environment y la rotación de secrets para producción.
Dataset Empresa Demo Cuyo S.A. + seed + storyboard. Lo que se carga en staging y demo pero nunca en producción.
Este documento define qué y cómo. Los pasos siguientes ejecutan la construcción real del pipeline y la sucesión de releases hasta producción piloto.
Fase 7 implícita · restore drill mensual. Una vez en producción, agendar un restore drill mensual: tomar el último backup automático, restaurarlo en ambiente sandbox y validar que la app levanta sin errores. Un backup que no se restaura periódicamente es un backup que probablemente no funciona el día que se necesita.
Este documento es el contrato técnico para llevar FARO Connect de laptop a staging, demo y producción. Con esto en mano, cualquier socio técnico o CTO puede ejecutar las 6 fases sin tener que reconstruir el porqué de cada decisión. Pasá al hub para ver el resto del pack o seguí con observabilidad/QA/gobierno.
→ Volver al hub modelos NDA