Skills
Arquitetura de projeto

Project Multitenant

Archetype de projeto para SaaS multi-tenant — React + Django + PostgreSQL + JWT via cookie httpOnly. Invoque no início de um projeto novo desse tipo, ou quando o dev perguntar "que stack usamos pra isso" e o projeto for um SaaS com múltiplos clientes/organizações.

Projeto: Multi-tenant SaaS

Combinação de stack para produtos SaaS com múltiplos tenants (organizações/clientes isolados). Este skill não redefine convenções de código — elas moram nos skills atômicos frontend/* e backend/* linkados abaixo. Aqui vive só a decisão de quais peças combinar e por quê.

Peças da stack

Camada Escolha Convenções em
Componentes React 18 + Vite + TypeScript frontend/react, frontend/typescript, frontend/vite
Estilo SCSS Modules + BEM frontend/scss-bem
Formulários React Hook Form + zod frontend/react-hook-form-zod
Tabelas TanStack Table frontend/tanstack-table
Testes (frontend) Vitest + Testing Library frontend/vitest-testing-library
Backend Django 5 + DRF + PostgreSQL backend/django-drf
Autenticação JWT via cookie httpOnly backend/jwt-cookie-auth
Frontend ↔ cookie auth fetch/axios com credentials + header CSRF reference/frontend-auth.md
Lint/format/hooks (frontend) ESLint + Prettier + Husky + lint-staged + commitlint eslint-prettier-husky
Lint/format/hooks (backend) Ruff + pre-commit framework ruff-precommit

Por que esta combinação

  • Django + PostgreSQL: modelagem relacional rica (tenants, planos, permissões por org) é o forte do Django ORM; produtividade alta pra CRUD administrativo que todo SaaS tem.
  • Cookie httpOnly em vez de header Bearer: o frontend é sempre a própria SPA servida pelo mesmo produto — não há cliente third-party consumindo a API. Cookie httpOnly fecha a superfície de XSS que rouba token de localStorage, que é o vetor mais comum contra SPAs autenticadas.
  • TanStack Table em vez de montar tabela na mão: SaaS multi-tenant quase sempre tem telas de listagem densa (usuários da org, faturas, logs) com sort/filtro/paginação — reimplementar isso por tela é o tipo de esforço que não deveria variar de projeto pra projeto.
  • SCSS Modules + BEM em vez de Tailwind: SaaS multi-tenant tende a acumular telas de admin complexas e às vezes theming por tenant — um arquivo de estilo isolado por componente escala melhor pra isso do que classes utilitárias espalhadas. Se o projeto for majoritariamente telas simples, reavalie contra project-spa.

Regra inegociável específica deste archetype

  • Todo model, queryset e endpoint que retorna dado de tenant filtra por tenant/organização — não existe endpoint "global" que um usuário de um tenant possa usar pra enxergar dado de outro. Isso é além do filtro por usuário já exigido em backend/django-drf.

CI é obrigatório, não opcional

Todo projeto deste archetype tem .github/workflows/ci.yml (separado do harness-gate.yml herdado do template) rodando, em todo PR: lint + format check + typecheck do frontend (eslint-prettier-husky) e lint + format check do backend (ruff-precommit), além dos testes de cada lado. Hook local (Husky/pre-commit) é conveniência do dia a dia; CI é o gate que ninguém consegue pular com --no-verify. Projeto sem esse workflow não tem as regras de frontend/*/backend/* realmente aplicadas — documentação sem enforcement é a primeira coisa que quebra sob prazo apertado.

Skills relacionadas

  • Fluxo de issue, branch e PR: workflow-branching, workflow-prs, workflow-issues
  • Feature list e baseline: ratchet-feature-list

Documentos de referência

Frontend ↔ auth por cookie httpOnly

fonte ↗

Frontend ↔ auth por cookie httpOnly

Referência de project-multitenant. Volte ao índice para o quando-invocar.

O backend (backend/jwt-cookie-auth) seta os tokens em cookies httpOnly; o frontend nunca lê nem guarda o access/refresh token diretamente — só precisa mandar credentials/withCredentials e ecoar o cookie CSRF.

// src/lib/api/client.ts
import axios from 'axios';

function getCsrfCookie(): string {
  return document.cookie
    .split('; ')
    .find((row) => row.startsWith('csrftoken='))
    ?.split('=')[1] ?? '';
}

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true, // manda e recebe cookies (access/refresh/csrf)
});

api.interceptors.request.use((config) => {
  if (['post', 'put', 'patch', 'delete'].includes(config.method ?? '')) {
    config.headers['X-CSRFToken'] = getCsrfCookie();
  }
  return config;
});
  • Login/logout só fazem POST para /token/ e /logout/ — não guardam nada em localStorage/sessionStorage/estado global além de "estou autenticado" (booleano ou dados do usuário retornados pela própria API).
  • Expiração de access token: um 401 dispara POST /token/refresh/ (o refresh também vem de cookie) e repete a request original; se o refresh falhar, redireciona pro login.
  • VITE_API_URL aponta pro domínio da API — se front e API estão em domínios diferentes, o backend precisa de SameSite='None' + Secure=True (ver backend/jwt-cookie-auth).