Por qué Supabase RLS para Multi-Tenancy

Seamos realistas: cuando se trata de manejar multi-tenancy en aplicaciones SaaS, tienes opciones. Puedes crear una base de datos separada por inquilino, lo que suena como la nirvana organizativa pero es caro y francamente doloroso de gestionar. O puedes intentar usar esquemas separados, lo cual es menos complicado operacionalmente pero aún no es un paseo por el parque cuando se trata de migraciones. Pero luego está la joya del mundo SaaS—tablas compartidas con filtrado a nivel de fila. Supabase facilita este enfoque gracias a su Row Level Security (RLS) nativa de PostgreSQL.

¿Por qué importa? Simple. Tu filtrado de datos ocurre a nivel de base de datos. Si te equivocas en una cláusula WHERE en tu ruta de API de Next.js, no pasarás la noche pensando en violaciones de datos, porque la base de datos en sí es tu red de seguridad. Y realmente, en estos tiempos, eso no es un lujo—es una necesidad.

Pero no nos engañemos. RLS añade sobrecarga a tus consultas, complica la depuración y puede causarte problemas durante migraciones. Entonces, ¿cómo se comparan diferentes enfoques de multi-tenancy?

Enfoque Nivel de Aislamiento Costo Complejidad Operativa Rendimiento de Consultas
Base de datos por inquilino Completo Alto ($50-200/inquilino/mes) Muy Alto Mejor
Esquema por inquilino Fuerte Medio Alto (migraciones) Bueno
Tablas compartidas + RLS A nivel de fila Bajo Medio Bueno (con salvedades)
Filtrado a nivel de aplicación Ninguno Más bajo Bajo Mejor

Para la mayoría de productos SaaS con menos de 10,000 inquilinos, las tablas compartidas con RLS te dan el mejor valor por tu dinero. Es en lo que nos sumergimos aquí.

Multi-Tenant Next.js con Supabase RLS: Guía de Producción

Patrones de Arquitectura: Compartido vs Aislado

Antes siquiera de pensar en escribir código, debes elegir tu estrategia de resolución de inquilino. En la realidad, te encontrarás principalmente con dos enfoques:

Tenancy Basado en Subdominio

¿Alguna vez viste tenant-slug.tuapp.com? Bienvenido al patrón más común para SaaS B2B. Es elegante, profesional, y hace que la resolución de inquilino en middleware sea pan comido.

Tenancy Basado en Ruta

Este es tu básico /org/tenant-slug/dashboard. Más fácil de configurar ya que no hay DNS comodín, y funciona en plataformas como Vercel sin dominios personalizados. Pero seamos honestos: se siente un poco como usar calcetines con sandalias. Generalmente recomendamos basado en subdominio para aplicaciones B2B de producción y basado en ruta para herramientas internas o MVPs. ¿Cambiar después? Maldecirás tu yo pasado—cambiar estos patrones no es broma.

Configuración del Esquema de Inquilino

Aquí hay un patrón de esquema que no nos ha defraudado en tres lanzamientos de producción diferentes:

-- Tabla de inquilino central
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  plan TEXT NOT NULL DEFAULT 'free',
  created_at TIMESTAMPTZ DEFAULT now(),
  settings JSONB DEFAULT '{}'
);

-- Tabla de unión de membresía
CREATE TABLE memberships (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
  role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
  created_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(user_id, org_id)
);

-- Ejemplo de tabla con alcance de inquilino
CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID REFERENCES organizations(id) ON DELETE CASCADE NOT NULL,
  name TEXT NOT NULL,
  description TEXT,
  created_by UUID REFERENCES auth.users(id),
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Índice en org_id — necesitarás esto en CADA tabla con alcance de inquilino
CREATE INDEX idx_projects_org_id ON projects(org_id);
CREATE INDEX idx_memberships_user_id ON memberships(user_id);
CREATE INDEX idx_memberships_org_id ON memberships(org_id);

La tabla memberships es el pegamento que mantiene todo junto. Todas tus políticas RLS apuntarán a ella como si fuera su primo favorito. Los usuarios pueden unirse a múltiples organizaciones, y sus roles dictan qué pueden y qué no pueden hacer. Y aquí hay una pequeña joya de sabiduría: siempre—seriamente, siempre—indexa org_id en cada tabla con alcance de inquilino. De lo contrario, observa cómo tus consultas avanzan como melaza una vez que estés nadando en datos. Nos sorprendieron cuando el panel de un cliente se desplomó de 50ms a 8 segundos con 100,000 filas. Lección aprendida.

Políticas RLS que Realmente Escalan

Aquí es donde los tutoriales típicamente se retiran, dejándote varado. Te lanzan auth.uid() = user_id y dicen, "¡Buena suerte!" Pero el RLS multi-inquilino no puede simplificarse así.

El Patrón de Función Auxiliar

¿Por qué desordenar cada política con comprobaciones de membresía? Usa una función auxiliar en su lugar:

-- Auxiliar: verificar si el usuario actual es miembro de una organización
CREATE OR REPLACE FUNCTION public.is_member_of(org UUID)
RETURNS BOOLEAN AS $$
  SELECT EXISTS (
    SELECT 1 FROM memberships
    WHERE user_id = auth.uid()
    AND org_id = org
  );
$$ LANGUAGE sql SECURITY DEFINER STABLE;

-- Auxiliar: obtener el rol del usuario en una organización
CREATE OR REPLACE FUNCTION public.get_role_in(org UUID)
RETURNS TEXT AS $$
  SELECT role FROM memberships
    WHERE user_id = auth.uid()
    AND org_id = org
  LIMIT 1;
$$ LANGUAGE sql SECURITY DEFINER STABLE;

¿Por qué SECURITY DEFINER? Porque la función se ejecuta con los privilegios del creador, omitiendo RLS en la tabla memberships. Sin esto, corres el riesgo de caer en una madriguera de referencias circulares donde RLS en memberships causa que se bloqueen las comprobaciones de membresía en las que otras tablas dependen.

¿Y la parte STABLE? Indica al planificador de consultas que la salida de la función permanece consistente para la misma entrada durante una sola consulta, permitiendo algunos beneficios de caché agradables. ¿Tentado de usar IMMUTABLE? No lo hagas. La membresía puede cambiar entre transacciones.

Políticas para Tablas con Alcance de Inquilino

Veamos algunas políticas para nuestra tabla projects:

-- Habilitar RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- SELECT: los miembros pueden ver proyectos en sus organizaciones
CREATE POLICY "Members can view org projects"
  ON projects FOR SELECT
  USING (public.is_member_of(org_id));

-- INSERT: administradores y propietarios pueden crear proyectos
CREATE POLICY "Admins can create projects"
  ON projects FOR INSERT
  WITH CHECK (
    public.get_role_in(org_id) IN ('owner', 'admin')
  );

-- UPDATE: administradores y propietarios pueden actualizar proyectos
CREATE POLICY "Admins can update projects"
  ON projects FOR UPDATE
  USING (public.is_member_of(org_id))
  WITH CHECK (
    public.get_role_in(org_id) IN ('owner', 'admin')
  );

-- DELETE: solo los propietarios pueden eliminar proyectos
CREATE POLICY "Owners can delete projects"
  ON projects FOR DELETE
  USING (
    public.get_role_in(org_id) = 'owner'
  );

Políticas para la Tabla de Membresías en Sí

Esta es complicada. La tabla memberships obtiene su propio RLS, pero no puede usar las funciones auxiliares porque ellas, a su vez, consultan memberships—toca las pesadillas de referencias circulares:

ALTER TABLE memberships ENABLE ROW LEVEL SECURITY;

-- Los usuarios pueden ver membresías en organizaciones a las que pertenecen
CREATE POLICY "Users can view org memberships"
  ON memberships FOR SELECT
  USING (
    org_id IN (
      SELECT org_id FROM memberships WHERE user_id = auth.uid()
    )
  );

-- Solo los propietarios pueden añadir miembros
CREATE POLICY "Owners can add members"
  ON memberships FOR INSERT
  WITH CHECK (
    org_id IN (
      SELECT org_id FROM memberships
      WHERE user_id = auth.uid() AND role = 'owner'
    )
  );

Sí, hay una subconsulta en la misma tabla. Y sí, PostgreSQL la maneja perfectamente. La subconsulta verifica tu propia membresía, sin ser afectada por la política que se está definiendo ya que RLS solo envuelve la consulta externa. Pero prueba esto—seriamente, no quieres encontrar un error en producción.

Multi-Tenant Next.js con Supabase RLS: Guía de Producción - arquitectura

Middleware de Next.js para Resolución de Inquilino

Con Next.js 15 y el elegante App Router, el middleware ejecutándose en el edge es el arrendador perfecto para la resolución de inquilino. Aquí está nuestro patrón confiable para configuraciones basadas en subdominio:

// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const PUBLIC_ROUTES = ['/login', '/signup', '/invite'];

export async function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') || '';
  const currentHost = hostname.split('.')[0];

  // Omitir para dominio principal y localhost
  const isMainDomain = currentHost === 'app' || currentHost === 'www' || currentHost === 'localhost:3000';

  let response = NextResponse.next({
    request: { headers: request.headers },
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => {
            request.cookies.set(name, value);
            response.cookies.set(name, value, options);
          });
        },
      },
    }
  );

  const { data: { user } } = await supabase.auth.getUser();

  if (!isMainDomain) {
    response.headers.set('x-tenant-slug', currentHost);

    if (!user && !PUBLIC_ROUTES.some(r => request.nextUrl.pathname.startsWith(r))) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)'],
};

El encabezado x-tenant-slug es oro puro. Úsalo para dejar que tus Componentes de Servidor y rutas de API sepan con qué inquilino están tratando. Si estás colaborando con nosotros en un proyecto Next.js, configurar esto es nuestra prioridad del día uno.

Flujo de Autenticación en Aplicaciones Multi-Inquilino

Supabase Auth juega de manera neutral en el juego de multi-tenancy. Los usuarios existen en una esfera global—las relaciones de inquilino son tu rompecabezas a resolver. Aquí está nuestro plan:

  1. El usuario se registra: Crea un usuario de autenticación, construye una organización, y conjura una membresía con un rol de 'propietario'.
  2. El usuario es invitado: El administrador redacta una invitación pendiente, un nuevo usuario se une a través del enlace de invitación, y poof—aparece una membresía con el rol especificado.
  3. El usuario inicia sesión: Extrae inquilino del subdominio, confirma membresía, escóltalo a su panel.
// app/api/auth/signup/route.ts
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { email, password, orgName, orgSlug } = await request.json();
  const supabase = await createClient();

  // Registrar al usuario
  const { data: authData, error: authError } = await supabase.auth.signUp({
    email,
    password,
  });

  if (authError) return NextResponse.json({ error: authError.message }, { status: 400 });

  // Usar un cliente de rol de servicio para la creación de org (omite RLS)
  const adminClient = createAdminClient();

  const { data: org, error: orgError } = await adminClient
    .from('organizations')
    .insert({ name: orgName, slug: orgSlug })
    .select()
    .single();

  if (orgError) return NextResponse.json({ error: orgError.message }, { status: 400 });

  // Crear membresía de propiedad
  await adminClient
    .from('memberships')
    .insert({
      user_id: authData.user!.id,
      org_id: org.id,
      role: 'owner',
    });

  return NextResponse.json({ org });
}

Observa que confiamos en un cliente de rol de servicio durante el registro. El usuario aún no tiene ninguna membresía, así que RLS los dejaría en apuros para la creación de organizaciones. Es uno de esos problemas clásicos de arranque—tu clave de rol de servicio será tu varita mágica.

Y no puedo enfatizar esto lo suficiente: Nunca, jamás expongas tu clave de rol de servicio al cliente. Es estrictamente para código del lado del servidor.

Componentes de Servidor y RLS: El Problema de SSR

Los Componentes de Servidor de Next.js 15 están vinculados al servidor, elevando el juego de seguridad. Pero hay un obstáculo cuando se usa Supabase RLS: tienes que proporcionar la sesión del usuario al cliente de Supabase para que las políticas RLS sepan quién está en la mesa.

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // Esto puede fallar en Componentes de Servidor (solo lectura)
            // El middleware maneja la actualización de cookies
          }
        },
      },
    }
  );
}
// app/[orgSlug]/projects/page.tsx
import { createClient } from '@/lib/supabase/server';
import { headers } from 'next/headers';

export default async function ProjectsPage() {
  const supabase = await createClient();
  const headersList = await headers();
  const tenantSlug = headersList.get('x-tenant-slug');

  // Obtener el ID de org del slug
  const { data: org } = await supabase
    .from('organizations')
    .select('id')
    .eq('slug', tenantSlug)
    .single();

  if (!org) return <div>Organization not found</div>;

  // RLS filtra automáticamente — solo devuelve proyectos
  // donde el usuario actual tiene membresía
  const { data: projects } = await supabase
    .from('projects')
    .select('*')
    .eq('org_id', org.id)
    .order('created_at', { ascending: false });

  return (
    <div>
      {projects?.map(project => (
        <ProjectCard key={project.id} project={project} />
      ))}
    </div>
  );
}

Aquí está el gran problema: incluso si alguien manipula el org_id en la solicitud, RLS no cederá. Bloquea el acceso a proyectos a menos que el usuario sea miembro. Técnicamente, .eq('org_id', org.id) es redundante para la seguridad—RLS se encarga de eso—pero es bueno para el rendimiento y la legibilidad.

Optimización de Rendimiento y Trampas Comunes

El Problema de Consulta RLS N+1

Cada comprobación de política RLS genera una subconsulta. Enganchar una política de 10 filas cuando estás mirando 100 filas significa 100 rondas de búsqueda de membresía. Afortunadamente, PostgreSQL es lo suficientemente inteligente como para cachear—pero hay sobrecarga.

Solución: Usa STABLE en funciones auxiliares (como describimos). También, considera denormalizar org_id en los claims JWT:

-- Hook de JWT personalizado (Supabase Dashboard > Auth > Hooks)
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event JSONB)
RETURNS JSONB AS $$
DECLARE
  org_ids UUID[];
BEGIN
  SELECT array_agg(org_id) INTO org_ids
  FROM memberships
  WHERE user_id = (event->>'user_id')::UUID;

  event := jsonb_set(
    event,
    '{claims,org_ids}',
    to_jsonb(org_ids)
  );

  RETURN event;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Luego tu política RLS se convierte en:

CREATE POLICY "Members can view"
  ON projects FOR SELECT
  USING (
    org_id = ANY(
      (SELECT array(SELECT jsonb_array_elements_text(
        auth.jwt()->'org_ids'
      ))::UUID[])
    )
  );

Esto elimina completamente la búsqueda de la tabla de membresía. Los IDs de org vienen directamente del JWT. Advertencia: Los claims de JWT se estampan en el inicio de sesión. Cambia la membresía de alguien, y tendrán que reautenticarse para sincronizar los claims. Típicamente, eso es totalmente manejable—solo mantenlo en tus documentos.

Agrupación de Conexiones

Supabase proporciona agrupación de conexiones a través de PgBouncer. Si vas en vivo con Next.js en Vercel, recuerda: URL de agrupador para rutas de API y componentes de servidor.

# Para operaciones regulares (agrupadas)
DATABASE_URL=postgres://user:pass@db.project.supabase.co:6543/postgres

# Solo para migraciones (directo)
DIRECT_URL=postgres://user:pass@db.project.supabase.co:5432/postgres

Cualquiera en Supabase Pro por $25 al mes obtiene 200 conexiones concurrentes a través del agrupador. Para la mayoría de aplicaciones SaaS tímidas de 1000 usuarios concurrentes, es más que suficiente.

Índices que Absolutamente Necesitas

Aquí está el conjunto de índices de fuerza bruta para una configuración multi-inquilino:

-- En cada tabla con alcance de inquilino
CREATE INDEX idx_{table}_org_id ON {table}(org_id);

-- Índices compuestos para consultas comunes
CREATE INDEX idx_projects_org_created ON projects(org_id, created_at DESC);

-- Membresías — fuertemente consultadas por RLS
CREATE INDEX idx_memberships_user_org ON memberships(user_id, org_id);
CREATE INDEX idx_memberships_org_role ON memberships(org_id, role);

EXPLAIN ANALYZE—el mejor amigo de un desarrollador. Ve cómo tus consultas se comportan con RLS a bordo. Podrías llevarte una sorpresa desagradable sobre qué decide hacer el planificador sin los índices correctos.

Pruebas de Políticas RLS

Todos omiten esto, sin embargo es tu mejor red de seguridad contra fugas de datos. Probamos políticas RLS directamente en SQL:

-- Probar como un usuario específico
SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}';
SET role = 'authenticated';

-- Esto solo debe devolver proyectos a los que el usuario tiene acceso
SELECT * FROM projects;

-- Esto debe fallar (el usuario no es miembro de esta org)
INSERT INTO projects (org_id, name) VALUES ('other-org-uuid', 'Sneaky Project');

-- Restablecer
RESET role;

Y no olvidemos pgTAP para políticas críticas:

BEGIN;
SELECT plan(3);

-- Configurar contexto de prueba como usuario A (miembro de org 1)
SET LOCAL request.jwt.claims = '{"sub": "user-a-uuid"}';
SET LOCAL role = 'authenticated';

SELECT is(
  (SELECT count(*) FROM projects WHERE org_id = 'org-1-uuid')::INTEGER,
  5,
  'User A sees 5 projects in their org'
);

SELECT is(
  (SELECT count(*) FROM projects WHERE org_id = 'org-2-uuid')::INTEGER,
  0,
  'User A sees 0 projects in other org'
);

SELECT throws_ok(
  $$INSERT INTO projects (org_id, name) VALUES ('org-2-uuid', 'Hack')$$,
  'new row violates row-level security policy',
  'User A cannot insert into other org'
);

SELECT * FROM finish();
ROLLBACK;

Ejecuta estas en CI. Cada migración que juegue con políticas RLS debería enviar la suite de pruebas completa a través de un entrenamiento vigoroso.

Lista de Verificación de Implementación en Producción

¿Listo para enviar? Prepárate con esto:

  • RLS habilitado en cada tabla que alberga datos de inquilino
  • Clave de rol de servicio guardada del lado del servidor, en ningún lugar cerca de un cliente
  • org_id adecuadamente indexado en todas las tablas con alcance de inquilino
  • Funciones auxiliares de membresía marcadas como SECURITY DEFINER y STABLE
  • JWT claims personalizados listos y funcionando (si estás en la ruta JWT)
  • ¿Tienes agrupación de conexiones enganchada para implementación en la nube?
  • Políticas RLS fuera de control de calidad con pgTAP o similares
  • ¿Aceleraste EXPLAIN ANALYZE en consultas cruciales con RLS ejecutándose?
  • ¿No falta el flujo de invitación/registro en ningún arranque de membresía?
  • ¿Hay límite de velocidad en puntos finales de autenticación? Supabase ofrece opciones integradas
  • ¿Volteaste el interruptor para habilitar RLS para tablas del esquema auth en el Supabase Dashboard (a menudo una mina terrestre)?
  • ¿Incrustaste monitoreo para consultas lentas? (Supabase Dashboard > Database > Query Performance)

¿Lanzando un producto multi-inquilino y buscas a alguien que haya navegado estas aguas? Nuestras soluciones de desarrollo de CMS sin cabeza o un chat rápido a través de nuestra página de contacto podrían ser justo lo que necesitas.

Preguntas Frecuentes

¿Puedo usar Supabase RLS para aplicaciones con miles de inquilinos? Absolutamente. Hemos piloteado RLS de tabla compartida con más de 5,000 inquilinos y millones de filas sin sudar. ¿La salsa secreta? Indexación adecuada en columnas org_id y funciones auxiliares STABLE. ¿Considerando 50,000+ inquilinos o extravaganzas de miles de millones de filas? Sumérgete en particionar tablas por org_id o coquetea con una configuración de esquema por inquilino.

¿Cómo manejo el cambio de inquilino cuando un usuario pertenece a múltiples organizaciones? Guarda la organización activa en una cookie o URL (subdominio). ¿Cambiar orgs? Ajusta el subdominio/cookie y obtén datos nuevos. No guardes la organización activa en el JWT—requiere un relogin para cambiar. Una cookie que tu middleware puede ver es el camino a seguir.

¿Qué sucede si olvido habilitar RLS en una tabla? Cada usuario autenticado podría tocar cada fila. Esa es la postura predeterminada de PostgreSQL—sin restricciones a nivel de fila en tablas sin RLS. El Supabase Dashboard marca tablas que faltan RLS, pero incrustar esto en CI con consultas a pg_tables y pg_policies ayuda también.

¿Debo usar la clave de rol de servicio de Supabase o cocinar un rol PostgreSQL personalizado para tareas administrativas? En su mayoría, la clave de rol de servicio es suficiente. Omite RLS por completo, así que es tu secreto superior para uso solo del lado del servidor. ¿Necesitas gobernanza granular (como un rol "admin" merodeando en todas las orgs pero tímido de eliminaciones)? Ese es territorio PostgreSQL personalizado—avanzado y generalmente fuera de tu radar hasta que el tooling interno complejo lo requiera.

¿Cómo ejecuto migraciones de base de datos sin tropezar con políticas RLS? Supabase CLI (supabase db push o supabase migration) junto con la URL de base de datos directa (omitiendo agrupación) tiene tu espalda. Tuck ediciones de políticas RLS en la misma migración que cambios de esquema. Lanza migraciones de prueba contra un proyecto de staging—Supabase te permite girar ramas de vista previa en Pro solo para este tipo de cosas.

¿Pueden las políticas RLS llegar a datos de otras APIs o servicios? No. Las políticas RLS se sientan acogidas en SQL, evaluadas por PostgreSQL. ¿De lujo verificando datos externos (como un servicio de bandera de característica)? Cementa ese dato en una tabla de base de datos, luego referencia en tu política. Un patrón típico es sincronizar estados de suscripción desde Stripe a una columna organizations.plan.

¿Cuál es la tarifa de rendimiento de RLS en comparación con filtrado a nivel de aplicación? En nuestros puntos de referencia Supabase Pro (2 vCPU, 8GB RAM), RLS añade 1-3ms extra por consulta para políticas de comprobación de membresía básicas con los índices correctos. Vuelve loco con complejidad de política o joins y podrías añadir 5-15ms. La táctica de claims JWT (almacenar org_ids en el token) lo reduce por debajo de 1ms ya que no hay danza de subconsultas. Para aplicaciones web típicas, ese goteo de latencia es insignificante.

¿Cómo funciona esto con suscripciones Supabase Realtime? Supabase Realtime juega según las reglas RLS. Sintoniza cambios de tabla y atrapa solo eventos de filas que eres elegible para ver según RLS. Esto se implementa lista para usar con cero tinkering extra. Solo asegúrate de que tu Supabase del lado del cliente tenga la sesión del usuario, que @supabase/ssr maneja perfectamente.