SaaS Starter

Bounded contexts

Os sete módulos entregues em apps/server/src/modules/, o que cada um possui, e o que cruza a fronteira.

Um bounded context = uma pasta sob apps/server/src/modules/ com seu próprio domain/, application/, infrastructure/, interfaces/http/. Módulos não importam de domain/ ou application/ uns dos outros. Eles se comunicam via:

  1. Eventos de domínio no event bus em memória (preferido para fan-out).
  2. HTTP quando se quer um contract de API estável.
  3. Shared kernel em packages/shared (apenas para primitives genuínos — IDs, erros, Result).

iam — Identity & access

apps/server/src/modules/iam/

Possui User, verificação de email, sessions (delegadas ao 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 Prisma, glue do 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 permissions users:*.

Flows de auth baseados em cookies funcionam porque apiClient.withCredentials = true no client. Não reintroduza bearer tokens.

tenancy — Organizações & memberships

apps/server/src/modules/tenancy/

Possui Organization, Membership, linhas dinâmicas de Role, e o middleware organizationContext que hidrata req.organization (org ativa) e suas permissions computadas para o usuário atual.

Um usuário pode pertencer a várias organizações; cada membership tem seu próprio role e, portanto, seu próprio grant de permissions. Trocar de org = atualizar o cookie active-org; a próxima request vê um req.grants diferente.

billing — Subscriptions, plans, providers

apps/server/src/modules/billing/

Possui Subscription, Plan, ingest de webhooks. Provider-agnostic: interface PaymentProvider em domain/, três implementações concretas em infrastructure/ (Stripe, Mercado Pago, Polar). O env PAYMENT_PROVIDER decide qual é vinculada.

Webhooks pousam em /webhooks/<provider>, são verificados, se transformam em eventos de domínio (subscription.activated, subscription.canceled, …), e atualizam a linha local de Subscription. O frontend lê Subscription.status — nunca a API do provider.

O middleware requireActiveSubscription (infrastructure/http/require-subscription.ts) gateia endpoints premium.

usage — Metering de free-tier e enforcement de cotas

apps/server/src/modules/usage/

Possui UsageMeterDefinition, UsageCounter, UsageEvent. Produtos downstream registram meters no boot (ex.: tickets_created, jobs_created), fornecem um mapa plano → cap, e chamam QuotaEnforcer.checkAndIncrement a partir dos use cases que produzem eventos faturáveis. Lê a subscription ativa de billing via o port cross-module IGetActiveSubscriptionPort — sem tocar as tabelas de billing.

Três cadências de reset (lifetime / monthly / yearly); as janelas periódicas rolam via o job BullMQ usage-period-rollover. Desabilitado por default (USAGE_METERING_ENABLED=false); ver Usage metering para a referência completa.

notifications — Email & in-app

apps/server/src/modules/notifications/

application/ declara o port (EmailSender); infrastructure/ provê Resend, SES, e um adapter no-op. Listeners (ex. user.created → welcome email) vivem no bootstrap e enfileiram um job BullMQ emails; o worker pega e chama o sender.

Por isso este módulo não tem pasta domain/ — não há aggregate, apenas um side effect.

storage — File uploads

apps/server/src/modules/storage/

Possui uploads de avatar (e qualquer outro binário que a app precise). Interface StorageProvider com implementações null / local / s3 / uploadthing. STORAGE_PROVIDER=null retorna 503 dos endpoints de upload — sem falha silenciosa.

As keys são content-addressed: avatars/{userId}-{sha256:16}.{ext}. O hash torna seguro o header de cache imutável (Cache-Control: public, max-age=31536000, immutable) entre re-uploads — um novo upload escreve uma nova key, então um CDN nunca serve bytes obsoletos.

URLs do provider local são construídas a partir de BETTER_AUTH_URL (a URL do server) porque o static middleware que serve uploads roda no server, não no Next.js.

audit — Audit log

apps/server/src/modules/audit/

Registro append-only de ações sensíveis (mudanças de role, eventos de billing, mutações admin de users). Listeners no event bus escrevem as linhas; não há API pública de mutação.

feature-flags — Gates de runtime

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

Flags por org, por usuário ou globais. Leituras são cacheadas in-process; escritas invalidam. O middleware req.featureFlags expõe um accessor tipado para que os handlers não espalhem string keys.

O que cruza uma fronteira

ConcernMecanismo
Usuário se cadastra → enviar welcome emailiam publica user.created → listener de notifications enfileira job
Webhook do Stripe ativa subscriptionbilling publica subscription.activated → listener de audit escreve log
Endpoint precisa saber se a subscription está ativaMiddleware lê tenancybilling (chamada cross-module estilo HTTP dentro do processo)
Branded ID, Result, DomainError@app/shared
Forma de body / response HTTP@app/contracts

Se você se pegar alcançando do domain/ de um módulo para o de outro, pare — isso é um evento ou um port, não um import.

Nesta página