Ir para o conteúdo
← Projetos Projeto Acadêmico

FakeStore BaaS

Backend-as-a-Service multi-tenant headless para e-commerce com isolamento completo de dados via Row-Level Security nativo do PostgreSQL. Billing Stripe, webhooks com fila BullMQ, frete nacional, upload via Cloudflare R2 e 215 testes automatizados. Projeto acadêmico com arquitetura de produto real.

01

Contexto

O objetivo era construir uma API headless de e-commerce que qualquer lojista pudesse integrar via API key — sem compartilhar banco de dados, sem risco de vazamento de dados entre tenants. O desafio central foi implementar isolamento multi-tenant de forma estrutural, não por convenção de código: uma query sem filtro WHERE não deveria retornar dados de outro tenant nem em caso de bug.

02

Papel & Equipe

Papel Desenvolvedor Backend (solo)
Duração Em desenvolvimento contínuo
Equipe Individual
03

Opções consideradas

Isolamento por WHERE clauses no código da aplicação Rejeitado

Simples, mas qualquer bug ou query esquecida vaza dados de outro tenant — a segurança depende de convenção, não de mecanismo

Row-Level Security (RLS) nativo do PostgreSQL Escolhido

SET LOCAL app.current_tenant_id por transação — o próprio banco recusa qualquer acesso fora do tenant. Zero vazamento mesmo com bugs no código Node.js

Prisma ORM Rejeitado

Ergonomia de queries, mas a abstração impede o controle fino de SET LOCAL necessário para ativar o contexto RLS por transação

Knex.js query builder Escolhido

Queries explícitas, suporte a raw SQL para SET LOCAL, controle granular de transações — encaixa perfeitamente no modelo RLS

Entrega de webhooks inline no request Rejeitado

Simples, mas um endpoint lento do cliente bloqueia o processo e não tem retry — um webhook com falha silenciosa deixa o cliente sem notificação

Worker BullMQ em processo isolado Escolhido

Worker separado, retry exponencial (5x), HMAC-SHA256 nas assinaturas, audit trail de entregas — comportamento de produto real

04

Solução

Arquitetura

Express 4 + TypeScript multi-tenant BaaS. Cada request resolve o tenant via SHA-256 do x-api-key (cache Redis 5min). Transações RLS-scoped com SET LOCAL — banco isola os dados estruturalmente. Worker BullMQ separado para entrega assíncrona de webhooks.

Destaques técnicos

  • RLS via SET LOCAL app.current_tenant_id — zero vazamento de dados entre tenants mesmo com bug no código
  • 3 camadas de autenticação: API key (SHA-256 hash), JWT RS256 (usuário), API Secret (bcrypt S2S)
  • Webhook engine isolado em container Docker próprio — BullMQ + retry exponencial + assinaturas HMAC-SHA256
  • Valores monetários em centavos (Integer) — elimina bugs de ponto flutuante por design
  • SDK TypeScript gerado automaticamente do OpenAPI spec (openapi-generator-cli)
  • 215 testes em 28 suites: 13 de integração (incluindo isolamento RLS) + 15 unitários

API / Backend: Express 4 · PostgreSQL 16 + RLS · Redis 7 + BullMQ · Stripe · Cloudflare R2 · Melhor Envio

05

Resultados

Métrica Valor
Testes automatizados 215 testes / 28 suites
Suites de integração 13 (inclui RLS isolation)
Suites unitárias 15 (middlewares, services, utils)
CI/CD GitHub Actions (push + PR para main)
Isolamento multi-tenant RLS nativo — banco recusa cruzamento de dados
06

Aprendizados

O que é e o que não é

FakeStore BaaS não tem frontend. É uma API headless — o lojista integra via API key e constrói qualquer interface em cima. O “Fake” do nome não é depreciativo: é uma referência à FakeStore API pública usada em tutoriais, que o projeto substitui com uma versão real, multi-tenant e pronta para produção.

Por que RLS e não WHERE clauses

A decisão mais importante do projeto foi onde implementar o isolamento de dados. A abordagem convencional é colocar WHERE tenant_id = $1 em todas as queries da aplicação. Funciona — até que alguém esqueça, ou até que um JOIN pule o filtro, ou até que uma query de debug rode em produção sem o contexto.

Com RLS, o PostgreSQL recusa qualquer acesso a linhas fora do tenant ativo na transação — independente do que o código Node.js faça. O mecanismo é:

SET LOCAL app.current_tenant_id = '42';
-- A partir daqui, qualquer SELECT/INSERT/UPDATE/DELETE
-- filtra automaticamente tenant_id = 42
-- Rollback ou fim da transação limpa o contexto

O teste de RLS na suite de integração verifica isso explicitamente: cria dois tenants com dados separados e confirma que as queries de um não retornam nada do outro, nem com SQL direto.

Worker de webhooks em processo separado

O webhook engine roda em um container Docker completamente separado da API. Quando o pedido é confirmado, a API empurra um job para a fila BullMQ. O worker consome assincronamente, assina o payload com HMAC-SHA256 e faz entrega com retry exponencial (até 5 tentativas, base 60s). Toda entrega — sucesso ou falha — fica registrada em webhook_events.

Isso significa que um endpoint de cliente que demora 10 segundos para responder não bloqueia nenhuma request da API. E o cliente consegue auditar exatamente quando cada webhook foi entregue, com qual resposta HTTP.

Dinheiro como Integer

Todos os valores monetários no banco são centavos (Integer). R$ 49,90 vira 4990. Descontos percentuais são basis points: 15% = 1500. A divisão para reais acontece só na resposta JSON. Isso elimina 0.1 + 0.2 = 0.30000000000000004 estruturalmente — nenhum desenvolvedor precisa lembrar de usar toFixed(2).

Código-fonte ↗