SaaS Starter

Bounded contexts

Los siete módulos incluidos en apps/server/src/modules/, qué posee cada uno, y qué cruza la frontera.

Un bounded context = una carpeta bajo apps/server/src/modules/ con su propio domain/, application/, infrastructure/, interfaces/http/. Los módulos no importan desde domain/ o application/ de otro. Se comunican mediante:

  1. Eventos de dominio sobre el event bus en memoria (preferido para fan-out).
  2. HTTP cuando se desea un contrato de API estable.
  3. Shared kernel en packages/shared (sólo para primitives genuinos — IDs, errores, Result).

iam — Identidad y acceso

apps/server/src/modules/iam/

Posee User, verificación de email, sessions (delegadas a BetterAuth). Repo:

domain/
  user.ts                  Aggregate. Username, locale, picture, active flag, lastLogin.
  email.ts                 Value object.
  user-repository.ts       Interface.
application/
  ports/
  use-cases/               Register, login, change-password, update-profile, list-users…
infrastructure/
  Adapter de Prisma, glue de BetterAuth.
interfaces/http/
  auth.controller.ts       /api/auth/*
  auth.middleware.ts       authMiddleware (401), sessionHydration (anon-friendly).
  users.routes.ts          /api/users — admin CRUD gateado por permisos users:*.

Los flows de auth basados en cookies funcionan porque apiClient.withCredentials = true en el client. No reintroduzcas bearer tokens.

tenancy — Organizaciones y memberships

apps/server/src/modules/tenancy/

Posee Organization, Membership, filas dinámicas de Role, y el middleware organizationContext que hidrata req.organization (org activa) y sus permisos computados para el user actual.

Un user puede pertenecer a muchas organizaciones; cada membership tiene su propio rol y, por tanto, su propio grant de permisos. Cambiar de org = actualizar la cookie de active-org; la próxima request ve un req.grants distinto.

billing — Subscriptions, plans, providers

apps/server/src/modules/billing/

Posee Subscription, Plan, ingest de webhooks. Provider-agnostic: interface PaymentProvider en domain/, tres implementaciones concretas en infrastructure/ (Stripe, Mercado Pago, Polar). El env PAYMENT_PROVIDER decide cuál se enlaza.

Los webhooks aterrizan en /webhooks/<provider>, son verificados, se convierten en eventos de dominio (subscription.activated, subscription.canceled, …), y actualizan la fila local de Subscription. El frontend lee Subscription.status — nunca la API del provider.

El middleware requireActiveSubscription (infrastructure/http/require-subscription.ts) gatea endpoints premium.

usage — Metering de free-tier y enforcement de cuotas

apps/server/src/modules/usage/

Posee UsageMeterDefinition, UsageCounter, UsageEvent. Los productos downstream registran meters al boot (p.ej. tickets_created, jobs_created), proveen un map plan → cap, y llaman a QuotaEnforcer.checkAndIncrement desde los use cases que producen eventos facturables. Lee la subscription activa de billing vía el port cross-module IGetActiveSubscriptionPort — sin tocar las tablas de billing.

Tres cadencias de reset (lifetime / monthly / yearly); las ventanas periódicas rolan vía el job BullMQ usage-period-rollover. Deshabilitado por default (USAGE_METERING_ENABLED=false); ver Usage metering para la referencia completa.

notifications — Email & in-app

apps/server/src/modules/notifications/

application/ declara el port (EmailSender); infrastructure/ provee Resend, SES, y un adapter no-op. Los listeners (ej. user.created → welcome email) viven en bootstrap y encolan un job BullMQ emails; el worker lo levanta y llama al sender.

Por eso este módulo no tiene carpeta domain/ — no hay aggregate, sólo un side effect.

storage — File uploads

apps/server/src/modules/storage/

Posee los uploads de avatares (y cualquier otro binario que la app necesite). Interface StorageProvider con implementaciones null / local / s3 / uploadthing. STORAGE_PROVIDER=null devuelve 503 desde los endpoints de upload — sin falla silenciosa.

Las keys son content-addressed: avatars/{userId}-{sha256:16}.{ext}. El hash hace seguro al header de cache inmutable (Cache-Control: public, max-age=31536000, immutable) entre re-uploads — un nuevo upload escribe una key nueva, así que un CDN nunca sirve bytes obsoletos.

Las URLs del provider local se construyen desde BETTER_AUTH_URL (la URL del server) porque el static middleware que sirve los uploads corre en el server, no en Next.js.

audit — Audit log

apps/server/src/modules/audit/

Registro append-only de acciones sensibles (cambios de rol, eventos de billing, mutaciones admin de users). Los listeners en el event bus escriben las filas; no hay API pública de mutación.

feature-flags — Gates de runtime

apps/server/src/modules/feature-flags/

Flags por org, por user o globales. Las lecturas se cachean in-process; las escrituras invalidan. El middleware req.featureFlags expone un accessor tipado para que los handlers no salpiquen string keys.

Qué cruza una frontera

ConcernMecanismo
User se registra → enviar welcome emailiam publica user.created → listener de notifications encola un job
Webhook de Stripe activa subscriptionbilling publica subscription.activated → listener de audit escribe el log
Endpoint necesita saber si la subscription está activaMiddleware lee tenancybilling (llamada cross-module estilo HTTP dentro del proceso)
Branded ID, Result, DomainError@app/shared
Forma de body / response HTTP@app/contracts

Si te encontrás llegando desde domain/ de un módulo al de otro, frená — eso es un evento o un port, no un import.

En esta página