iasaaspythonnextjsstriperag

DocChat — Como construímos um SaaS de RAG com IA local e Stripe

7 minutes 14/07/2026

Tempo estimado de leitura: 7 minutos


No começo de 2026, a gente tinha um problema concreto: clientes chegavam com PDFs — contratos, relatórios, atas — e pediam “me responde o que está aqui”. Dava pra abrir um por um, mas escalar isso pra dezenas de documentos simultâneos não era viável manualmente.

A resposta natural era RAG (Retrieval-Augmented Generation). Mas a pergunta seguinte era: dá pra fazer isso rodando 100% local, sem depender de API externa, e ainda cobrar por meio de um checkout Stripe?

Nasceu o DocChat — um SaaS de chat com documentos onde você faz upload de um PDF, faz perguntas em linguagem natural, e o modelo responde com base no conteúdo do documento. Tudo rodando localmente num Mac Studio M2 Ultra.

A stack

ComponenteTecnologia
BackendFastAPI (Python 3.11)
FrontendNext.js 16 (App Router)
LLMQwen 3 Coder 32B (Ollama, local)
Embeddingsnomic-embed-text (Ollama)
Vector DBChromaDB (persistente)
AuthJWT (HS256) + bcrypt
PagamentosStripe Checkout
ArmazenamentoSQLite (após migração de JSON)

A escolha do Qwen 32B não foi aleatória. Testamos Mistral, Llama 3 e DeepSeek antes. O Qwen 3 Coder se destacou por três razões: excelente compreensão de português (surpreendentemente melhor que modelos do mesmo porte), capacidade de seguir instruções de contexto longo sem alucinar, e rodagem confortável em 64 GB unificados do M2 Ultra com 4-bit quantization via Ollama.

Como funciona o RAG local

O fluxo é direto:

  1. Upload do PDF → Extração de texto com PyPDF, chunking com langchain-text-splitters (tamanho 1000 caracteres, overlap 200)
  2. Embeddings → Cada chunk vira um vetor 768-d via nomic-embed-text rodando local
  3. Armazenamento → ChromaDB persiste os vetores em disco (chroma_db/)
  4. Query → Quando o usuário pergunta algo, a pergunta é embedada e o ChromaDB faz busca por similaridade (top-5 chunks mais relevantes)
  5. Geração → Os chunks viram contexto no prompt do Qwen, que responde apenas com base no conteúdo recuperado
# A espinha dorsal do chat (versão simplificada)
@app.post("/chat")
async def chat(req: ChatRequest, user_id: str = Depends(get_current_user)):
    # 1. Embed a pergunta
    embedding = ollama.embeddings(model="nomic-embed-text", prompt=req.question)
    
    # 2. Busca similaridade no ChromaDB
    results = collection.query(
        query_embeddings=[embedding["embedding"]],
        n_results=5,
        where={"doc_id": req.doc_id}
    )
    
    # 3. Monta contexto + pergunta
    context = "\n\n".join(results["documents"][0])
    system_prompt = f"Responda com base no contexto abaixo.\n\nCONTEXTO:\n{context}"
    
    # 4. LLM responde
    response = ollama.chat(
        model="qwen32b",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": req.question},
        ],
    )
    return {"answer": response["message"]["content"]}

O latency médio do M2 Ultra: ~4 segundos por pergunta para chunks de até 5000 tokens de contexto. Aceitável para uma ferramenta de análise, não para chat em tempo real.

Stripe integrado com checkout funcional

A monetização veio com Stripe Checkout — dois planos:

  • Pro (R$29/mês): Upload de 50 documentos, 500 perguntas/mês
  • Enterprise (R$99/mês): Upload ilimitado, perguntas ilimitadas, workspaces multiusuário

O fluxo é padrão Stripe: frontend chama /create-checkout no backend, que devolve uma URL de sessão. O usuário é redirecionado, paga, e o webhook Stripe atualiza o plano no banco. O detalhe que deu trabalho foi gerenciar o estado entre o webhook e a resposta síncrona — Stripe pode levar alguns segundos para disparar o webhook, então o frontend faz polling no endpoint /auth/me até ver o plano atualizado.

@app.post("/create-checkout")
@limiter.limit("3/minute")
def create_checkout(req: CheckoutRequest, user_id: str = Depends(get_current_user)):
    try:
        session = stripe.checkout.Session.create(
            customer_email=user_id,
            payment_method_types=["card"],
            line_items=[{"price": price_id, "quantity": 1}],
            mode="subscription",
            success_url="http://localhost:3004/success",
            cancel_url="http://localhost:3004/pricing",
            metadata={"user_id": user_id},
        )
        return {"url": session.url}
    except stripe.error.StripeError as e:
        logger.error(f"Stripe error: {e}")
        raise HTTPException(400, "Erro ao processar pagamento. Tente novamente.")

Testamos com cartão 4242 4242 4242 4242 — checkout funcional, webhook recebendo eventos, upgrade de plano automático.

Os desafios

1. RAG local head-to-head com GPU cloud

O principal desafio técnico foi performance. Qwen 32B em CPU (M2 Ultra é unificado, mas ainda assim não é GPU dedicada) dá conta, mas o gargalo real é o embedding + busca vetorial para cada pergunta. A solução foi cachear embeddings de perguntas repetidas e pré-calcular embeddings de documentos no momento do upload, não no momento da pergunta.

2. Segurança — 11 vulnerabilidades corrigidas

Fizemos uma auditoria de segurança completa e encontramos 11 vulnerabilidades. As mais críticas:

  • JWT Secret regenerado a cada restart — Sem arquivo .jwt_secret persistente, todo restart invalidava todos os tokens. Todos os usuários logged out de uma vez.
  • CORS com wildcard * + credentials — Combinação inválida que browsers rejeitam, mas proxies e clients não-browser aceitam. Qualquer site malicioso podia fazer requisições autenticadas.
  • Sem rate limiting em auth — Ataque de brute-force a 1000 requests/minuto, sem bloqueio.
  • bcrypt com apenas 8 rounds — OWASP recomenda 12+. 8 rounds em hardware moderno quebra senhas simples em minutos.
  • Upload sem limite de tamanho — Um PDF de 100 GB encheria o disco sem impedimento.
  • Token JWT em localStorage — JavaScript de qualquer script third-party na página podia ler o token.

Todas foram corrigidas antes do primeiro deploy. A auditoria completa está no repositório e virou checklist para projetos seguintes.

3. Migração de JSON file-based para SQLite

O projeto original armazenava cada usuário em um arquivo JSON separado (users/{email}.json). Funcionava para 19 usuários, mas não escalava:

  • Sem concorrência — duas requests simultâneas corrompiam o arquivo
  • Sem atomicidade — crash no meio da escrita quebrava o dado
  • Sem transações — sem rollback, sem backup consistente

Migramos para SQLite com fallback automático para JSON em caso de falha, e uma função de migração que leu os 19 arquivos existentes e inseriu no banco.

Resultados

  • Checkout Stripe funcional — Upgrade de plano, downgrade, cancelamento, tudo testado
  • 23 usuários no banco (19 da fase JSON + 4 novos durante testes)
  • RAG local rodando — Qwen 32B responde com base em documentos de até 100 páginas
  • 11 VULNs corrigidas — auditoria completa, risco residual baixo
  • Multi-workspace — usuários Enterprise podem criar workspaces com times e coleções de documentos compartilhadas

O que aprendemos

Rodar um modelo 32B localmente é viável para um SaaS B2B de nicho, mas não escala para milhares de usuários simultâneos sem uma GPU dedicada. Para o volume do DocChat (dezenas de usuários, perguntas esporádicas), o M2 Ultra segura bem — CPU fica em ~40% durante inferência, RAM em ~35 GB dos 64 GB disponíveis.

A lição maior foi sobre segurança em SaaS feito às pressas. O DocChat começou como um protótipo funcional em dois dias. Funcionava, mas as 11 vulnerabilidades mostravam que funcionalidade não é o mesmo que prontidão para produção. A auditoria de segurança foi o momento de maior retorno sobre investimento de tempo em todo o projeto.

Experimente

O DocChat está rodando em http://localhost:3004 no ambiente de desenvolvimento. Faça upload de um PDF, faça perguntas, e veja o RAG local em ação. O checkout Stripe está funcional com cartão de teste 4242 4242 4242 4242.


Stack completa: FastAPI + Qwen 3 Coder 32B (Ollama) + ChromaDB + Next.js 16 + Stripe + SQLite. Código disponível em ~/work/haruo/web/docchat/.


Sobre a Haruo: Haruo é uma software house full-stack especializada em produtos web, APIs e automação com IA. haruo.dev


Quer construir um SaaS de IA com RAG local? Fale com a Haruo →

Vamos construir algo incrível juntos?

Na Haruo, transformamos ideias em produtos digitais que geram resultados reais. Conte com nossa equipe de engenharia e IA.

Falar com a Haruo →