Inventário de acesso ao banco — apps/fullstack e apps/worker
Documento gerado a partir de busca estática no código (prisma.*, $queryRaw, $executeRaw, $transaction) e revisão manual dos arquivos maiores. Não substitui EXPLAIN ANALYZE nem métricas de produção.
Convenções
| Coluna / termo | Significado |
|----------------|-------------|
| R | Leitura (findUnique, findFirst, findMany, count, aggregate, groupBy, $queryRaw de leitura) |
| W | Escrita (create, update, upsert, delete, *Many, $executeRaw de escrita, INSERT em $queryRaw) |
| Peso | Heurística de custo relativo na aplicação (volume de linhas, agregações, scans com filtro amplo, raw SQL em messages) |
| Cache | Cache-aside Redis de resultado de consulta (cacheAside no fullstack) ou cache pontual no worker; ver secção Cache |
Cliente Prisma: @pilot-status/database (prisma importado nos apps).
Escopo: código de produção sob apps/fullstack/src e apps/worker/src, excluindo **/*.test.ts e **/test/**. Testes de integração/unitários também usam Prisma para fixtures; não estão listados linha a linha (ver Apêndice).
Picos no pool (fullstack): em MessageService e PrismaAnalyticsRepository, leituras agregadas (vários count, findMany+count, groupBy, queries de analytics) executam em sequência por pedido, em vez de Promise.all em paralelo, para reduzir o número de ligações ao pool em simultâneo (com possível aumento ligeiro da latência do request).
Legenda de peso
| Peso | Critério (exemplos no repositório) |
|------|-------------------------------------|
| Alta | Várias contagens/agregações em messages; $queryRaw com SUM/AVG/buckets; listagem paginada + count com filtros amplos ou ILIKE; findMany em lote com cursor para reconciliação; groupBy em messageAttempt; admin com $queryRaw agregando por tenant; fluxo send-message com múltiplas transações por job |
| Média | findMany paginado com where indexável; updateMany limitado por batch; alguns joins |
| Baixa | findUnique / findFirst por id ou chave natural; create/update unitário; leituras pequenas em tabelas de configuração |
Cache
Fullstack — cacheAside (Redis, resultado de leitura)
Implementação: apps/fullstack/src/lib/cache.ts. Domínios e TTL padrão: apps/fullstack/src/shared/cache-domains.ts.
| Domínio (CacheDomain) | TTL padrão (s) | Staleness | Onde é usado (entrada HTTP) | Loader / dados |
|-------------------------|----------------|-----------|-----------------------------|------------------|
| dashboard_overview | 15 | near_real_time | app/api/dashboard/route.ts | Estatísticas via MessageService / analytics |
| analytics_charts | 60 | aggregated | app/api/analytics/dashboard/route.ts, app/api/v1/analytics/dashboard/route.ts, app/api/analytics/ask-ai/route.ts | MessageService.getAnalyticsDashboard (agregações em messages) |
| projects_list | 300 | static | app/api/projects/route.ts | Lista de projetos (Prisma) |
| templates_list / templates_details | 300 | static | app/api/templates/route.ts, app/api/templates/[id]/route.ts, app/api/v1/templates/route.ts, app/api/v1/templates/[id]/route.ts | Templates / detalhe |
| webhooks | 300 | static | app/api/webhooks/route.ts | WebhookService.listByTenant |
| api_keys | 20 | near_real_time | app/api/api-keys/route.ts | Lista de API keys |
| logs_messages | 10 | near_real_time | app/api/logs/route.ts | Logs de mensagens (PrismaAnalyticsRepository.getLogs ou caminho raw) |
| logs_optins | 10 | near_real_time | app/api/logs/optins/route.ts | WhatsAppTransactionalOptInService.listOptIns |
| subscription_status | 5 | critical | app/api/subscription/route.ts | Estado de subscrição |
| optin_status | 5 | critical | app/api/v1/messages/opt-in/route.ts | Opt-in por API pública |
| profile | 600 | static | app/api/profile/route.ts | Perfil do utilizador |
| production_request | 15 | near_real_time | app/api/production-request/route.ts | Pedido de produção |
| onboarding_assets | 600 | static | app/api/internal/onboarding/assets/route.ts | ensureOnboardingAssets |
| auth_accounts | 300 | static | app/api/auth/list-accounts/route.ts | Contas ligadas |
| admin_users / admin_templates / admin_production_requests / admin_feedback | 15 | near_real_time | app/api/admin/route.ts (consoante tab) | Listagens admin |
| logs_messages (chave inclui id da mensagem) | 10 | near_real_time | app/api/v1/messages/[id]/route.ts | MessageService.getByIdentifier |
| tenant_config (chave auto_recharge nas partes do cache) | 600 | static | app/api/auto-recharge/route.ts | prisma.autoRechargeSettings.findUnique + Stripe |
Invalidação automática após mutações Prisma (best-effort): apps/fullstack/src/lib/prisma-cache-invalidation.ts — registerPrismaMutationListener mapeia modelo → domínios Redis. Nota: resolveTenantIds pode executar leituras extra (tenant.findMany, user.findMany, webhook.findMany, template.findMany, etc.) para descobrir tenantId quando o where não traz tenant explícito — custo indireto além da mutação.
Invalidação explícita (invalidateTenantDomain) em rotas: por exemplo webhooks, api-keys, templates, projects, profile, internal/onboarding/*, internal/messages/cancel-scheduled, v1/messages/cancel, admin ao aprovar templates/pedidos.
Fullstack — Redis sem cache-aside de query Prisma
| Uso | Ficheiros / serviços |
|-----|---------------------|
| Dedup / lock ingestão Evolution | internal/webhook/dual-ingestion.ts, handlers de webhook |
| Eventos configuráveis do webhook (JSON) | services/webhook.service.ts — chaves ps:webhook:events:* |
| Alertas instância desligada | internal/webhook/handlers/connection.ts, etc. |
| Dedup pdev | app/api/pdev/webhook/route.ts |
Worker
| Tipo | Detalhe |
|------|---------|
| Sem cacheAside | Não existe camada equivalente ao fullstack para cachear resultados Prisma |
| Cache pontual de lista | list-desired-instance-names.ts: resultado de whatsAppInstance.findMany pode ser guardado em Redis (TTL WORKER_INSTANCE_NAMES_LIST_CACHE_TTL_MS) |
| Redis operacional | Locks (send-message), dedup RabbitMQ (evolution-consumer.ts), contadores retry (sent-delivery-retry.ts, sent-delivery-check.ts), alertas — não substituem leituras Prisma de negócio |
Nota: mutações Prisma feitas só no worker não passam pelo listener de invalidação do Next (registado no fullstack). O cache Redis do fullstack pode ficar stale até TTL ou até uma mutação ocorrer no processo fullstack / invalidação manual.
Fullstack — rotas app/api (Prisma direto ou via serviço citado)
Rotas com prisma no ficheiro (resumo). Onde só há serviços, indicam-se as operações principais no serviço (ver secção Serviços).
| Rota / ficheiro | R/W resumido | Peso | Cache (cacheAside / invalidação) |
|-----------------|-------------|------|--------------------------------------|
| admin/route.ts | R: $queryRaw contagens por tenant; user.findMany; productionRequest.findMany; feedback.findMany; template.findUnique (duas tabs); W: productionRequest.findUnique; project.upsert; $transaction productionRequest.update + project.update; productionRequest.update | Alta (raw agregado + listas admin) | cacheAside admin_*; invalidateTenantDomain em ações |
| api-keys/route.ts | Via cacheAside → serviço/repo API keys | Média | api_keys |
| auth/list-accounts/route.ts | Via cacheAside → auth / accounts | Baixa | auth_accounts |
| auth/unlink-account/route.ts | W: limpa cache keys Redis por prefixo; orquestra auth (Prisma em lib/auth.ts) | Média | Usa buildTenantDomainPrefix; invalidações em auth |
| auto-recharge/route.ts | Via serviço + cacheAside | Baixa–média | cacheAside |
| auto-recharge/setup/route.ts | W: auto-recharge.service (Prisma) | Baixa | — |
| billing/history/route.ts | R: histórico billing (Prisma no handler) | Média | — |
| checkout/route.ts | R/W: subscription.findFirst/create; packagePurchase.create | Média | — |
| dashboard/route.ts | Via cacheAside + MessageService / analytics | Alta (agregações) | dashboard_overview |
| feedback/route.ts | W: feedback create | Baixa | — |
| internal/auth/oauth/complete/route.ts | W: fluxo onboarding / user | Média | — |
| internal/auth/phone-number/verify-otp/route.ts | W: OTP / user | Média | — |
| internal/messages/[id]/resend/route.ts | W: MessageService / fila | Média | — |
| internal/messages/[id]/route.ts | R/W: mensagem interna | Média | — |
| internal/messages/[id]/trace/route.ts | R: message, messageAttempt, messageTimelineEvent | Média | — |
| internal/messages/send/route.ts | W: envio interno via MessageService | Média | — |
| internal/onboarding/confirm-phone/route.ts | R/W: project, projectWhatsAppOptIn, user | Baixa | invalidateTenantDomain("profile") |
| internal/onboarding/test-message/route.ts | R/W: user, whatsAppInstance, tenant | Baixa | invalidateTenantDomain("profile") |
| internal/onboarding/verify-auth/route.ts | R: project, projectWhatsAppOptIn | Baixa | — |
| internal/templates/[id]/test/route.ts | R: templates / versões | Baixa | — |
| internal/templates/test-draft/route.ts | R/W: rascunho | Média | — |
| internal/webhook/dual-ingestion.ts | R: evolutionIngestionEvent.findUnique; W: $executeRaw upsert ingestion; evolutionWebhookEvent.create; evolutionIngestionEvent.update | Média–alta (volume webhook) | Redis lock/dedup, não cache de SELECT |
| internal/webhook/handlers/connection.ts | R/W: whatsAppInstance, tenant, user | Média | Redis alertas |
| internal/webhook/handlers/messages-update.ts | W: message.update | Média | — |
| internal/webhook/handlers/messages-upsert.ts | R/W: project, projectWhatsAppOptIn (várias), webhook, apiKey | Média–alta | — |
| internal/webhook/persist-evolution-message-jids.ts | W: evolutionMessageJid.upsert | Baixa | — |
| internal/webhook/utils.ts | R: templateVersion.findUnique; whatsAppInstance.findUnique; tenant.findUnique; user.findMany | Média (caminhos longos) | — |
| labels/* | R/W: labels/contacts via PrismaLabelRepository / handlers | Média | — |
| logs/route.ts | Via cacheAside → PrismaAnalyticsRepository.getLogs | Alta com filtro status | logs_messages |
| pdev/webhook/route.ts | W: pdevWebhookProcessedEvent.createMany | Baixa | Redis dedup |
| production-request/route.ts | Via cacheAside | Baixa | production_request |
| profile/route.ts | Via cacheAside + updates | Baixa | profile |
| projects/route.ts | Via cacheAside + mutations | Média | projects_list |
| stripe/webhook/route.ts | R/W: user, autoRechargeSettings.upsert, tenant, packagePurchase, subscription (várias) | Média | — |
| subscription/route.ts | Via cacheAside | Baixa | subscription_status |
| templates/* | CRUD templates/versões + promote | Média | templates_* |
| v1/api-keys/route.ts | W: criação rotação API key (serviço) | Média | — |
| v1/groups/route.ts | R: dados grupos (Prisma/serviço) | Média | — |
| v1/messages/send/route.ts | W: MessageService | Média | — |
| v1/messages/opt-in/route.ts | Via cacheAside | Baixa | optin_status |
| v1/numbers/[id]/route.ts | R: tenant, apiKey (Promise.all) | Baixa | — |
| v1/numbers/[id]/status/route.ts | R: estado número (Prisma) | Baixa | — |
| v1/projects/route.ts | R: projetos API pública | Baixa | — |
| v1/templates/* | R: templates API pública | Baixa | cacheAside nas rotas GET |
| whatsapp-instances/route.ts | R: user.findUnique + serviço instâncias | Média | — |
| whatsapp-instances/[id]/route.ts | R/W: instância | Média | — |
| Rotas só com serviços (sem prisma no route.ts) | | | |
| webhooks/route.ts | WebhookService | Média | cacheAside + invalidação |
| webhooks/[id]/route.ts | WebhookService | Média | invalidação |
| logs/optins/route.ts | WhatsAppTransactionalOptInService | Média | logs_optins |
| analytics/dashboard/route.ts | MessageService.getAnalyticsDashboard | Alta | analytics_charts |
| v1/analytics/dashboard/route.ts | idem | Alta | idem |
| analytics/ask-ai/route.ts | idem + chamada IA | Alta (DB + IA) | analytics_charts |
| internal/onboarding/assets/route.ts | ensureOnboardingAssets | Média | onboarding_assets |
| internal/messages/cancel-scheduled/route.ts | MessageService.cancelScheduledForTenant + $executeRaw interno | Média | invalidação logs_messages |
| v1/messages/cancel/route.ts | idem | Média | invalidação logs_messages |
Fullstack — services/
| Ficheiro | Operações principais (modelos) | Peso |
|----------|-------------------------------|------|
| message.service.ts | project, apiKey, templateVersion, message (create, find, update, list+count, groupBy), $transaction; rawMessageListCount / rawMessageListIds; queryAnalyticsPeriodTotals / queryAnalyticsDailyBuckets; cancelScheduledMessageAtomicUpdate | Alta |
| webhook.service.ts | webhook.findMany, $transaction create/update com webhookWhatsAppInstance, webhook.delete, webhookLog.create/findMany | Média |
| whatsapp-instance.service.ts | whatsAppInstance CRUD + updates | Média |
| api-key.service.ts | project, apiKey | Média |
| template.service.ts | project, template, templateVersion (CRUD extensivo) | Média |
| auto-recharge.service.ts | autoRechargeSettings, billing | Baixa–média |
| payment.service.ts | subscription, packagePurchase, Stripe sync | Média |
| billing-event.service.ts | billingEvent.create | Baixa |
| subscription-access.service.ts | subscription, limits | Baixa |
| rate-limit.service.ts | subscription, message.count, packagePurchase.aggregate, autoRechargeSettings, message.count (diário) | Alta em tenants com muitas mensagens |
| monthly-usage-alert.service.ts | monthlyUsageAlert, tenant, user | Média |
| pilot-status-whatsapp-notify.service.ts | user.findMany | Baixa |
| whatsapp-transactional-optin.service.ts | opt-ins / listagens | Média |
| whatsapp-webhook-group-delivery-sync.service.ts | sync entrega grupos | Média |
Fullstack — lib/ e modules/*/adapters/prisma
| Ficheiro | R/W | Peso | Notas |
|----------|-----|------|--------|
| message-analytics-aggregates.ts | R: $queryRaw agregados e buckets por dia | Alta | Usado por MessageService.getAnalyticsDashboard |
| message-prisma-text-status-raw.ts | R: $queryRaw count, ids, listagens com compat status::text | Alta | Fallback quando enum MessageStatus em drift |
| message-cancel-scheduled-update.ts | W: $executeRaw update atómico cancelamento | Média | |
| prisma-cache-invalidation.ts | R: várias queries para resolver tenantId | Média | Disparada em mutations |
| app-session.ts | R: user, tenant, project | Baixa | Por pedido autenticado |
| auth.ts | R/W: account, tenant, subscription, phoneNumberOtp, user | Média | Fluxos OAuth / telefone |
| environment-permissions.ts | R/W: project | Baixa | |
| onboarding/ensure-onboarding-assets.ts | R/W: tenant, project, user, template | Média | |
| prisma-analytics.repository.ts | R: message.count x6 ou $queryRaw count; message.findMany+count; raw logs | Alta | |
| prisma-api-key.repository.ts | CRUD apiKey | Média | |
| prisma-subscription.repository.ts | subscription | Baixa | |
| prisma-customer-webhook.repository.ts | webhook | Média | |
| prisma-label.repository.ts | label, labelContact | Média | |
| prisma-whatsapp-instance.repository.ts | whatsAppInstance | Média | |
Worker — ficheiros de produção
| Ficheiro | R/W resumido | Peso | Cache |
|----------|-------------|------|--------|
| send-message.ts | message.findUnique (include grande); messageAttempt.count; message.update (muitos ramos); messageSendRequest.findUnique/update; whatsAppInstance.findUnique/updateMany; $transaction (marketing jitter / irregular batch); leituras extra message.findUnique | Alta | Redis lock / marketing keys apenas |
| message-reconciler.ts | R: $queryRaw / message.findMany (fila QUEUED); W: message.updateMany; R: messageSendRequest.findMany; R: messageAttempt.groupBy | Alta | — |
| sent-message-reconciler.ts | R: $queryRaw/message.findMany; R: evolutionWebhookEvent.findMany; W: message.update (várias) | Alta | — |
| delivered-read-reconciler.ts | R: $queryRaw/message.findMany; W: message.updateMany | Alta | — |
| sent-delivery-check.ts | R: message.findUnique; W: message.updateMany | Média | Redis resentKey |
| sent-delivery-retry.ts | R: $queryRaw/message.findMany; W: $executeRaw + message.updateMany; R: messageSendRequest.findMany | Alta | Redis contadores |
| ingestion-reconciler.ts | R: evolutionIngestionEvent.findMany; R: evolutionWebhookEvent.findFirst; W: evolutionIngestionEvent.update | Média | Redis done |
| rabbitmq/evolution-consumer.ts | R: evolutionIngestionEvent.findUnique; W: $executeRaw, evolutionWebhookEvent.create, evolutionIngestionEvent.update | Média | Redis lock |
| rabbitmq/evolution-go-consumer.ts | W: evolutionWebhookEvent.create | Baixa | — |
| all-instances-healthcheck.ts | R: whatsAppInstance.findMany/findUnique; W: whatsAppInstance.updateMany | Alta (muitas instâncias) | Redis alertas |
| recovery-sweep.ts | R: whatsAppInstance.findMany; W: whatsAppInstance.update | Média | — |
| whatsapp.ts | W: whatsAppInstance.updateMany; R: user.findMany | Média | — |
| labels-upsert.ts | W: label.upsert; R/W: labelContact | Baixa | — |
| evolution-status.ts | R: evolutionMessageJid.findMany | Média | — |
| list-desired-instance-names.ts | R: whatsAppInstance.findMany | Média | Sim — cache Redis opcional |
| index.ts | R/W: subscription, billingEvent, apiKey, message.updateMany, labelContact.updateMany, whatsAppInstance, tenant, user, subscription.findMany | Média–alta (updateMany globais) | Redis fila/worker |
| modules/messaging/adapters/prisma/prisma-message.repository.ts | message, messageSendRequest, messageAttempt | Média | — |
| modules/whatsapp-instance/adapters/prisma/prisma-whatsapp-instance.repository.ts | whatsAppInstance, user | Média | — |
| modules/billing-subscription/adapters/prisma/prisma-subscription.repository.ts | subscription | Baixa | — |
| bootstrap/shutdown.ts | W: prisma.$disconnect() | — | — |
Diagrama (fluxo simplificado fullstack)
flowchart LR
subgraph http [Next_API_Route]
R[GET_handler]
end
subgraph cache [Redis_cacheAside]
CA[cacheAside]
end
subgraph db [Prisma_Postgres]
P[queries]
end
R --> CA
CA -->|miss| P
CA -->|hit| R
P -->|mutation_listener| INV[invalidateTenantDomain]
INV --> cache
Apêndice — ficheiros de teste com Prisma
Pesquisa global inclui dezenas de ficheiros em **/test/** e *.test.ts (fixtures, mocks). Para listagem exata, executar no repositório:
find apps/fullstack/src apps/worker/src \( -path '*/test/*' -o -name '*.test.ts' \) \
-name '*.ts' -exec grep -lE 'prisma\\.' {} \;
Manutenção
Ao adicionar endpoints ou jobs:
- Incluir neste documento ou atualizar tabelas.
- Se usar
cacheAside, registar domínio emcache-domains.tse mapear invalidação emprisma-cache-invalidation.tsquando aplicável.