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:
- Eventos de dominio sobre el event bus en memoria (preferido para fan-out).
- HTTP cuando se desea un contrato de API estable.
- 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
| Concern | Mecanismo |
|---|---|
| User se registra → enviar welcome email | iam publica user.created → listener de notifications encola un job |
| Webhook de Stripe activa subscription | billing publica subscription.activated → listener de audit escribe el log |
| Endpoint necesita saber si la subscription está activa | Middleware lee tenancy → billing (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.