Arquitetura
Visão alto-nível
┌────────────────────────────────────────────────────────────────────┐│ apps/web (React+Vite, PWA) ││ ├── shells: AppShell · AuthShell · PublicShell ││ │ ConsultancyShell · PlatformShell ││ ├── BrandingProvider (CSS custom properties em runtime) ││ └── routes: /platform/* · /consultancy/* · /clinic/* · / │└────────────────────────────────────────────────────────────────────┘ │ │ TanStack Query · Supabase JS ▼┌────────────────────────────────────────────────────────────────────┐│ Supabase ││ ├── Postgres 15 + RLS multi-tier ││ │ ├── helpers: is_platform_admin · is_consultancy_member ││ │ │ consultancy_can_view_clinic ││ │ │ consultancy_has_data_access_to ││ │ │ is_clinic_member ││ │ ├── pgcrypto: PII encrypted (paciente · Acolhe · CPF · etc) ││ │ ├── audit_logs (imutável, trigger genérico) ││ │ └── data_subject_access_log (cross-tier acesso) ││ ├── Auth + Custom Access Token Hook ││ │ (claims: is_platform_admin, consultancy_ids, clinic_ids, ││ │ role_key, permissions, aal) ││ ├── Storage (assinaturas Assinafy, anexos cifrados) ││ └── Edge Functions (Deno) ││ ├── ai-gateway (10 módulos, redact, response_schema) ││ ├── asaas-webhook · assinafy-webhook (idempotentes) ││ ├── send-email (Resend) · send-push (web-push) ││ └── ~40 outras (ver docs/bootstrap-archive/05-edge-functions/specs/) │└────────────────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ Provedores externos ││ Asaas (cobrança 2 níveis: plataforma + consultoria) ││ Assinafy (assinatura digital — cláusula art. 299 CP) ││ Resend (email transacional, DKIM/SPF/DMARC verificados) ││ hCaptcha (anti-fraude em forms públicos) ││ Anthropic (LLM via AI Gateway) │└────────────────────────────────────────────────────────────────────┘Multi-tenancy em 3 níveis
Toda tabela tenant-scoped tem consultancy_id direto OU indireto via clinic.consultancy_id. Sem exceção. 5 helpers RLS canônicos cobrem todos os cenários — nunca usar is_superadmin() (não existe no v2):
| Helper | Quando usar |
|---|---|
is_platform_admin() | Tabelas de configuração + métricas agregadas + audit logs (sem PII) |
is_consultancy_member(consultancy_id) | Tabelas próprias da consultoria |
consultancy_can_view_clinic(consultancy_id, clinic_id) | Tabelas operacionais de clínicas (sem PII clínica) |
consultancy_has_data_access_to(clinic_id) | Tabelas operacionais COM PII clínica (respeita consent) |
is_clinic_member(clinic_id) | Tabelas próprias da clínica |
Detalhes em ADR 0001 — Multi-Consultancy Foundation.
Consent em camadas
Default deny. Cascata estrita pra platform_admin ver dado de clínica em agregado: clínica autoriza consultor (A=granted) E consultor autoriza plataforma (B=granted). Sem A → dado não flui pra cima nem em agregação.
Detalhes em ADR 0009 — Consent em Camadas.
AI Gateway
Toda chamada de LLM passa por supabase/functions/ai-gateway/index.ts. Nunca fetch('https://api.anthropic.com/...') direto do front ou de outra edge function. PII redacted antes de qualquer prompt (packages/ai/src/redact.ts).
10 módulos:
| Módulo | Persona | Acesso a dados |
|---|---|---|
clinical_summary | clinic_admin/staff | PII clínica (com consent + redact) |
scheduling_bot | clinic_admin/staff | Agenda da clínica |
database_analyst | consultancy | Dados consultoria (Text-to-SQL guardrails) |
sales_analyst | consultancy | Funil + leads |
consulting_insights | consultancy | Cross-clinic (com consent) |
proposal_draft | consultancy | Lead + questionário |
pre_call_briefing | consultancy | Cross-clinic |
financial_health_advisor | clinic_admin | Financeiro próprio |
retention_radar | consultancy | Engagement |
platform_advisor ⭐ | platform_admin | Apenas métricas agregadas (sem PII) |
response_schema JSON Schema: TODA chave que caller vai ler — mesmo nullable — está em required.
Branding em camadas
Tokens são CSS custom properties (--color-primary, --color-accent). <BrandingProvider> injeta valores em :root baseado no tenant resolvido — cascata custom_domain → subdomain → path → platform_default.
Consultorias e clínicas customizam apenas logo + 2 cores (primary + accent). Cores derivadas (success, danger, surface, text) por algoritmo com guarantee WCAG AA. Fonte fixa em Inter Variable bundled (@fontsource-variable/inter) — fontes externas em runtime são proibidas (LGPD: Google Fonts vaza dado).
Detalhes em ADR 0003 — Design System & Theming.
PWA + mobile-first
apps/web é PWA por padrão (display: standalone). Manifest válido, ícones 192/512/maskable, service worker via vite-plugin-pwa (Workbox). Notificações push são opt-in contextual (nunca no load).
Mobile-first 375×667. Tap targets ≥ 44×44. Fonte UI ≥ 14px (corpo ≥ 16px). Zero scroll horizontal em 375px. Inputs com inputmode/autocomplete. Gráficos com ResponsiveContainer. Safe-area via env(safe-area-inset-*). min-h-dvh (não h-screen — quebra iOS Safari).
Detalhes em ADR 0004 — PWA + Mobile-First + Push.
Edge Functions
Cada função tem entry [functions.<nome>] em supabase/config.toml com verify_jwt explícito. Test edge-functions-config bloqueia merge sem entry. Workflow deploy-edge-functions.yml deploya automaticamente em push pra main (lê config.toml como fonte de verdade).
Webhooks idempotentes via provider_event_id UNIQUE em billing_events. Webhook sem HMAC (caso Assinafy): handler reconsulta API autenticada antes de mudar estado interno.
Provider creds env-aware via resolver (getAsaasConfig(consultancyId), getPlatformAsaasConfig()) — nunca Deno.env.get('PROVIDER_API_KEY') direto.
Mais leitura
- ADRs — 15 decisões arquiteturais com contexto + alternativas.
AGENTS.md— 51 regras invioláveis.docs/bootstrap-archive/— referência canônica completa (~150 docs, congelada após Sprint 1 B1).