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:
- Eventos de domínio no event bus em memória (preferido para fan-out).
- HTTP quando se quer um contract de API estável.
- 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
| Concern | Mecanismo |
|---|---|
| Usuário se cadastra → enviar welcome email | iam publica user.created → listener de notifications enfileira job |
| Webhook do Stripe ativa subscription | billing publica subscription.activated → listener de audit escreve log |
| Endpoint precisa saber se a subscription está ativa | Middleware lê tenancy → billing (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.
links (mibio)
apps/server/src/modules/links/
Vertical link-in-bio para coaches e consultores — o produto mibio. Page é
o perfil público (slug, theme, estado de publicação). Block é a entrada
do smart link com type + config: jsonb, validada por schemas Zod por
BlockType (LINK, HEADER, PAYMENT_LINK, BOOKING_LINK, WHATSAPP,
LEAD_CAPTURE, IMAGE, SOCIAL_ROW; EMBED e TESTIMONIAL reservados).
A ordem dos blocks usa inteiros position esparsos (10, 20, 30…) para que
um reorder não precise renumerar todas as linhas.
Lead registra capturas dos blocks LEAD_CAPTURE.
@@unique([pageId, email]) previne cadastros duplicados por página.
Os repositórios seguem o layout DDD padrão. A superfície HTTP é montada em
/api/v1/links/* com permissões links:read / links:write; dados PII
de leads são gateados separadamente por links:leads:read /
links:leads:export.
Superfície pública. Dois endpoints sem auth alimentam a página ao vivo:
GET /api/v1/public/pages/by-slug/:slug— devolve a página publicada + blocks habilitados, servido por um cache LRU in-process (60s TTL, 1000 entradas) que envolve os repos Prisma. A invalidação vive eminfrastructure/cache/caching-repositories.ts: cadasave/deletena página ou em um block remove as chaves slug + page-id correspondentes, então as escritas do admin aparecem na próxima leitura pública. A response setaX-Cache: HIT|MISSpara debug +Cache-Control: public, max-age=60, stale-while-revalidate=300para qualquer CDN à frente cachear de forma coerente.POST /api/v1/public/leads— captura{ blockId, email, name? }, derivapageIddo block (a superfície pública não pode spoofar outra página), upsert em(pageId, email)para deduplicar, rate-limited por IP via o tierauthIpLimiter.
Templates. Quatro arquétipos de coach hardcoded em
modules/links/infrastructure/templates/registry.ts (Coach Ejecutivo,
Wellness, Terapeuta, Consultor B2B). NÃO são um modelo Prisma — na
criação da página, o use case applyTemplate traduz um BlockSeed[] em
linhas reais de Block nas posições 10, 20, 30, … e copia o theme,
title e bio do template para a página. GET /api/v1/links/templates
lista (name / description / thumbnail).
Render público. A rota catch-all do Next App Router
app/[slug]/page.tsx faz SSR fetch a /api/v1/public/pages/by-slug/:slug
e renderiza a página com oito componentes de block sob
apps/client/components/public/blocks/ (LeadCapture é o único client
component — gerencia o estado do form). Slugs reservados são filtrados
server-side via apps/client/lib/public/reserved-slugs.ts (mirror do
SLUG_RESERVED do contrato + rotas de marketing) então /admin,
/login, etc. não colidem com rotas reais. Uma imagem OG dinâmica em
app/[slug]/opengraph-image.tsx (next/og) renderiza um PNG 1200×630 do
perfil para os links compartilhados.
Editor é plano 4.
analytics (mibio)
apps/server/src/modules/analytics/
Event store append-only para tráfego das páginas mibio. PageView e
BlockClick são linhas imutáveis; DailyRollup é o agregado
materializado ((pageId, date) único) que alimenta o tab de analytics
do editor sem re-escanear eventos brutos.
Caminho de ingest. Dois endpoints POST públicos sem auth em
/api/v1/public/analytics/{view,click} validam um payload Zod pequeno e
retornam 204 antes de fazer o trabalho. O handler resolve a página
(por slug para views, por blockId para clicks) e insere uma linha;
payloads inválidos / slugs desconhecidos são logados em warn e nunca
chegam ao caller. Isto é fire-and-forget por design — a página pública
SSR emite esses beacons via navigator.sendBeacon (clicks) e
fetch({ keepalive: true }) (view inicial), então não bloqueiam o
render nem são cancelados ao navegar para uma URL externa.
Caminho de leitura. GET /api/v1/links/pages/:id/analytics?range=7d|30d|90d
retorna totais + clicks por block + série densa por dia. Gateado por
links:read e scoped à organização dona.
Jobs em background. Dois schedules BullMQ:
analytics-rollup— diário às 03:15 UTC. Para cada página com atividade nas últimas 36h, materializa os totais de ontem emDailyRollup. Idempotente via unique key em(pageId, date).analytics-prune— semanal aos domingos 04:00 UTC. Apaga linhas raw dePageView+BlockClickcom mais de 90 dias.DailyRollupé conservado para sempre (barato, uma linha por página por dia).
Ambos os processors vivem em analytics/infrastructure/jobs/ e se
registram em bootstrap/worker.ts; o installer do schedule
(scheduleAnalyticsJobs) roda em cada cold start a partir de index.ts
e é no-op quando Redis não está configurado.
O render público já está vivo (ver links acima). A UI de analytics do
editor chega no plano 4.