Skip to content

Infraestructura

Dónde corre el sistema, cómo se despliega y a dónde va a migrar. Para el diseño del software ver Arquitectura; para levantarlo en tu máquina ver Onboarding.

Estado

Primer despliegue a producción: 2026-08-07. Sin clientes reales todavía.

Reparto de dominios

Todo cuelga de micamaronera.com, registrado y con DNS en Cloudflare.

SubdominioQué sirveDónde vive
api.micamaronera.comAPI REST (NestJS)Railway
admin.micamaronera.comPanel de tenantCloudflare Pages
platform.micamaronera.comPanel de plataformaCloudflare Pages + Access
docs.micamaronera.comEste sitioCloudflare Pages + Access
send.micamaronera.comRebotes y SPF del correoResend (automático)
micamaronera.com · www.Redirect a admin.Cloudflare Redirect Rules

Por qué subdominios de un mismo apex

No es estético: es lo que hace que la autenticación funcione sin debilitarla.

admin.micamaronera.com llamando a api.micamaronera.com es cross-origin pero same-site (mismo dominio registrable). Eso permite que la cookie httpOnly del refresh token siga con SameSite=Lax, que es el valor seguro (ADR-0014).

Con dominios distintos (ej. admin.pages.devapi.up.railway.app) habría que bajar a SameSite=None, más débil y frágil ante bloqueadores de terceros. El síntoma sería confuso: el login funciona, pero la sesión se pierde en cada F5.

Infraestructura actual

   [Técnico en campo]        [Administrador]           [Superadmin]
    Flutter + Drift        admin.micamaronera.com   platform.micamaronera.com
   (aún sin distribuir)     Cloudflare Pages         Cloudflare Pages + Access
          │                        │                         │
          └────────────────────────┼─────────────────────────┘
                                   │  HTTPS · SameSite=Lax

                        api.micamaronera.com
              ┌──────────────────────────────────────┐
              │   Railway · US East (Virginia)       │
              │   Docker · healthcheck /api/v1/health│
              │   pre-deploy: prisma migrate deploy  │
              └────────┬──────────────────┬──────────┘
                       │ red privada      │ red privada (IPv6)
                       ▼                  ▼
                Railway Postgres     Railway Redis
                (snapshots)          (cache RBAC + idempotencia)

                        micamaronera.com → Resend (SMTP 587)

Los tres servicios de Railway están en la misma región (US East / Virginia). No es opcional: si el backend queda en otra, cada query cruza el continente y la red privada pierde su razón de ser.

Componentes

ComponenteServicioNotas
APIRailway, imagen DockerDockerfile multi-stage, node:22-slim, usuario sin privilegios
Base de datosRailway PostgresVolumen persistente. Backups por snapshot, sin PITR
CacheRailway RedisVolumen persistente. Cache de permisos + idempotencia del outbox
Paneles webCloudflare PagesBuild automático desde main, CDN global, gratis
DocumentaciónCloudflare Pages + AccessAcceso restringido por email
CorreoResend (SMTP)Dominio apex verificado, región São Paulo

Variables de entorno en producción

Los valores viven en Railway y Cloudflare, nunca en el repositorio. Esta es la lista de qué se configura; el detalle de cada una está en .env.template de cada repo.

VariableServicioNota
DATABASE_URLRailwayReferencia al servicio Postgres (hostname privado, ver abajo)
REDIS_URLRailwayReferencia al servicio Redis (ver abajo)
REDIS_FAMILYRailway6 — obligatorio, ver más abajo
WEB_URLRailwayhttps://admin.micamaronera.com — base de los enlaces de invitación
CORS_ORIGINSRailwayLos dos paneles. Nunca vacío: vacío = permitir todos
COOKIE_SECURERailwaytrue
SWAGGER_ENABLEDRailwayfalse
JWT_*_SECRETRailwayTres secretos aleatorios, distintos de los de desarrollo
MAIL_*RailwayResend: host smtp.resend.com, puerto 587, user resend
VITE_API_URLCloudflare Pageshttps://api.micamaronera.com/api/v1 (admin y platform)
SUPERADMIN_EMAIL / _PASSWORDRailwayTemporales — borrar tras el primer login

Las dos conexiones de datos no se escriben a mano: se referencian al servicio vecino con la sintaxis de Railway, que resuelve al hostname privado interno (no sale a internet ni paga tráfico de salida):

bash
DATABASE_URL=${{Postgres.DATABASE_URL}}
REDIS_URL=${{Redis.REDIS_URL}}
REDIS_FAMILY=6

PORT no se configura: lo inyecta Railway. Fijarlo a mano deja el servicio inalcanzable.

Procedimiento de despliegue

Backend — automático al pushear a main:

  1. Railway construye la imagen desde el Dockerfile.
  2. Corre el pre-deploy: npx prisma migrate deploy. Si una migración falla, el deploy se aborta y la versión anterior sigue viva.
  3. Levanta el contenedor y verifica el healthcheck en /api/v1/health.

Paneles y docs — automático al pushear a main: Cloudflare Pages construye y publica.

Rollback: en Railway, Deployments → deploy anterior → Redeploy. Funciona solo si la migración es compatible hacia atrás — de ahí la regla de nunca hacer migraciones destructivas en un solo release (expandir primero, contraer en un release posterior).

Verificación post-despliegue

bash
curl https://api.micamaronera.com/api/v1/health

Debe responder "database": "connected".

El healthcheck es liveness, no readiness

/api/v1/health captura el error de base de datos y devuelve 200 igual, con database: "disconnected" en el cuerpo. Railway lo va a dar por sano aunque Postgres esté caído. Mirá el contenido, no el código de estado.

Checklist completo:

  • "database": "connected" en el health
  • Logs con Conectado a Redis y sin Redis no disponible
  • F5 en una ruta interna de admin. y platform. no da 404
  • El login en platform. sobrevive a un F5 (valida la cookie)
  • Una invitación real llega a una casilla externa y el magic link activa la cuenta
  • /api/docs devuelve 404

Gotchas conocidos

Cuatro cosas que rompieron el primer despliegue. Todas silenciosas o de síntoma engañoso.

REDIS_FAMILY=6 es obligatorio en Railway

La red privada de Railway es IPv6-only y ioredis resuelve DNS en IPv4 por defecto, así que redis.railway.internal no resuelve. El fallo es invisible: RedisService degrada a PostgreSQL por diseño y solo emite un warning con throttle. La app funciona, pero sin cache de permisos (una query a la base por request) y sin idempotencia del outbox.

Verificalo en los logs explícitamente. Ver ADR-0063.

Sin Pre-Deploy Command, la base queda vacía

Si el comando npx prisma migrate deploy no está configurado, el servicio arranca contra una base sin schema y muere en el onModuleInit de PermissionsService con P2021 relation "public.actions" does not exist, en bucle de reinicio.

El generador de Prisma no es determinista por defecto

moduleFormat e importFileExtension se infieren del entorno, y esa inferencia difiere entre macOS local y el contenedor de build de Linux: en el contenedor emite imports con extensión .ts que tsc (con module: commonjs) no reescribe, produciendo require("./internal/class.ts") y un ReferenceError: exports is not defined in ES module scope al arrancar. Están fijados explícitamente en prisma/schema.prismano quitar ese bloque.

El entrypoint es dist/src/main, no dist/main

PrismaService importa el cliente generado desde ../../generated/prisma/client, fuera de src/, lo que eleva el directorio raíz de la compilación. package.jsonstart:prod y el CMD del Dockerfile tienen que coincidir.

Costos aproximados

ConceptoAproximado
Railway (API + Postgres + Redis)US$ 10-20/mes según uso
Cloudflare Pages ×3$0
Cloudflare Access (≤50 usuarios)$0
Resend (hasta 3.000 emails/mes)$0
Dominio~US$ 10/año

Crédito de prueba

Railway arrancó con crédito de prueba. Cuando se agote, los tres servicios se detienen. Cargar método de pago antes de poner datos de un cliente real.


Infraestructura objetivo

La capa de datos migra a servicios gestionados especializados. Está documentada, no ejecutada.

ComponenteHoyObjetivoPor qué
PostgresRailwayNeonPITR real (Railway solo hace snapshots) + branching de base por entorno o PR
RedisRailwayUpstashTLS por defecto, pay-per-request; el uso real es mínimo
APIRailwayRailwaysin cambios
EstáticosCloudflare PagesCloudflare Pagessin cambios

Disparador

Cuando los datos de un cliente real vivan en producción.

Mientras la base esté vacía o solo tenga datos de demo, los snapshots de Railway alcanzan: si algo se rompe, se resetea y se resiembra. El día que haya información que no se puede regenerar, la diferencia entre snapshot diario y point-in-time recovery es la diferencia entre perder un día de trabajo de campo y no perder nada.

Por qué la migración es barata

Es un cambio de dos variables de entorno más un pg_dump/pg_restore. Cero cambios de código — precisamente porque tanto DATABASE_URL como REDIS_URL son connection strings y no host/puerto sueltos (ADR-0063).

Pasos previstos:

  1. Crear la base en Neon y el Redis en Upstash.
  2. pg_dump desde Railway → pg_restore en Neon (ventana de mantenimiento corta).
  3. Cambiar DATABASE_URL y REDIS_URL en Railway.
  4. Con Upstash, REDIS_FAMILY deja de hacer falta (URL pública con TLS): quitarla.
  5. Verificar el checklist post-despliegue completo.
  6. Recién ahí, dar de baja los servicios de datos de Railway.

Detalle de la decisión en ADR-0062.

Qué NO cambia

El backend sigue en Railway. Migrarlo también significaría rehacer build, dominio y variables sin ganar nada: Railway hace bien lo que hace con contenedores. Lo que no hace bien —para el nivel de garantía que pide el dato de un cliente— es ser una base de datos.