Cómo crear una plataforma de suscripción por niveles con Supabase y Webflow

Cómo crear una plataforma de suscripción por niveles con Supabase y Webflow

Aprende a crear una plataforma de suscripción por niveles en Webflow donde la seguridad a nivel de fila (RLS) refuerza el acceso en la base de datos, para que un olvido en la validación de una ruta no filtre contenido de pago.

Cómo crear una plataforma de suscripción por niveles con Supabase y Webflow

Ismail Ajagbe
Redactor técnico
Ver perfil del autor
Ismail Ajagbe
Redactor técnico
Ver perfil del autor
Índice de contenidos

Potencia tu plataforma de suscripción con una autorización robusta a nivel de base de datos. Al centralizar las reglas de acceso en Supabase, puedes escalar tu contenido con total confianza, sabiendo que la experiencia de cada miembro se mantiene segura y alineada con sus niveles de suscripción desde el primer día.

La mayoría de los sitios de suscripción protegen el contenido en la capa de aplicación. Una página verifica la sesión, decide que el visitante es un miembro Pro y muestra la lección. Eso funciona hasta que aparece un segundo punto de entrada, y al final, siempre hay un segundo punto de entrada.

Supabase ofrece un enfoque distinto, ya que la regla de autorización puede residir en la propia base de datos. Una consulta de contenido por el que el miembro no ha pagado no devolverá nada, ya sea que provenga de tu página, de una ruta de API perdida o de alguien curioso con la URL del proyecto.

Esta guía construye una suscripción por niveles sobre esa base, en Webflow Cloud, con el control de acceso escrito en SQL en lugar de en un componente.

¿Qué necesitas para crear una suscripción de Supabase en Webflow Cloud?

Necesitas un proyecto de Supabase con autenticación funcional, una aplicación de Webflow Cloud para servir las páginas protegidas y disposición para escribir las reglas de acceso en SQL en lugar de usar condiciones de componentes.

Aquí tienes la lista completa antes de la primera migración:

  • Un proyecto de Supabase con inicio de sesión por correo electrónico o redes sociales ya configurado
  • Tu URL de proyecto y clave pública, además de la clave secreta almacenada por separado para tareas exclusivas de administración
  • Un proyecto de Webflow Cloud que ejecute Next.js 15 o superior, con Node.js 22 o posterior instalado localmente.
  • El paquete @supabase/ssr que gestiona las cookies de sesión que necesita una aplicación renderizada en el servidor

Si el inicio de sesión aún no funciona, nuestra guía de autenticación de Supabase cubre esa capa, y esta guía comienza donde aquella termina. Una vez que la autenticación funciona, todo lo demás depende de una sola decisión: el nivel reside en una tabla en la que el miembro no puede escribir.

5 pasos para proteger contenido por nivel de suscripción en Webflow

La estructura consta de dos tablas, una política que las vincula, una ruta que consulta con la clave pública, un webhook que cambia los niveles cuando alguien paga y una fecha de caducidad que se verifica al momento de la lectura.

El orden es deliberado, ya que cada paso asume las garantías establecidas por el anterior.

1. Modela el nivel como datos que el miembro no puede cambiar

Crea una tabla de suscripciones vinculada al ID de usuario de Supabase, con el nivel y una fecha de caducidad. Mantenla separada de cualquier tabla de perfil que el miembro pueda editar, porque en el momento en que el nivel y el nombre de usuario conviven en la misma fila editable, una actualización de perfil se convierte en una mejora de plan gratuita.

Luego, crea la tabla de contenido con el nivel que requiere cada elemento:

-- Membership tier lives on a row the member cannot edit.
create table public.memberships (
  user_id uuid primary key references auth.users on delete cascade,
  tier text not null default 'free' check (tier in ('free', 'pro', 'vip')),
  expires_at timestamptz
);

create table public.lessons (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  body text not null,
  required_tier text not null default 'free'
    check (required_tier in ('free', 'pro', 'vip'))
);

-- Without this, any role holding a grant can read the whole table.
alter table public.memberships enable row level security;
alter table public.lessons enable row level security;

-- Adding policies does not remove existing grants, so revoke first.
revoke all on table public.memberships from anon, authenticated;
revoke all on table public.lessons from anon, authenticated;

grant select on table public.memberships to authenticated;
grant select on table public.lessons to authenticated;

-- Every signup needs a row, or the policy below finds nothing and
-- even free lessons stay hidden.
create function public.seed_membership()
returns trigger language plpgsql security definer as $$
begin
  insert into public.memberships (user_id)
  values (new.id)
  on conflict do nothing;
  return new;
end;
$$;

create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.seed_membership();

El disparador no es un adorno. La política de lecciones verifica si existe una fila de suscripción, por lo que un miembro que se registra pero nunca paga necesita una entrada inicial en el nivel gratuito; de lo contrario, la política no encontrará nada y ocultará incluso las lecciones gratuitas. Sin esto, la default 'free' en la tabla nunca se aplica a nadie.

Hay otras dos líneas que importan más de lo que parece. Habilitar la seguridad a nivel de fila (Row Level Security) es lo que hace que la tabla sea privada: Supabase es claro al respecto, una tabla en un esquema expuesto sin RLS puede ser leída y modificada por cualquier rol que tenga permisos sobre ella.

La revocación es el paso sutil. Añadir políticas no elimina los permisos existentes, así que en un proyecto que todavía otorga y por defecto, primero debes revocar y luego conceder solo lo que cada rol realmente necesita. Terminas este paso con dos tablas que no devuelven nada a nadie.anonauthenticated

2. Escribe la política que actúa como filtro real

Una política funciona como una cláusula WHERE añadida a cada consulta sobre la tabla, y se ejecuta dentro de Postgres en lugar de en tu aplicación. Ese es el punto clave: se aplica sin importar qué ruta, script o cliente realice la petición.

La política de membresía es la sencilla, y la política de contenido es donde se aplican los niveles:

-- A member can read their own membership row, and nobody else's.
create policy "Members read own membership"
on public.memberships for select
to authenticated
using ( (select auth.uid()) = user_id );

-- A lesson is readable only if the member's tier is high enough
-- and their membership has not lapsed.
create policy "Members read lessons for their tier"
on public.lessons for select
to authenticated
using (
  exists (
    select 1
    from public.memberships m
    where m.user_id = (select auth.uid())
      and (m.expires_at is null or m.expires_at > now())
      and case public.lessons.required_tier
            when 'free' then true
            when 'pro'  then m.tier in ('pro', 'vip')
            when 'vip'  then m.tier = 'vip'
            else false
          end
  )
);

Lee la segunda política como una frase: una lección es visible si existe una fila de membresía para el usuario actual, no ha caducado y tiene un nivel al menos tan alto como el que requiere la lección.

Un visitante que no ha iniciado sesión es un caso distinto, y vale la pena ser preciso porque los permisos fueron revocados de y nunca se restauraron; Postgres lanza un error de permiso antes de que se ejecute cualquier política. Eso es una petición fallida en lugar de una lista vacía, por lo que la ruta debe capturarla y devolver un 401.anon

Si prefieres mostrar lecciones gratuitas a visitantes que no han iniciado sesión, otorga a y añade una política que exponga solo el nivel gratuito.selectanon

Prueba esto antes de construir nada sobre ello. Inicia sesión como miembro gratuito y consulta la tabla de lecciones directamente desde el editor SQL de Supabase como ese usuario. Si aparece una lección Pro, la política está mal, y nada de lo que construyas encima lo arreglará.

Termina este paso con una tabla de lecciones que devuelva conjuntos de filas diferentes para un miembro gratuito y uno Pro, sin necesidad de código de aplicación.

3. Consulta desde un Route Handler con la clave pública

Con el filtro en la base de datos, el código de la aplicación se vuelve notablemente aburrido, lo cual es señal de que funciona.

La ruta solicita todas las lecciones y deja que la política decida qué significa eso:

// app/api/lessons/route.ts
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { createServerClient } from '@supabase/ssr'

export async function GET() {
  const jar = await cookies()

  // Publishable key, not the secret one. The database decides what
  // this session is allowed to see.
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL as string,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY as string,
    {
      cookies: {
        getAll: () => jar.getAll(),
        setAll: (list) => {
          try {
            list.forEach(({ name, value, options }) =>
              jar.set(name, value, options)
            )
          } catch {
            // Called from a context that cannot set headers.
          }
        },
      },
    }
  )

  // No tier filter here on purpose: the policy applies it, so a bug
  // in this file cannot widen access.
  const { data, error } = await supabase
    .from('lessons')
    .select('id, title, body, required_tier')

  if (error) {
    return NextResponse.json({ error: 'Could not load lessons' }, { status: 500 })
  }

  return NextResponse.json({ lessons: data })
}

La ausencia de un filtro de nivel en este archivo es deliberada, no un descuido. Si el filtro estuviera aquí, una futura refactorización podría eliminarlo y publicar contenido de pago por error, y nada fallaría. Con la regla en la base de datos, el mismo error devuelve la misma lista restringida.

Fíjate también en la clave. Esta ruta usa la clave pública, por lo que la consulta se ejecuta como el miembro que ha iniciado sesión. Usar la clave secreta aquí saltaría todas las políticas que acabas de escribir y devolvería todas las lecciones a todo el mundo.

Terminas este paso con una ruta que devuelve resultados diferentes para distintos miembros sin contener ni un solo condicional sobre niveles.

4. Cambia los niveles solo desde una ruta de confianza

Algo tiene que promocionar a un miembro cuando paga, y ese algo no puede ser el propio miembro. Este es el único lugar donde la clave secreta es apropiada, porque escribir en la tabla de membresías es exactamente la operación que las políticas están impidiendo que hagan los demás.

En la práctica, esto es un webhook de tu proveedor de pagos: verifica la firma, vincula al cliente con un usuario de Supabase y actualiza el nivel y la fecha de caducidad. Verifica la firma antes de confiar en los datos, porque un endpoint de webhook sin verificar es una API pública para regalarte una membresía VIP.

Mantén este manejador pequeño y separado de las rutas de lectura, para que la clave secreta aparezca en un solo archivo fácil de auditar. Nunca la importes en nada que renderice una página.

Al finalizar este paso, podrás activar un evento de prueba de pago y ver cómo cambia el nivel del miembro en la tabla sin tener que tocar el panel de control.

5. Gestiona el vencimiento como un estado, no como una tarea programada

Las suscripciones caducan, y la tentación es crear una tarea programada que revise las filas vencidas y las degrade. Esa tarea terminará fallando en algún momento y, cuando eso ocurra, los miembros caducados mantendrán su acceso.

La política anterior evita esto al verificar expires_at en el momento de la lectura. El acceso termina en cuanto pasa la marca de tiempo, sin necesidad de tareas, porque cada consulta responde a la pregunta "¿esta suscripción está vigente?" en lugar de hacerlo una vez por noche.

Conserva la fila caducada en lugar de borrarla. Es el registro de lo que alguien tuvo; hace que la renovación sea una simple actualización y permite que un miembro que regresa vea su historial en lugar de una cuenta vacía.

Terminarás este paso con una suscripción que caduca correctamente, incluso si todos tus procesos en segundo plano dejan de funcionar.

¿Qué hace que falle la restricción de acceso por suscripción en Supabase?

Casi todos los fallos aquí se deben a una de estas dos cosas: una consulta que devuelve más de lo que debería porque falta una regla, o que no devuelve nada porque falta un permiso. Ambos parecen errores de la aplicación, pero no lo son.

Estos cuatro puntos cubren los casos que vale la pena revisar primero.

Los miembros pueden ver contenido superior a su nivel

Causa: O bien nunca se habilitó RLS en la tabla, o la consulta se está ejecutando con la clave secreta. Una tabla sin RLS puede ser leída por cualquier rol con permisos, y la clave secreta omite las políticas por diseño, por lo que ambos casos producen el mismo síntoma: una puerta que no restringe nada.

Solución: Confirma que RLS esté habilitado en todas las tablas del esquema expuesto y luego verifica qué clave utiliza la ruta fallida para configurar su cliente. La prueba más fiable es consultar la tabla directamente desde el editor SQL como un miembro gratuito, en lugar de hacerlo a través de tu aplicación, ya que eso aísla la regla de la base de datos de todo lo que hayas escrito a su alrededor.

Si el editor SQL devuelve filas restringidas, el problema es la política; si no lo hace, el problema es la clave.

Los miembros registrados no ven nada en absoluto

Causa: RLS está habilitado y ninguna política otorga acceso, o los permisos fueron revocados y no se restauraron. Una vez que RLS está activo, no se puede acceder a los datos a través de la API con una clave pública hasta que una política lo permita. Por lo tanto, una tabla con políticas pero sin permisos, o con permisos pero sin políticas, devuelve un conjunto vacío en ambos casos.

Solución: Revisa ambas partes, no solo una. Confirma que el rol tiene permisos sobre la tabla y que existe una política para la operación que se intenta realizar, recordando que una política de selección no sirve para una inserción.

Un resultado vacío sin errores es la forma habitual en que se manifiesta este problema, por lo que suele confundirse con un error de datos en lugar de uno de acceso.

La sesión no aparece dentro del gestor de rutas

Causa: El cliente de Supabase se creó sin conectar las cookies, por lo que la solicitud se ejecuta como un visitante anónimo aunque el navegador tenga la sesión iniciada. Cualquier política que dependa de auth.uid() se evalúa entonces contra la nada.

Solución: Usa el cliente de servidor de con y @supabase/ssrgetAllsetAll implementados para el almacenamiento de cookies de la solicitud, ya que un cliente que puede leer cookies pero no escribirlas no puede refrescar una sesión caducada y empezará a fallar en cuanto el primer token expire.

Registra el ID de usuario resuelto una vez durante el desarrollo para confirmar que la ruta detecta al miembro que esperas. Este error es fácil de diagnosticar mal porque se presenta exactamente igual que el caso de resultado vacío anterior: la consulta tiene éxito, la política se evalúa correctamente y la respuesta para un usuario anónimo es, legítimamente, nada.

La compilación falla después de añadir las rutas de membresía

Causa: Una directiva en uno de los archivos nuevos. Webflow Cloud despliega Next.js a través del adaptador OpenNext Cloudflare, el cual no es compatible con el entorno de ejecución edge de Next.js.export const runtime = 'edge'

Solución: Elimina la línea y vuelve a desplegar, ya que los Route Handlers ya se ejecutan en el entorno de ejecución de Workers sin ella. Busca en todo el proyecto, no solo en el archivo que editaste por última vez, porque una sola directiva en cualquier lugar hará que la compilación falle.

Vale la pena comprobar esto primero, ya que los ejemplos de Supabase a menudo están escritos para otros hosts donde la directiva se espera o es inofensiva, por lo que un controlador copiado puede incluirla sin que te des cuenta.

La propia página de Webflow sobre cómo traer tu propia aplicación todavía les dice a los lectores de Next.js que la añadan, lo que significa que la instrucción que seguiste puede ser la causa en lugar de cualquier cosa que hayas escrito.

Qué puedes construir a continuación con Supabase y Webflow

Una vez que el control de niveles se establece en la base de datos, el mismo patrón de políticas se extiende a todo lo que implica una membresía: acceso a archivos por miembro a través de Storage, comentarios que solo los miembros pueden publicar y una vista de administrador que utiliza una política basada en un rol en lugar de una aplicación separada.

Para profundizar más en el aspecto de los pagos, nuestra guía de autenticación y pagos cubre la facturación de suscripciones con un modelo de cuenta similar. Para las rutas de conexión sin código, consulta la integración de Webflow y Supabase.

Preguntas frecuentes

¿Es seguro poner la clave pública de Supabase en el navegador?

Sí, y Supabase documenta que es seguro exponerla en una página web, aplicación móvil o código fuente. Esa seguridad depende totalmente de que la seguridad a nivel de fila (Row Level Security) esté habilitada con políticas, porque los privilegios de la clave son bajos por diseño y es la base de datos la que aplica el acceso.

¿Cuál es la diferencia entre la clave anon y la clave pública?

Principalmente el nombre. Supabase documenta anon como la versión antigua de la clave pública y service_role como la versión antigua de la clave secreta. Ambas siguen funcionando junto a las claves más nuevas, pero Supabase indica que las claves antiguas quedarán obsoletas a finales de 2026, por lo que las nuevas compilaciones deberían usar las claves pública y secreta.anonservice_role

¿Por qué no comprobar simplemente el nivel en el código de mi aplicación?

Porque esa comprobación solo protege las rutas que la ejecutan. Una política en la base de datos se aplica a cada consulta de cada cliente, por lo que una nueva ruta añadida más tarde hereda la regla en lugar de tener que recordar incluirla.

¿Las políticas reemplazan a los permisos (grants)?

No, y asumir que lo hacen es una forma común de dejar una tabla más abierta de lo previsto. Añadir una política no elimina un permiso existente. Revoca los permisos que no desees y luego vuelve a conceder solo lo que cada rol necesite junto con las políticas.

¿Cómo se deben gestionar las membresías caducadas?

Comprueba la fecha de caducidad dentro de la política en lugar de degradar las filas según un calendario. El acceso termina cuando pasa la fecha, sin depender de un proceso en segundo plano que podría no ejecutarse.


Última actualización
February 14, 2026
Categoría

Artículos relacionados


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo

Empieza gratis

Prueba Webflow todo el tiempo que quieras con nuestro plan Starter gratuito. Compra un plan de sitio de pago para publicar, alojar y desbloquear funciones adicionales.

Empieza, es gratis
Ver demostración

Prueba Webflow todo el tiempo que quieras con nuestro plan Starter gratuito. Compra un plan de sitio de pago para publicar, alojar y desbloquear funciones adicionales.