Cómo construir un SaaS multi-tenant en 14 días con Supabase y FastAPI

18 agosto 2026 · Euskolabs

Construir un SaaS escalable no debería tomar meses. En euskolabs entregamos MVPs SaaS multi-tenant en menos de 14 días usando un stack optimizado: Supabase para base de datos y auth, FastAPI para el backend, Stripe para pagos y Docker para despliegue. Esta es la guía técnica de cómo lo hacemos.

Día 1: Arquitectura y diseño de base de datos

La clave del multi-tenant está en el diseño de la base de datos. Usamos Row Level Security (RLS) de PostgreSQL via Supabase para aislar los datos de cada tenant sin necesidad de filtros manuales en cada query.

Esquema de tablas

-- Tabla de tenants (organizaciones/clientes)
CREATE TABLE tenants (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  plan TEXT DEFAULT 'free',
  stripe_customer_id TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Tabla de usuarios vinculada a tenants
CREATE TABLE users (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
  email TEXT UNIQUE NOT NULL,
  role TEXT DEFAULT 'member',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Tabla de datos del SaaS (ejemplo: proyectos)
CREATE TABLE projects (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  status TEXT DEFAULT 'active',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Row Level Security: cada usuario solo ve datos de su tenant
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY "tenant_isolation" ON projects
  FOR ALL USING (tenant_id = (
    SELECT tenant_id FROM users 
    WHERE id = auth.uid()
  ));

Con RLS activado, cualquier query que ejecute Supabase automáticamente filtra por el tenant del usuario autenticado. No hay forma de que un tenant acceda a datos de otro, ni siquiera por error.

Días 2-5: Backend con FastAPI

FastAPI genera automáticamente documentación OpenAPI, valida tipos con Pydantic y soporta async nativo. La estructura del backend:

app/
├── main.py          # Entry point + routers
├── deps.py          # Dependencias (auth, db)
├── models.py        # Modelos Pydantic
├── routers/
│   ├── tenants.py   # CRUD tenants
│   ├── projects.py  # CRUD proyectos
│   ├── auth.py      # Login/registro via Supabase
│   └── billing.py   # Webhooks Stripe
└── supabase.py      # Cliente Supabase

Middleware de autenticación

from fastapi import Depends, HTTPException
from supabase import create_client

async def get_current_user(token: str = Depends(oauth2_scheme)):
    supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
    user = supabase.auth.get_user(token)
    if not user:
        raise HTTPException(401, "No autorizado")
    return user

Días 6-9: Frontend y Stripe

Para el frontend usamos Next.js con SSR para SEO y splits de código automáticos. La integración con Stripe incluye:

@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
    event = stripe.Webhook.construct_event(
        payload, sig, ENDPOINT_SECRET
    )
    if event["type"] == "invoice.paid":
        tenant_id = event["data"]["object"]["metadata"]["tenant_id"]
        activate_tenant(tenant_id)
    elif event["type"] == "invoice.payment_failed":
        tenant_id = event["data"]["object"]["metadata"]["tenant_id"]
        suspend_tenant(tenant_id)

Días 10-14: Testing, Docker y despliegue

Dockerfile

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml

services:
  api:
    build: .
    ports: ["8000:8000"]
    env_file: .env
    restart: always
  caddy:
    image: caddy:2
    ports: ["443:443", "80:80"]
    volumes: ["./Caddyfile:/etc/caddy/Caddyfile"]

Caddy gestiona SSL automáticamente con Let's Encrypt. El SaaS queda live en producción con HTTPS.

Conclusiones

Con este stack entregamos SaaS multi-tenant funcionales en 14 días:

¿Quieres lanzar tu SaaS? Solicita un diagnóstico en euskolabs.com.

← Volver al blog