Pular para o conteúdo principal

Arquitetura

A plataforma Ozzie é dividida em duas camadas: uma Channel API stateful que os aplicativos para consumidores chamam diretamente, e uma Intelligence API sem estado que o canal faz proxy internamente.


Design em duas camadas

┌──────────────────────────────────────────────────────────────────┐
│ YOUR APPLICATION (Lovable / consumer app) │
└─────────────────────────┬────────────────────────────────────────┘
│ all API calls

┌──────────────────────────────────────────────────────────────────┐
│ Layer 1 — Channel API (channel.ozzieapp.com) │
│ │
│ • Stateful: owns users, OTP, onboarding, chat history │
│ • Handles end-user authentication (OTP via WhatsApp or email) │
│ • Stores channel_users, otp_codes, chat_messages, transactions │
│ • Proxies intelligence requests to Layer 2 internally │
│ │
│ Base URL: https://channel.ozzieapp.com/v1 │
└─────────────────────────┬────────────────────────────────────────┘
│ internal proxy (not called by consumers)

┌──────────────────────────────────────────────────────────────────┐
│ Layer 2 — Intelligence API (api.ozzieapp.com) │
│ │
│ • Stateless: receives full context as request input │
│ • Returns plans, envelopes, chat responses, quiz scores │
│ • No session affinity, no stored history │
│ • personality_type is always a caller-supplied parameter │
│ │
│ Base URL: https://api.ozzieapp.com/v1 (internal use only) │
└──────────────────────────────────────────────────────────────────┘
informação

Os aplicativos para consumidores — incluindo o Lovable — chamam apenas channel.ozzieapp.com. A camada de inteligência em api.ozzieapp.com é um serviço interno; você não a chama diretamente.


Por que duas camadas?

A Channel API gerencia tudo que é stateful:

  • Identidade do usuário final e autenticação OTP
  • Progressão do estágio de onboarding (financial_intakepersonalityplanactive)
  • Histórico de chat persistente
  • Transações, metas e alertas de gastos

A Intelligence API gerencia tudo que é computacional:

  • Inferência de chat com IA sem estado
  • Pontuação de personalidade
  • Geração de plano e envelopes
  • Recomendações de movimentos financeiros

Mantê-las separadas permite que a camada de inteligência escale de forma independente e seja reutilizada em diferentes canais sem nunca ser responsável pelo estado do usuário.


Como personality_type flui

A personalidade é pontuada uma vez (usando os endpoints do quiz) e armazenada pela sua aplicação. Em cada chamada de inteligência subsequente, sua aplicação lê o personality_type armazenado e o passa como parâmetro:

User takes quiz


POST /v1/personality/score
→ returns personality_type (e.g. "GIVER")
→ your app stores it


On every subsequent intelligence call, your app passes it explicitly:

GET /v1/users/:id/financial-profile?personality_type=GIVER
POST /v1/users/:id/plan/generate body: { personality_type: "GIVER" }
POST /v1/money-moves/generate body: { personality_type: "GIVER", ... }

Princípio fundamental: personality_type nunca é armazenado na camada de inteligência. Sua aplicação é a fonte e sempre o passa explicitamente.


Tabelas do banco de dados

Tabelas da Channel API

TabelaFinalidade
channel_usersRegistros de usuários do canal com estado de onboarding
otp_codesArmazenamento de códigos OTP (hash bcrypt, com controle de TTL)
chat_messagesHistórico de conversa persistente por usuário
transactionsRegistros de transações do usuário
user_goalsMetas de poupança do usuário
money_move_tasksPassos agendados e concluídos
spending_alertsRegistros de alertas de gastos excessivos por envelope

Tabelas da Intelligence API

TabelaFinalidade
api_clientsCredenciais de clientes de API
api_rate_windowsEstado de rate limiting
usersRegistros de identidade de usuários
financial_intake_resultsSnapshots de intake enviados
financial_plansRegistros de planos gerados
personality_questionsCatálogo de perguntas do quiz
money_moves_catalogBiblioteca de templates de movimentos

Exemplos de fluxo de requisição

Autenticação OTP (camada de canal)

Consumer app: POST /v1/auth/otp/request
body: { identifier: "+5511999990000", language: "pt" }


channel-api:
1. Generates 6-digit code, bcrypt-hashes it, stores with 10-min TTL
2. Sends code via WhatsApp (auto-detected from E.164 phone)
3. Returns { channel, masked, expires_at }

Consumer app: POST /v1/auth/otp/verify
body: { identifier: "+5511999990000", code: "482910" }


channel-api:
1. Validates code against bcrypt hash
2. Returns { user_id, core_user_id, onboarding_stage }

Geração de plano (roteada pela camada de canal)

Consumer app: POST /v1/users/usr_123/plan/generate
body: { personality_type: "STOCKPILER" }


channel-api:
1. Verifies user belongs to this client
2. Loads user's stored intake data
3. Proxies to intelligence layer


intelligence-api:
1. Applies STOCKPILER envelope weights
2. Generates plan with action items
3. Returns plan


channel-api:
4. Persists plan to channel DB
5. Returns plan to consumer app

Completude de chat (com histórico persistente)

Consumer app: POST /v1/users/usr_123/chat/completion
body: {
message: "Am I on track?",
personality_type: "STOCKPILER",
user_context: { ... }
}


channel-api:
1. Loads conversation history from chat_messages
2. Proxies to intelligence layer with history injected


intelligence-api:
1. Uses message + history + personality_type + user_context
2. Returns AI reply


channel-api:
3. Persists user message + assistant reply to chat_messages
4. Returns reply to consumer app

Injeção de contexto

O endpoint de chat aceita um objeto user_context que sua aplicação monta a partir do seu próprio banco de dados antes de cada chamada. Este é o mecanismo principal para passar estado do usuário rico e em tempo real para a camada de inteligência sem que a API precise armazenar nada.

Campos que sua aplicação pode injetar:

  • Identidade e preferências: name, language, personality_type, plan_tier, plan_speed
  • Snapshot financeiro: monthly_income, goal_progress, total_saved_cents
  • Histórico comportamental: completed_moves_count, last_confirmation_note, next_move
  • Sinais de sessão: entry_point, spending_alert_envelope, spending_alert_ratio
  • Contexto de recuperação: rag_context — texto livre do seu próprio pipeline de recuperação

Todos os campos de user_context são opcionais. A camada de inteligência usa o que estiver presente para personalizar a resposta; campos ausentes simplesmente não são referenciados.

Your database channel-api request
──────────────── ──────────────────────────────
users.name ───► user_context.name
users.personality_type ───► user_context.personality_type
plans.plan_tier ───► user_context.plan_tier
plans.plan_speed ───► user_context.plan_speed
goals.percentage ───► user_context.goal_progress.percentage
moves.completed_count ───► user_context.completed_moves_count
moves.next_date ───► user_context.next_move.date
session.entry_point ───► user_context.entry_point
rag_pipeline.output ───► user_context.rag_context

Roteamento por entry_point

Quando um usuário abre a tela de chat, seu app sabe como ele chegou. Passar entry_point em user_context permite que o Ozzie gere uma mensagem de abertura contextualmente adequada (entry_point_opener) que seu app pode exibir antes de o usuário digitar qualquer coisa.

User arrives entry_point value entry_point_opener
──────────────── ───────────────── ──────────────────────────────
Opens app directly ───► "unprompted" ───► Personalized greeting with passos + next move
WhatsApp expense prompt ───► "weekly_expense" ───► Asks for expenses with input options
Overspending notification ───► "spending_alert" ───► References envelope + asks about recalibration
Monthly review link ───► "monthly_review" ───► Summarizes last month + asks about this month
Passo reminder ───► "move_reminder" ───► null (channel renders the passo card instead)
No entry_point sent ───► (omit field) ───► null

Seu app deve resolver o entry_point a partir dos parâmetros de inicialização da sessão (deep link, payload de notificação, etc.) antes de montar o objeto user_context.


Estados de erro

CódigoHTTPDescrição
INTAKE_REQUIRED422O intake financeiro deve ser enviado antes de gerar um plano
PERSONALITY_REQUIRED422personality_type deve ser fornecido (se obrigatório pelo endpoint)
PLAN_REQUIRED422O plano deve ser gerado antes que este endpoint possa ser chamado
INVALID_CODE400Código OTP incorreto ou já utilizado
CODE_EXPIRED400Código OTP ultrapassou o TTL de 10 minutos