Pular para conteúdo

Arquitetura — Rune Courier

Visao Geral

graph TB
    subgraph Frontends
        FE1[Hotel Frontend<br/>:5173]
        FE2[Chronicle Frontend<br/>:3040]
        FE3[Chronos Frontend<br/>:3050]
    end

    subgraph "Rune Courier"
        REST[REST API<br/>/bff/*]
        WS[WebSocket<br/>/ws/chat]
    end

    subgraph Dados
        PG[(PostgreSQL<br/>:5460)]
        REDIS[(Redis<br/>:6381)]
    end

    subgraph Servicos Haus
        OATH[OATH<br/>:5001]
        GUILD[Guild<br/>:5002]
        HERALD[Herald<br/>:5004]
        SCROLLS[Scrolls<br/>:5003]
    end

    FE1 --> REST
    FE1 <--> WS
    FE2 --> REST
    FE2 <--> WS
    FE3 --> REST
    FE3 <--> WS

    REST --> PG
    REST --> REDIS
    REST --> OATH
    REST --> GUILD
    WS --> REDIS

    REST --> HERALD
    REST --> SCROLLS

Backend

Camadas

parley/
├── api/
│   ├── controllers/         # REST controllers (/bff/*)
│   │   ├── BffConversationController.java
│   │   ├── BffMessageController.java
│   │   ├── BffUserController.java
│   │   └── HealthController.java
│   ├── dtos/                # Request/Response DTOs
│   ├── websocket/           # WebSocket (/ws/chat)
│   │   ├── ChatWebSocket.java
│   │   ├── ChatEvent.java
│   │   └── ChatEventType.java
│   └── exceptions/          # GlobalExceptionHandler
├── application/services/
│   ├── ConversationService.java
│   ├── MessageService.java
│   ├── PresenceService.java
│   └── ReadReceiptService.java
├── domain/
│   ├── entities/            # Panache entities
│   │   ├── Conversation.java
│   │   ├── ConversationParticipant.java
│   │   └── Message.java
│   └── enums/
│       ├── ConversationType.java   # DIRECT, GROUP
│       ├── ContentType.java        # TEXT, IMAGE, FILE, SYSTEM
│       └── ParticipantRole.java    # ADMIN, MEMBER
└── infrastructure/
    ├── cache/RedisCacheService.java
    ├── security/
    │   ├── AuthFilter.java          # extends BaseAuthFilter
    │   └── ParleyContext.java       # (applicationId, tenantId, userId)
    ├── startup/GuildInitializer.java
    └── websocket/ChatBroadcastService.java

Seguranca

sequenceDiagram
    participant FE as Frontend
    participant AF as AuthFilter
    participant OATH as OATH Service
    participant PC as ParleyContext
    participant C as Controller

    FE->>AF: Request + Bearer Token<br/>+ X-Application-Id<br/>+ X-Tenant-Id
    AF->>OATH: POST /auth/introspect
    OATH-->>AF: User context (id, email, roles)
    AF->>PC: Popula (appId, tenantId, userId)
    PC->>PC: requireContext() valida
    AF->>C: Request processada

Modelo de Dados

erDiagram
    Conversation ||--o{ ConversationParticipant : "tem"
    Conversation ||--o{ Message : "contem"
    Message }o--o| Message : "reply_to"

    Conversation {
        uuid id PK
        varchar application_id
        varchar tenant_id
        varchar type "DIRECT | GROUP"
        varchar name
        uuid created_by
        boolean is_active
        jsonb metadata
        timestamp created_at
    }

    ConversationParticipant {
        uuid id PK
        uuid conversation_id FK
        uuid user_id
        varchar user_email
        varchar user_name
        varchar role "ADMIN | MEMBER"
        boolean is_active
        uuid last_read_message_id
        timestamp joined_at
    }

    Message {
        uuid id PK
        uuid conversation_id FK
        uuid sender_id
        varchar sender_email
        varchar sender_name
        text content
        varchar content_type "TEXT | IMAGE | FILE | SYSTEM"
        uuid reply_to_id FK
        boolean is_edited
        boolean is_deleted
        jsonb metadata
        timestamp created_at
    }

Indices

Tabela Indice Colunas
conversations idx_conversations_tenant (application_id, tenant_id)
conversations idx_conversations_active (is_active)
conversation_participants UNIQUE (conversation_id, user_id)
conversation_participants idx_participants_user (user_id, is_active)
messages idx_messages_conversation (conversation_id, created_at DESC)

Migrations (Flyway)

Versao Descricao
V1 CREATE TABLE conversations + indices
V2 CREATE TABLE conversation_participants + unique constraint
V3 CREATE TABLE messages + partial index (non-deleted)

WebSocket — Broadcast

O ChatBroadcastService gerencia conexoes ativas:

ConcurrentHashMap<String, Map<UUID, Map<String, WebSocketConnection>>>
                    │           │           │
                    │           │           └── connectionId → connection
                    │           └── userId → connections
                    └── tenantKey (appId:tenantId) → users

Fluxo de mensagem

sequenceDiagram
    participant U1 as User A
    participant REST as REST API
    participant DB as PostgreSQL
    participant BC as BroadcastService
    participant U2 as User B (WebSocket)

    U1->>REST: POST /bff/conversations/{id}/messages
    REST->>DB: INSERT message
    REST->>DB: UPDATE conversation.updated_at
    REST->>BC: broadcastNewMessage(conversationId, message)
    BC->>BC: Busca participantes da conversa
    BC->>BC: Filtra conexoes ativas no tenant
    BC->>U2: WebSocket: MESSAGE_RECEIVED

Autenticacao WebSocket

sequenceDiagram
    participant FE as Frontend
    participant WS as ChatWebSocket
    participant OATH as OATH Service
    participant BC as BroadcastService

    FE->>WS: Connect ws://host/ws/chat<br/>?token={jwt}&app={appId}&tenant={tenantId}
    WS->>OATH: POST /auth/introspect (token)
    OATH-->>WS: User context
    WS->>BC: registerConnection(appId, tenantId, userId, connection)
    WS-->>FE: Connected

    Note over FE,WS: Ping/pong para manter conexao

    FE->>WS: {"type":"TYPING","conversationId":"uuid"}
    WS->>BC: broadcastTyping(conversationId, userId, TYPING_START)

Redis

Estrutura de chaves

Chave Tipo TTL Uso
parley:presence:{app}:{tenant} SET - User IDs online
parley:typing:{conversationId}:{userId} STRING 5s Indicador de digitacao

Operacoes

Operacao Comando Redis
Marcar online SADD parley:presence:{key} {userId}
Marcar offline SREM parley:presence:{key} {userId}
Listar online SMEMBERS parley:presence:{key}
Verificar online SISMEMBER parley:presence:{key} {userId}
Indicar typing SET parley:typing:{convId}:{userId} 1 EX 5

Guild — Roles e Permissoes

O GuildInitializer registra no startup:

Roles

Role Descricao
PARLEY_ADMIN Administracao do chat (gerenciar conversas e participantes)
PARLEY_USER Usuario padrao (criar conversas e enviar mensagens)

Permissoes

Permissao Descricao
parley:conversation:create Criar conversas
parley:conversation:read Ler conversas
parley:conversation:manage Gerenciar conversas (editar, adicionar participantes)
parley:message:send Enviar mensagens
parley:message:edit Editar proprias mensagens
parley:message:delete Deletar proprias mensagens
parley:admin:* Acesso administrativo total

Configuracao

Portas

Servico Porta externa Porta interna
Parley app 5060 8080
PostgreSQL 5460 5432
PgBouncer 6460 6432
Redis 6381 6379

Variaveis de Ambiente

# Banco
DB_USER=parley
DB_PASSWORD=parley123
DB_URL=jdbc:postgresql://pgbouncer-parley:6432/parleydb

# Redis
REDIS_HOST=redis://parley-redis:6379

# Servicos
OATH_API_URL=http://oath:8080
GUILD_API_URL=http://guild:8080
HERALD_API_URL=http://herald:8080
SCROLLS_URL=http://scrolls:8080

# Aplicacao
GUILD_APPLICATION_ID=parley
GUILD_AUTH_EMAIL=parley
GUILD_AUTH_PASSWORD=PARLEY987!@#

Observabilidade

  • Logs: JSON estruturado para Grafana/Loki (service=parley)
  • Metricas: Prometheus via Micrometer (/q/metrics)
  • Health: /health, /health/ready, /health/live
  • Audit: Scrolls para eventos criticos (criacao conversa, delete mensagem)