# 01 — Arquitetura > Documento espinha. Todos os outros dependem deste. Se houver conflito entre documentos, este prevalece sobre decisões de stack e estrutura. ## 1. Visão geral ``` ┌─────────────────── Docker Compose ───────────────────┐ QR / Wi-Fi │ │ na portaria │ traefik (TLS automático, roteamento) │ │ │ │ │ ▼ │ ├── portaria-backend (Spring Boot, stateless) │ web/visitor ──────┼──────┤ ├── REST (OpenAPI) │ React PWA │ │ ├── WebSocket (sinalização de chamada) │ │ │ ├── Outbox publisher + ShedLock │ apps/ morador ────┼──────┤ └── Resilience4j → LiveKit / FCM / APNs │ operador │ │ │ Compose MP │ ├── postgres 17 + PostGIS │ │ │ └── flyway · outbox · fila · jobs │ web/admin ──────┼──────┤ │ React + TS │ ├── livekit-server (SFU) + coturn (TURN) │ │ ├── minio (fotos, recados, gravações) │ │ └── otel-collector → prometheus/grafana/loki │ └──────────────────────────────────────────────────────┘ │ FCM (Android) · APNs + PushKit/CallKit (iOS) ``` **Princípio central:** o backend é *stateless*. Todo estado vive no Postgres, no MinIO ou no LiveKit. Nenhuma informação de chamada em andamento, timer de escalonamento ou sessão fica em memória de processo. Isso é o que permite replicar o backend e sobreviver a restart sem deixar visitante preso na porta. ## 2. Stack por camada | Camada | Tecnologia | Justificativa | |---|---|---| | Backend | **Spring Boot 3 + Kotlin** (JDK 21) | Transações declarativas, Actuator, ShedLock, Resilience4j e Spring Security de fábrica. A robustez aqui vem de infraestrutura pronta e testada, não de código nosso | | Persistência | **PostgreSQL 17 + PostGIS** | Geofence do visitante (`ST_DWithin`), fila durável, outbox e dados de negócio num só lugar transacional | | Migrations | **Flyway** | Versionamento linear e auditável do schema | | Acesso a dados | **jOOQ** | SQL tipado. Preferido sobre JPA aqui porque as consultas de auditoria e relatório do admin são analíticas, e JPA atrapalha nesse perfil | | Mídia | **LiveKit** (self-hosted) + **coturn** | SFU open-source, dockerizado, com SDKs oficiais Android, Swift e JS | | Objetos | **MinIO** (API S3) | Fotos, recados em vídeo e gravações. Trocável por S3 sem mudar código | | Apps móveis | **Compose Multiplatform** (Android + iOS) | Uma base de UI para morador e operador | | Web visitante | **Vite + React + TS** (PWA) | TTI ~1s em 4G. O visitante não instala nada e não espera | | Web admin | **Vite + React + TS** | Mesma toolchain; ecossistema maduro de tabela densa, mapa e player | | Compartilhado | **Kotlin Multiplatform** (`shared/`) | DTOs e máquina de estados, únicos entre backend e apps | | Observabilidade | **OpenTelemetry** → Prometheus / Grafana / Loki | SLOs medidos, não presumidos | ## 3. Estrutura do monorepo ``` Projeto-Portaria/ ├── settings.gradle.kts # inclui :backend e :shared ├── shared/ # Kotlin Multiplatform │ └── src/commonMain/kotlin/br/com/portaria/shared/ │ ├── model/ # entidades de domínio │ ├── dto/ # kotlinx.serialization — contrato de rede │ ├── state/ # máquina de estados (VisitStateMachine) │ └── validation/ # regras compartilhadas ├── backend/ # Spring Boot 3 + Kotlin │ └── src/main/kotlin/br/com/portaria/ │ ├── domain/ # entidades, agregados, portas │ ├── application/ # casos de uso (services) │ ├── adapter/ │ │ ├── in/rest/ # controllers + OpenAPI │ │ ├── in/ws/ # WebSocket de sinalização │ │ └── out/ # jooq, livekit, push, storage, notification │ ├── infra/ # outbox, scheduler, security, config │ └── resources/db/migration/ # Flyway ├── apps/ # Compose Multiplatform │ ├── composeApp/ # código comum (morador + operador) │ ├── androidApp/ │ └── iosApp/ ├── web/ │ ├── visitor/ # Vite + React + TS (PWA) │ ├── admin/ # Vite + React + TS │ └── shared-ui/ # design tokens + componentes comuns ├── infra/ │ ├── docker-compose.yml │ ├── docker-compose.prod.yml │ ├── livekit/ coturn/ traefik/ observability/ └── docs/ prompts/ ``` **Gradle** governa `shared/`, `backend/` e `apps/`. **pnpm workspaces** governa `web/`. São dois mundos de build que só se encontram no contrato OpenAPI. ### Fluxo de contratos ``` shared/dto (Kotlin) ──────► backend usa direto ──────► apps Compose usam direto │ └──► backend expõe /v3/api-docs (OpenAPI) │ └──► openapi-typescript ──► web/*/src/api/types.ts ``` Os webs **nunca** escrevem tipos de API à mão. São gerados no build a partir do OpenAPI, o que faz uma mudança de contrato quebrar o build do front em vez de quebrar em produção. ## 4. O módulo `shared/` Contém apenas o que precisa ser idêntico entre servidor e app. Não é uma biblioteca de utilidades. ```kotlin // shared/src/commonMain/kotlin/br/com/portaria/shared/state/VisitState.kt enum class VisitState { PENDENTE, TOCANDO, ESCALONADA, FILA_OPERADOR, EM_CHAMADA, AUTORIZADA, NEGADA, RECADO_EM_VIDEO, EXPIRADA, CANCELADA } object VisitStateMachine { private val transicoes: Map> = mapOf( PENDENTE to setOf(TOCANDO, CANCELADA), TOCANDO to setOf(EM_CHAMADA, ESCALONADA, AUTORIZADA, NEGADA, CANCELADA), ESCALONADA to setOf(EM_CHAMADA, FILA_OPERADOR, RECADO_EM_VIDEO, AUTORIZADA, NEGADA, EXPIRADA), // ... ) fun permite(de: VisitState, para: VisitState) = para in (transicoes[de] ?: emptySet()) fun ehFinal(estado: VisitState) = estado in setOf(AUTORIZADA, NEGADA, EXPIRADA, CANCELADA, RECADO_EM_VIDEO) } ``` A mesma classe valida no app (para desabilitar botões) e no servidor (para rejeitar comandos inválidos). O servidor é a autoridade — o cliente só antecipa. ## 5. Os sete padrões de robustez Não são recomendações. São o núcleo de confiabilidade do sistema, e cada um existe porque há um modo de falha concreto em que gente fica presa na porta. ### 5.1 Transactional outbox **Falha que previne:** a visita é criada, o processo cai antes de publicar a notificação, e o morador nunca fica sabendo que há alguém na porta. ```kotlin @Transactional fun criarVisita(cmd: CriarVisitaCommand): Visit { val visita = visitRepository.save(Visit.nova(cmd)) outbox.enfileirar(VisitaCriada(visita.id, visita.unitId)) // mesma transação return visita } ``` Um publisher separado lê a `outbox`, despacha e marca como publicado, com retry e backoff exponencial. Entrega *ao menos uma vez* — por isso os consumidores são idempotentes. ### 5.2 Timeouts como jobs persistidos **Falha que previne:** o processo que segurava o timer de 20 segundos morre e o escalonamento nunca acontece. Nunca `delay()`, `Timer` ou `@Scheduled` de instância única guardando estado. O timeout é uma **linha na tabela `scheduled_jobs`** com `run_at`. Um poller com **ShedLock** garante que apenas uma réplica execute cada job. ```kotlin @Scheduled(fixedDelay = 1000) @SchedulerLock(name = "visit-timeouts", lockAtMostFor = "30s") fun processarTimeouts() { /* SELECT ... FOR UPDATE SKIP LOCKED */ } ``` ### 5.3 Optimistic locking **Falha que previne:** dois moradores da mesma unidade respondem juntos; um autoriza, o outro nega, e o último a gravar vence em silêncio. Coluna `version` em `visits`. Conflito devolve `409` com o estado atual, e o app mostra "outro morador já respondeu". ### 5.4 Idempotência **Falha que previne:** morador com 4G ruim toca "Autorizar" três vezes e gera três autorizações. Toda mutação aceita header `Idempotency-Key`. A tabela `idempotency_keys` guarda a resposta por 24h; repetição devolve a resposta original sem reexecutar. ### 5.5 Degradação graciosa **Falha que previne:** LiveKit fora do ar significa prédio trancado. ``` LiveKit indisponível (circuit breaker aberto) └─► modo ÁUDIO (menos banda, mesma sala) └─► modo FOTO+TEXTO (visitante manda foto, morador aprova sem chamada) └─► FILA_OPERADOR (humano resolve por telefone) ``` O nível de degradação vigente é exposto em `/actuator/health` e visível no painel admin. ### 5.6 Fila durável em Postgres `SELECT ... FOR UPDATE SKIP LOCKED` sobre a tabela `job_queue`. Redis existe apenas para o LiveKit (obrigatório em multi-instância) e para rate limiting — **nunca** para dados que não podem se perder. Menos peças móveis, e o enfileiramento participa da mesma transação dos dados. ### 5.7 SLO medido | SLO | Alvo | Métrica | |---|---|---| | QR → celular do morador tocando | p95 < 5s | `portaria.visit.ring_latency` | | Taxa de atendimento pelo morador | > 70% | `portaria.visit.answered_ratio` | | QR → resposta em entrega | p95 < 10s | `portaria.delivery.resolution_latency` | | Abandono do entregador | < 10% | `portaria.delivery.abandoned_ratio` | | Disponibilidade do fluxo de entrada | 99,5% | uptime do caminho crítico | Sem essas métricas não se sabe se o produto está funcionando — a falha aqui é silenciosa por natureza. ## 6. Segurança — visão arquitetural Três identidades distintas, três mecanismos: | Quem | Autenticação | Duração | |---|---|---| | **Visitante** | JWT efêmero emitido ao validar o QR + geofence. Sem cadastro | 15 min, escopo de uma visita | | **Morador / operador** | OIDC (Spring Security) + refresh token no keystore do dispositivo | Access 15 min, refresh 30 dias | | **Admin** | OIDC + MFA obrigatório | Sessão de 8h | **Busca cega de unidade:** o endpoint de destino recebe bloco + unidade e **nunca** confirma se existe ou quem mora lá. Resposta idêntica para unidade válida e inválida — **inclusive na linha do tempo de estados**: a visita-sombra percorre os mesmos estados nos mesmos tempos sem notificar ninguém (`03-FLUXOS-E-CONTRATOS.md` §2). Isso impede que qualquer pessoa com o QR enumere quem mora onde — um problema de segurança física antes de ser de privacidade. Detalhes em `06-LGPD-E-SEGURANCA.md`. ## 7. Multi-tenant preparado, não ativado Toda tabela de negócio tem `tenant_id NOT NULL`. Todo repositório filtra por ele. Policies de **Row Level Security** são criadas nas migrations mas ficam `DISABLE`d na v1. ```sql CREATE POLICY tenant_isolation ON visits USING (tenant_id = current_setting('app.current_tenant')::uuid); ALTER TABLE visits DISABLE ROW LEVEL SECURITY; -- v1 ``` Ativar multi-tenancy vira `ENABLE ROW LEVEL SECURITY` + a UI de gestão. Sem isso, adicionar `tenant_id` depois seria reescrever todo o schema e toda query. ## 8. ADRs — decisões e o que se abriu mão **ADR-001 · Spring Boot em vez de Ktor.** Ktor é mais leve e idiomático, mas minimalista: agendamento, retry, métricas, transações e circuit breaker seriam código nosso. Num sistema onde falha silenciosa significa gente presa na porta, preferimos infraestrutura madura à elegância. *Custo:* mais cerimônia, startup mais lento, imagem maior. **ADR-002 · React em vez de Compose/Wasm na web.** O bundle Wasm (3–8MB) daria TTI de 3–6s em 4G na tela do visitante — abandono garantido na portaria. *Custo:* a UI web não é compartilhada com os apps; só os contratos são. **ADR-003 · LiveKit self-hosted em vez de gerenciado.** Atende o requisito de infra dockerizada e evita custo por minuto no núcleo do produto. *Custo:* operar SFU, TURN e banda. Mitigado por `VideoCallProvider`, que permite trocar por gerenciado sem tocar nos apps. **ADR-004 · jOOQ em vez de JPA.** As consultas mais complexas do sistema são analíticas (auditoria, relatórios, filtros do admin), onde JPA atrapalha. *Custo:* passo de geração de código no build. **ADR-005 · Fila em Postgres em vez de Redis/Kafka.** Volume real é de ~0,3 req/s; Kafka seria desproporcional. Postgres dá durabilidade e transacionalidade com os dados. *Custo:* não escala para milhões de mensagens — irrelevante nesta ordem de grandeza. **ADR-006 · Legítimo interesse em vez de consentimento.** Consentimento sob "aceite ou não entre" não é livre e não é base legal válida. Ver `06-LGPD-E-SEGURANCA.md`. **ADR-007 · Sem reconhecimento facial na v1.** Elevaria os dados a *sensíveis* (LGPD art. 11), com exigência jurídica muito maior. Decisão de produto com fundamento legal, não limitação técnica. ## 9. Referências - `02-MODELO-DE-DADOS.md` — schema, outbox, fila, versionamento - `03-FLUXOS-E-CONTRATOS.md` — máquinas de estado e API - `05-INFRA-DOCKER.md` — compose, portas, dimensionamento - `06-LGPD-E-SEGURANCA.md` — bases legais e modelo de ameaças