Pular para o conteúdo

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):

HelperQuando 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.

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óduloPersonaAcesso a dados
clinical_summaryclinic_admin/staffPII clínica (com consent + redact)
scheduling_botclinic_admin/staffAgenda da clínica
database_analystconsultancyDados consultoria (Text-to-SQL guardrails)
sales_analystconsultancyFunil + leads
consulting_insightsconsultancyCross-clinic (com consent)
proposal_draftconsultancyLead + questionário
pre_call_briefingconsultancyCross-clinic
financial_health_advisorclinic_adminFinanceiro próprio
retention_radarconsultancyEngagement
platform_advisorplatform_adminApenas 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).