Ir para o conteúdo

Software Design Document (SDD)

Versão: 1.0.0 | Data: 2026-03-21 | Status: ✅ Aprovado


1. Diagramas de Sequência

1.1 — Autenticação

Fluxo de login com e-mail/senha ou Google OAuth. A autenticação acontece diretamente entre o Frontend e o Supabase Auth — o Backend apenas valida o JWT nas requisições subsequentes.

sequenceDiagram
    actor U as Usuário
    participant FE as Frontend (Next.js)
    participant SB as Supabase Auth
    participant BE as Backend (FastAPI)

    U->>FE: Clica em "Entrar com Google" ou preenche e-mail/senha
    FE->>SB: supabase.auth.signInWithOAuth() ou signInWithPassword()
    SB-->>FE: JWT (access_token + refresh_token)
    FE->>FE: Salva tokens em memória (não localStorage)

    Note over FE,BE: Toda requisição subsequente inclui o JWT no header

    FE->>BE: GET /users/me (Authorization: Bearer <JWT>)
    BE->>SB: Valida JWT via Supabase Admin API
    SB-->>BE: Payload do usuário (user_id, email)
    BE->>SB: SELECT * FROM users WHERE id = user_id
    SB-->>BE: Perfil do usuário (nível, plano, contador diário)
    BE-->>FE: UserProfile { id, name, level, plan, daily_interactions_used }
    FE->>U: Redireciona para tela principal

Fluxo de erro — token expirado:

sequenceDiagram
    actor U as Usuário
    participant FE as Frontend (Next.js)
    participant SB as Supabase Auth
    participant BE as Backend (FastAPI)

    FE->>BE: POST /chat/message (JWT expirado)
    BE-->>FE: 401 Unauthorized
    FE->>SB: supabase.auth.refreshSession()
    SB-->>FE: Novo JWT
    FE->>BE: POST /chat/message (novo JWT)
    BE-->>FE: 200 OK

1.2 — Conversa por Texto

A resposta da Claude é transmitida em streaming via SSE (Server-Sent Events) para o frontend exibir o texto conforme vai sendo gerado, sem esperar a resposta completa.

sequenceDiagram
    actor U as Usuário
    participant FE as Frontend (Next.js)
    participant BE as Backend (FastAPI)
    participant SB as Supabase
    participant AI as Claude (Anthropic)

    U->>FE: Digita mensagem e envia
    FE->>BE: POST /chat/message { session_id, content }

    BE->>BE: Valida JWT
    BE->>SB: Verifica daily_interactions_used do usuário
    SB-->>BE: { used: 7, plan: "free" }

    alt Limite atingido (free >= 10)
        BE-->>FE: 429 { error: "daily_limit_reached", reset_at: "..." }
        FE->>U: Exibe modal de upgrade
    else Dentro do limite
        BE->>SB: Busca histórico da sessão (últimas 10 mensagens)
        SB-->>BE: messages[]

        BE->>SB: INSERT messages (role: "user", content)
        BE->>SB: INCREMENT daily_interactions_used

        BE->>AI: POST /messages (stream: true)<br/>system_prompt + histórico + mensagem do usuário
        Note over BE,AI: Prompt inclui nível do usuário,<br/>persona do cenário e instrução de correção

        AI-->>BE: Stream de tokens

        loop Para cada chunk do stream
            BE-->>FE: SSE event: { type: "token", content: "..." }
            FE->>U: Exibe texto progressivamente
        end

        BE->>BE: Extrai correções do response completo
        BE->>SB: INSERT messages (role: "assistant", content)
        BE->>SB: INSERT corrections[] (se houver erros)
        BE->>SB: UPDATE sessions SET total_messages = total_messages + 1

        BE-->>FE: SSE event: { type: "done", corrections: [...] }
        FE->>U: Exibe correções abaixo da mensagem do usuário
    end

1.3 — Conversa por Voz

O fluxo de voz é o mais complexo — envolve 4 serviços externos em sequência. O usuário vê a transcrição antes de confirmar o envio, permitindo correção de erros de transcrição.

sequenceDiagram
    actor U as Usuário
    participant FE as Frontend (Next.js)
    participant BE as Backend (FastAPI)
    participant AZ as Azure Speech
    participant AI as Claude (Anthropic)
    participant OAI as OpenAI TTS
    participant SB as Supabase Storage

    U->>FE: Pressiona botão e fala em inglês
    FE->>FE: Captura áudio via Web Audio API
    U->>FE: Solta o botão (fim da gravação)

    FE->>BE: POST /speech/transcribe (áudio multipart/form-data)
    BE->>AZ: Envia áudio para STT + Pronunciation Assessment
    AZ-->>BE: { transcription, words: [{ word, accuracy_score, phoneme_feedback }] }

    BE-->>FE: { transcription, pronunciation_words[] }
    FE->>U: Exibe transcrição + palavras com score baixo destacadas

    alt Usuário edita a transcrição
        U->>FE: Corrige texto manualmente
    end

    U->>FE: Confirma envio
    FE->>BE: POST /chat/message { session_id, content: transcription, is_voice: true }

    BE->>SB: Verifica limite diário
    BE->>SB: Busca histórico da sessão
    BE->>SB: INSERT messages (role: "user", content, audio_url)
    BE->>SB: INSERT pronunciation_words[]

    BE->>AI: POST /messages (stream: true)
    AI-->>BE: Resposta em texto

    BE->>OAI: POST /audio/speech { input: response_text, voice: "alloy" }
    OAI-->>BE: Áudio MP3

    BE->>SB: Upload do áudio MP3 → Storage
    SB-->>BE: audio_url (signed URL)

    BE->>SB: INSERT messages (role: "assistant", content, audio_url)
    BE->>SB: INSERT corrections[] (se houver)

    BE-->>FE: { content, audio_url, corrections[], pronunciation_words[] }

    FE->>U: Reproduz áudio da IA automaticamente
    FE->>U: Exibe texto da resposta
    FE->>U: Exibe correções de pronúncia + gramaticais

1.4 — Upgrade para Plano Pro (Stripe)

O pagamento ocorre na página hospedada pelo Stripe (Checkout). O Backend é notificado via webhook — nunca pelo frontend diretamente.

sequenceDiagram
    actor U as Usuário
    participant FE as Frontend (Next.js)
    participant BE as Backend (FastAPI)
    participant ST as Stripe
    participant SB as Supabase

    U->>FE: Clica em "Upgrade para Pro"
    FE->>BE: POST /billing/checkout { plan_type: "monthly" }
    BE->>ST: stripe.checkout.sessions.create({ mode: "subscription", ... })
    ST-->>BE: { checkout_url }
    BE-->>FE: { checkout_url }
    FE->>U: Redireciona para página do Stripe

    U->>ST: Preenche dados do cartão e confirma
    ST->>ST: Processa pagamento

    alt Pagamento aprovado
        ST->>BE: POST /billing/webhook (event: checkout.session.completed)
        BE->>BE: Valida assinatura do webhook (stripe-signature header)
        BE->>SB: UPDATE users SET plan = "pro" WHERE id = user_id
        BE->>SB: INSERT subscriptions { stripe_customer_id, stripe_subscription_id, status: "active", ... }
        BE-->>ST: 200 OK

        ST->>FE: Redireciona para /success?session_id=...
        FE->>BE: GET /users/me
        BE-->>FE: UserProfile { plan: "pro" }
        FE->>U: Exibe tela de boas-vindas Pro

    else Pagamento recusado
        ST->>FE: Redireciona para /cancel
        FE->>U: Exibe mensagem de falha no pagamento
        Note over SB: Plano do usuário não é alterado
    end

1.5 — Encerramento de Sessão

sequenceDiagram
    actor U as Usuário
    participant FE as Frontend (Next.js)
    participant BE as Backend (FastAPI)
    participant SB as Supabase

    U->>FE: Clica em "Encerrar sessão"
    FE->>BE: PATCH /sessions/{session_id}/end

    BE->>SB: Busca todas as messages da sessão
    BE->>SB: Busca todas as corrections da sessão
    BE->>BE: Calcula error_rate = mensagens_com_erro / total_mensagens * 100

    BE->>SB: UPDATE sessions SET ended_at, error_rate, total_messages

    BE->>BE: Verifica critério de avanço de nível<br/>(5 sessões consecutivas com error_rate < 20%)

    alt Critério de avanço atingido
        BE-->>FE: { summary, level_up_suggestion: { from: "B1", to: "B2" } }
        FE->>U: Exibe resumo + sugestão de avanço de nível
        U->>FE: Aceita ou recusa o avanço
        FE->>BE: PATCH /users/me/level { level: "B2", reason: "auto" }
        BE->>SB: UPDATE users SET level = "B2"
        BE->>SB: INSERT user_levels { from: "B1", to: "B2", reason: "auto" }
    else Critério não atingido
        BE-->>FE: { summary }
        FE->>U: Exibe resumo da sessão (erros frequentes, total de mensagens)
    end

2. Contratos de API

Base URL: https://api.fluentloop.com.br/v1

Todas as rotas (exceto /billing/webhook) exigem header:

Authorization: Bearer <supabase_jwt>


Usuários

GET /users/me

Retorna o perfil do usuário autenticado.

Response 200:

{
  "id": "uuid",
  "email": "user@email.com",
  "name": "Lucas Mendes",
  "avatar_url": "https://...",
  "level": "B1",
  "plan": "free",
  "daily_interactions_used": 7,
  "daily_reset_at": "2026-03-21T03:00:00Z"
}

PATCH /users/me

Atualiza nome ou avatar do usuário.

Request:

{ "name": "Lucas M.", "avatar_url": "https://..." }

PATCH /users/me/level

Atualiza o nível do usuário (manual ou confirmação de avanço automático).

Request:

{ "level": "B2", "reason": "manual" }


Sessões

POST /sessions

Inicia uma nova sessão.

Request:

{
  "type": "roleplay",
  "pillar": "speaking",
  "scenario_id": "uuid"
}

Response 201:

{
  "id": "uuid",
  "type": "roleplay",
  "pillar": "speaking",
  "scenario": { "id": "uuid", "name": "Check-in em hotel", "ai_role": "Hotel receptionist" },
  "started_at": "2026-03-21T20:00:00Z"
}

GET /sessions

Lista sessões do usuário (paginado).

Query params: page, limit, type

Response 200:

{
  "data": [
    {
      "id": "uuid",
      "type": "roleplay",
      "pillar": "speaking",
      "scenario_name": "Check-in em hotel",
      "started_at": "...",
      "ended_at": "...",
      "total_messages": 12,
      "error_rate": 16.7
    }
  ],
  "total": 24,
  "page": 1
}

GET /sessions/{id}

Retorna detalhes completos de uma sessão (transcript + correções).

Response 200:

{
  "id": "uuid",
  "messages": [
    {
      "id": "uuid",
      "role": "user",
      "content": "I want to check in, please.",
      "audio_url": null,
      "corrections": [
        {
          "original_text": "I want to check in",
          "corrected_text": "I'd like to check in",
          "error_type": "vocabulary",
          "explanation": "'I'd like' is more polite and natural in formal contexts."
        }
      ],
      "created_at": "..."
    }
  ],
  "summary": {
    "total_messages": 12,
    "error_rate": 16.7,
    "most_common_error_type": "grammar"
  }
}

PATCH /sessions/{id}/end

Encerra uma sessão e calcula o resumo.

Response 200:

{
  "summary": {
    "total_messages": 12,
    "error_rate": 16.7,
    "duration_seconds": 480
  },
  "level_up_suggestion": {
    "from": "B1",
    "to": "B2"
  }
}


Chat

POST /chat/message

Envia mensagem e recebe resposta da IA em streaming (SSE).

Request:

{
  "session_id": "uuid",
  "content": "I want to check in, please.",
  "is_voice": false
}

Response: text/event-stream

data: {"type": "token", "content": "Sure"}
data: {"type": "token", "content": "! Let"}
data: {"type": "token", "content": " me help you."}
data: {"type": "done", "corrections": [...], "message_id": "uuid"}


Voz

POST /speech/transcribe

Transcreve áudio e retorna avaliação de pronúncia.

Request: multipart/form-data - audio: arquivo de áudio (WAV ou WebM) - session_id: uuid

Response 200:

{
  "transcription": "I want to check in please",
  "pronunciation_words": [
    { "word": "I", "position": 0, "accuracy_score": 98, "phoneme_feedback": null },
    { "word": "check", "position": 3, "accuracy_score": 62, "phoneme_feedback": "The 'ch' sound should be /tʃ/, not /ʃ/." }
  ]
}

POST /speech/tts

Gera áudio para um texto (usado internamente após resposta da IA).

Request:

{ "text": "Sure! Let me help you.", "message_id": "uuid" }

Response 200:

{ "audio_url": "https://supabase.storage.../audio/uuid.mp3" }


Cenários

GET /scenarios

Lista cenários disponíveis para o plano do usuário.

Response 200:

{
  "data": [
    {
      "id": "uuid",
      "name": "Check-in em hotel",
      "description": "Pratique um check-in em um hotel internacional.",
      "ai_role": "Hotel receptionist",
      "category": "travel",
      "difficulty": "B1",
      "is_free": true
    }
  ]
}


Pagamentos

POST /billing/checkout

Cria sessão de checkout no Stripe.

Request:

{ "plan_type": "monthly" }

Response 200:

{ "checkout_url": "https://checkout.stripe.com/..." }

POST /billing/webhook

Recebe eventos do Stripe. Não requer JWT — autenticação via stripe-signature header.

Eventos tratados: - checkout.session.completed → ativa plano Pro - customer.subscription.deleted → reverte para Free - invoice.payment_failed → marca past_due

GET /billing/subscription

Retorna dados da assinatura ativa.

Response 200:

{
  "plan_type": "monthly",
  "status": "active",
  "current_period_end": "2026-04-21T00:00:00Z",
  "stripe_customer_portal_url": "https://billing.stripe.com/..."
}


3. Tratamento de Erros

Códigos de status

Código Situação
400 Request inválido (campos faltando, tipo errado)
401 JWT ausente, inválido ou expirado
403 Recurso não permitido para o plano do usuário
404 Recurso não encontrado
429 Limite diário atingido
503 Serviço externo indisponível (Claude, Azure, OpenAI)

Formato padrão de erro

{
  "error": "daily_limit_reached",
  "message": "Você atingiu o limite de 10 interações diárias do plano Free.",
  "details": {
    "limit": 10,
    "used": 10,
    "reset_at": "2026-03-22T03:00:00Z",
    "upgrade_url": "/pricing"
  }
}

Estratégia por serviço externo

Serviço Falha Comportamento
Claude Timeout ou 5xx Retorna 503, frontend exibe "Tente novamente". Não salva a mensagem.
Azure Speech Falha na transcrição Retorna 503, frontend oferece fallback para digitação manual
Azure Speech Baixa confiança na transcrição Retorna transcrição com flag low_confidence: true, frontend avisa o usuário
OpenAI TTS Falha na geração de áudio Retorna resposta em texto normalmente, sem áudio (audio_url: null)
Stripe webhook Falha no processamento Retorna 500, Stripe retenta automaticamente por até 3 dias
Supabase Falha na escrita Retorna 503, nenhuma cobrança de interação é feita

4. Ciclo de Vida da Sessão

stateDiagram-v2
    [*] --> Criada : POST /sessions

    Criada --> Ativa : Primeira mensagem enviada

    Ativa --> Ativa : Usuário envia mensagem\nIA responde

    Ativa --> Encerrada : PATCH /sessions/{id}/end\n(usuário encerra manualmente)

    Ativa --> Abandonada : Sem atividade por 30 minutos\n(job agendado no backend)

    Abandonada --> Encerrada : Job encerra a sessão\nsem calcular resumo completo

    Encerrada --> [*]

Estados:

Estado Descrição ended_at error_rate
Criada Sessão iniciada, aguardando primeira mensagem null null
Ativa Conversa em andamento null null
Encerrada Finalizada pelo usuário com resumo calculado preenchido calculado
Abandonada Encerrada por inatividade, sem resumo completo preenchido null

5. Segurança

Ponto Implementação
Autenticação JWT gerado pelo Supabase, validado no Backend em toda requisição
Webhook Stripe Validado via stripe-signature header — rejeita qualquer request sem assinatura válida
Rate limiting Middleware no FastAPI: 60 req/min por IP para rotas públicas; limite de interações por usuário no banco
Inputs do usuário Sanitização no Backend antes de enviar ao Claude (prevenção de prompt injection)
Áudios no Storage URLs assinadas com expiração de 1 hora — sem acesso público permanente
Chaves de API Variáveis de ambiente no Railway/Vercel — nunca expostas no frontend