Cómo crear un portal de miembros con inicio de sesión y panel de control en Webflow Cloud

Cómo crear un portal de miembros con inicio de sesión y panel de control en Webflow Cloud

Aprende a crear un portal de miembros en Webflow Cloud donde cada consulta esté limitada al miembro que ha iniciado sesión.

Cómo crear un portal de miembros con inicio de sesión y panel de control en Webflow Cloud

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

Puedes diseñar un portal de miembros que mantenga los datos de cada usuario estrictamente privados, incluso si intentan buscar los de alguien más.

Añadir el inicio de sesión a una aplicación de Webflow Cloud es un problema resuelto, y lo hemos documentado más de una vez con Clerk, Auth0 y Supabase. Sin embargo, otro punto crítico es el portal.

Un portal es un conjunto de páginas donde cada consulta debe responder correctamente a "¿de quién son estos datos?", siempre, incluso cuando alguien cambia un número en la URL. La autenticación establece la identidad y luego se detiene, lo que deja la parte difícil totalmente en manos de tu capa de consulta.

Esta guía da por sentada la identidad y construye el portal: un modelo de datos con la propiedad integrada, una capa de datos que no puede ser invocada sin una sesión, un panel que solo lee las filas del miembro conectado y una edición de perfil que no puede dirigirse a otra persona.

¿Qué necesitas para crear un portal de miembros en Webflow Cloud?

Necesitas un proveedor de identidad ya configurado, un enlace a la base de datos para los datos de los miembros y una decisión clara sobre cuál de los tres lugares disponibles almacenará cada pieza de información.

Empieza por aquí:

  • Un proveedor de identidad que ya funcione en tu aplicación. Cualquiera sirve, y tenemos configuraciones completas para Clerk, roles de Auth0 y Supabase Auth
  • Un enlace a una base de datos SQLite en tu entorno de Webflow Cloud, declarado en un archivo wrangler.json para que la plataforma lo aprovisione al desplegar
  • Una aplicación de Webflow Cloud en Next.js 15 o superior, con Node.js 22 o posterior instalado localmente
  • Una frase que describa lo que un miembro debe ver al iniciar sesión, porque un panel sin una respuesta clara para eso se convierte simplemente en un menú de navegación

Antes de definir el esquema, decide dónde vive cada cosa.

Datos Dónde pertenece Por qué
Correo electrónico, contraseña, sesión Tu proveedor de identidad Nunca almacenes credenciales tú mismo; el proveedor es el responsable
Nombre de usuario, avatar, preferencias La base de datos de tu aplicación Lista para consultar, combinar y editar por el miembro
Plan o rol Reclamación del proveedor de identidad, reflejada en tu base de datos Léela del token para el control de acceso y guárdala para realizar consultas
Registros propiedad del miembro La base de datos de tu aplicación, con una columna de propietario Cada fila necesita un propietario, o no podrás delimitar la consulta
Páginas de marketing y contenido de ayuda CMS de Webflow Los editores lo cambian sin tocar la aplicación
Datos → Dónde pertenecen → Por qué
Correo electrónico, contraseña, sesión
Tu proveedor de identidad
Nunca almacenes credenciales tú mismo; el proveedor es el responsable
Nombre de usuario, avatar, preferencias
La base de datos de tu aplicación
Lista para consultar, combinar y editar por el miembro
Plan o rol
Reclamación del proveedor de identidad, reflejada en tu base de datos
Léela del token para el control de acceso y guárdala para realizar consultas
Registros propiedad del miembro
La base de datos de tu aplicación, con una columna de propietario
Cada fila necesita un propietario, o no podrás delimitar la consulta
Páginas de marketing y contenido de ayuda
CMS de Webflow
Los editores lo cambian sin tocar la aplicación

La cuarta fila es la más importante. Un registro propiedad de un miembro sin una columna de propietario no se puede delimitar, y añadir la propiedad a una tabla que ya tiene filas significa adivinar quién era el dueño de qué.

Una vez resuelto esto, la construcción depende de una regla aplicada sin excepciones: el ID del miembro proviene de la sesión, nunca de la solicitud.

5 pasos para crear un portal de miembros con inicio de sesión y panel de control

La construcción consiste en un límite de sesión, un esquema con propiedad, una capa de datos que nada puede eludir, un panel de control renderizado en el servidor y la edición de perfiles limitada al usuario que realiza la llamada.

Cada paso elimina una forma de leer accidentalmente los datos de otra persona, por lo que el orden es deliberado y no convencional.

1. Coloca una función entre tu aplicación y tu proveedor de identidad

Independientemente del proveedor que elijas, envuélvelo en una única función que devuelva al miembro actual o nada. Todo lo demás en el portal llama a eso, y nunca directamente al proveedor.

Vale la pena hacerlo incluso si estás seguro de tu proveedor. Te da un lugar único para responder "¿quién pregunta?", un lugar para cambiar si migras y una cosa para simular en las pruebas. Y lo que es más útil, significa que la regla de propiedad que aparece a continuación tiene una única fuente de verdad para el ID del miembro.

La estructura es sencilla: lee la sesión o el token que te proporcione el proveedor, verifícalo y devuelve un objeto normalizado con un id, un email, un displayName opcional y cualquier reclamación que quieras restringir. Devuelve null en lugar de lanzar un error cuando no haya sesión, así los llamadores podrán redirigir en vez de tener que capturarlo.

Usa el identificador de usuario del proveedor como tu id. Es estable, ya es único y te ahorra mantener una segunda tabla de identidad cuyo único trabajo sea asignar un ID a otro. Terminas este paso con una getSessionUser() que puedes llamar desde cualquier componente de servidor o manejador de rutas.

2. Dale a cada tabla propiedad de un miembro una columna de propietario

Ahora el esquema, y la regla que lo define: cualquier tabla que contenga datos de miembros debe incluir una columna que indique a qué miembro pertenecen.

Declara el enlace en un wrangler.json confirmado, apuntando migrations_dir a tu carpeta de migraciones:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "member-portal",
  "compatibility_date": "2025-04-15",
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "portal",
      "database_id": "placeholder",
      "migrations_dir": "migrations"
    }
  ]
}

La propiedad migrations_dir es la que hace que la siguiente parte funcione: Webflow Cloud aplica automáticamente las migraciones de esa carpeta al desplegar. Si la omites, obtendrás una base de datos aprovisionada sin tablas, lo que fallará más adelante y parecerá un error de consulta.

Luego escribe la migración:

-- migrations/0001_member_portal.sql
CREATE TABLE members (
  id           TEXT PRIMARY KEY,   -- your provider's user id (the `sub`)
  email        TEXT NOT NULL,
  display_name TEXT,
  plan         TEXT NOT NULL DEFAULT 'free',
  created_at   TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE documents (
  id         TEXT PRIMARY KEY,
  -- The owner column is the whole point. A row without one cannot be
  -- scoped to a member, and every query below depends on it.
  member_id  TEXT NOT NULL REFERENCES members(id),
  title      TEXT NOT NULL,
  body       TEXT,
  -- A DEFAULT is evaluated on INSERT only, and SQLite has no
  -- ON UPDATE clause, so every write has to set this itself or the
  -- dashboard's sort order silently becomes creation order.
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Index the owner, because every read filters on it.
CREATE INDEX documents_member_idx ON documents (member_id, updated_at);

Dos cosas sobre el lado de Webflow Cloud. La plataforma aprovisiona la base de datos y sustituye el database_id real al desplegar, así que un marcador de posición en tu configuración es suficiente, y el nombre del enlace name debe coincidir con tu código.

Además, Webflow Cloud valida wrangler.json antes de leer los enlaces, y un fallo de validación no detiene la compilación: simplemente registra el error y despliega tu aplicación sin enlaces, por lo que un despliegue exitoso aún puede dejarte con un env.DB indefinido en tiempo de ejecución.

La clave foránea en documents.member_id cumple una función real más allá del orden, ya que el SQLite de Webflow Cloud aplica restricciones de clave foránea por defecto sin necesidad de activarlas. Una fila huérfana se convierte en un error de base de datos en lugar de un registro al que nadie puede acceder.

Una advertencia sobre updated_at, ya que el panel lo usa para ordenar: elige un formato y mantente fiel a él. CURRENT_TIMESTAMP escribe YYYY-MM-DD HH:MM:SS, que se ordena correctamente como texto, y mezclar valores ISO-8601 escritos por la aplicación con un separador T en la misma columna rompe ese orden, lo que parece un error de caché. Terminas este paso con un esquema donde el propietario de cada fila es una columna, no una suposición.

3. Crea una capa de datos que no pueda llamarse sin una sesión

Este paso convierte la regla de propiedad de una convención en algo que el código impone. Cada lectura y escritura de datos de miembros pasa por un único módulo, y ese módulo obtiene la sesión por sí mismo en lugar de aceptar un ID de miembro.

La firma es la propiedad de seguridad. Una función que toma memberId como argumento puede ser llamada con cualquier ID de miembro por cualquier llamador, mientras que una función que lee la sesión por sí misma no puede.

Aquí tienes ese módulo:

// lib/member-data.ts
import { getCloudflareContext } from '@opennextjs/cloudflare'
import { getSessionUser } from '@/lib/session'

// The only entry point to member data. Nothing else in the app is
// allowed to touch the documents table, which is what makes the
// ownership rule enforceable rather than aspirational.
async function db() {
  const { env } = getCloudflareContext()
  return env.DB
}

export async function listMyDocuments() {
  const user = await getSessionUser()
  if (!user) throw new Error('Not signed in')

  const { results } = await (await db())
    .prepare(
      `SELECT id, title, updated_at
       FROM documents
       WHERE member_id = ?
       ORDER BY updated_at DESC`
    )
    .bind(user.id)
    .all<{ id: string; title: string; updated_at: string }>()

  return results
}

export async function getMyDocument(documentId: string) {
  const user = await getSessionUser()
  if (!user) throw new Error('Not signed in')

  // Note the two conditions. Filtering on id alone is the bug this
  // whole article exists to prevent: it returns another member's
  // document to anyone who edits the URL.
  return (await db())
    .prepare(
      `SELECT id, title, body, updated_at
       FROM documents
       WHERE id = ? AND member_id = ?`
    )
    .bind(documentId, user.id)
    .first<{
      id: string
      title: string
      body: string | null
      updated_at: string
    }>()
}

Observa bien getMyDocument. La cláusula WHERE filtra tanto por el ID del documento como por el ID del miembro, y esa segunda condición es la defensa total contra el error más común en los portales. Si consultas solo por ID, la página funciona perfectamente para todos los miembros, hasta que uno de ellos edita la URL y lee el documento de otra persona.

Sin errores, sin advertencias y nada en una suite de pruebas normal lo detecta, porque cada prueba asume que un miembro está leyendo sus propios datos.

Nota también que una fila inexistente y una fila que pertenece a otra persona son indistinguibles desde fuera. Eso es intencional: devolver "no encontrado" en lugar de "no es tuyo" evita confirmar que un ID determinado existe siquiera.

Terminas este paso con un módulo donde escribir una consulta sin alcance parecería obviamente incorrecto junto a sus funciones relacionadas.

4. Renderiza el panel en el servidor

Una vez configurada la capa de datos, el panel de control es principalmente diseño. La única decisión importante es dónde colocar el filtro de acceso.

Filtro en el servidor, antes de que se renderice nada:

// app/(portal)/dashboard/page.tsx
import Link from 'next/link'
import { redirect } from 'next/navigation'
import { getSessionUser } from '@/lib/session'
import { listMyDocuments } from '@/lib/member-data'

export default async function DashboardPage() {
  const user = await getSessionUser()

  // Gate on the server. A client-side redirect renders the page
  // first, which means the data has already left the building.
  if (!user) redirect('/login')

  const documents = await listMyDocuments()

  return (
    <main>
      <h1>Welcome back, {user.displayName ?? user.email}</h1>

      {documents.length === 0 ? (
        <p>Nothing here yet. Create your first document to get started.</p>
      ) : (
        <ul>
          {documents.map((doc) => (
            <li key={doc.id}>
              {/* next/link, not a plain anchor. Next.js applies the
                  mount path to Link, useRouter and redirect; a bare
                  href resolves against the site root and 404s. */}
              <Link href={`/dashboard/documents/${doc.id}`}>{doc.title}</Link>
            </li>
          ))}
        </ul>
      )}
    </main>
  )
}

Una comprobación en el lado del cliente no es un filtro real. Renderiza la página, obtiene los datos y luego los oculta, lo que significa que un visitante no autenticado ya ha recibido los registros para cuando la interfaz finge que no puede verlos. Redirigir desde un componente de servidor garantiza que no se envíe nada.

El estado vacío merece más atención de la que suele recibir. Un nuevo miembro llega a un panel de control sin nada, y esa primera pantalla determina si entiende para qué sirve el portal. Una lista vacía con una siguiente acción clara siempre es mejor que una lista vacía a secas.

Si añades llamadas desde el lado del cliente a tus propias rutas de API desde estas páginas, recuerda que necesitan la ruta de montaje de la aplicación: Webflow Cloud sirve tu aplicación bajo ella, Next.js la aplica a las definiciones de ruta y las peticiones manuales deben incluirla por sí mismas.

Terminas este paso con un panel de control que muestra al miembro que ha iniciado sesión sus propios registros y redirige a todos los demás.

5. Permite que los miembros editen su propio perfil, y solo el suyo

La edición del perfil es donde se pone a prueba la regla de propiedad, porque es el primer lugar donde un miembro te envía datos sobre sí mismo.

El manejador a continuación toma un nombre de visualización y nada más:

// app/api/profile/route.ts
import { NextResponse, type NextRequest } from 'next/server'
import { getCloudflareContext } from '@opennextjs/cloudflare'
import { getSessionUser } from '@/lib/session'

export async function PATCH(request: NextRequest) {
  const user = await getSessionUser()
  if (!user) {
    return NextResponse.json({ error: 'Not signed in' }, { status: 401 })
  }

  const { displayName } = (await request.json()) as { displayName?: string }
  const trimmed = (displayName ?? '').trim()

  if (!trimmed || trimmed.length > 80) {
    return NextResponse.json({ error: 'Invalid name' }, { status: 422 })
  }

  const { env } = getCloudflareContext()

  // The id comes from the session, never from the request body. If a
  // member could name the row they are updating, they could update
  // anyone's.
  const { meta } = await env.DB
    .prepare('UPDATE members SET display_name = ? WHERE id = ?')
    .bind(trimmed, user.id)
    .run()

  // An update that matched nothing is not a success. This is usually
  // a member row that was never created on first sign-in.
  if (meta.changes === 0) {
    return NextResponse.json({ error: 'No member record' }, { status: 404 })
  }

  return NextResponse.json({ ok: true })
}

La línea importante es la que falta: en ninguna parte se lee un ID de miembro del cuerpo de la solicitud. Esa es la diferencia entre un endpoint que actualiza tu perfil y uno que actualiza el de cualquiera.

Es fácil equivocarse, porque aceptar un ID parece más flexible, y esa flexibilidad es precisamente la vulnerabilidad.

Valida en el servidor incluso cuando el formulario ya lo haga. El formulario es una comodidad para los usuarios honestos; el manejador es lo que realmente protege. Y mantén la lista de campos permitidos limitada: aceptar un objeto arbitrario y expandirlo en una sentencia de actualización permite que un miembro configure columnas que nunca quisiste exponer, como su propio plan.

Terminas este paso con un portal donde un miembro puede cambiar lo que debe poder cambiar, y donde los endpoints se niegan a ser dirigidos a cualquier otra persona.

¿Qué hace que los portales de miembros fallen en Webflow Cloud?

Los fallos en los portales se dividen claramente en dos: los ruidosos, donde nadie puede iniciar sesión, y los silenciosos, donde todos pueden iniciar sesión pero ven lo que no deben. Los silenciosos son la mitad peligrosa, así que la primera entrada a continuación es la que debes leer incluso si tu portal parece funcionar bien.

Un miembro puede ver los registros de otro miembro

Causa: Una consulta filtrada por un ID de registro sin filtrar también por el propietario. Rara vez se trata de una falta de comprobación de autenticación; el miembro ha iniciado sesión correctamente, la sesión es válida y la página está protegida por un filtro funcional. El filtro responde si pueden ver un documento, pero no dice nada sobre cuál.

Solución: Busca en tu capa de datos las consultas contra tablas propiedad de los miembros y confirma que cada una incluya una condición de propietario. Luego, realiza una prueba adecuada: inicia sesión como miembro A, toma un ID de registro perteneciente al miembro B y solicítalo directamente.

Las pruebas normales nunca detectan esto, porque naturalmente pruebas como un miembro viendo sus propios datos. Si tu base de datos admite políticas a nivel de fila, añádelas como una segunda línea de defensa en lugar de como sustituto de la condición de consulta.

Ten en cuenta que el SQLite de Webflow Cloud no las ofrece, por lo que en esa arquitectura la condición de consulta es la única medida de seguridad que tienes, que es precisamente la razón por la que debe estar detrás de una capa de datos que nada pueda eludir.

Todos cierran sesión inmediatamente después de desplegar

Causa: Atributos de cookie que funcionaban en local pero no en producción. Las causas comunes incluyen una cookie configurada sin un origen HTTPS, un valor que bloquea el redireccionamiento de callback, o una cookie con un ámbito de ruta que no incluye la ruta de montaje de tu aplicación.SecureSameSite

Solución: Inspecciona las cabeceras de respuesta del callback en la pestaña de red y lee el valor que estás enviando realmente, no el que pretendías. Asegúrate de definir bien la dirección de la regla de ruta, ya que es fácil confundirse: el ámbito más permisivo coincide con cualquier ruta de solicitud, incluida la ruta de montaje.Set-CookiePath=/

Lo que rompe un portal es una cookie con un ámbito más estrecho que las páginas que la necesitan, como una configurada con una ruta que luego simplemente no existe en las rutas de tu panel de control.Path=/api/auth

La comprobación específica de Webflow Cloud son las URLs de callback permitidas del proveedor, que deben incluir la URL desplegada con la ruta de montaje en lugar de solo la local.

Un callback que no está en la lista falla antes de que se configure la cookie, lo cual parece idéntico desde fuera.

El panel de control falla porque el enlace a la base de datos no está definido

Causa: O bien se llamó al enlace en el ámbito del módulo en lugar de dentro de una solicitud, o el enlace nunca se aprovisionó. Ambos producen el mismo valor indefinido y el mismo seguimiento de pila inútil en la primera consulta.getCloudflareContext()

Solución: Confirma que la llamada al contexto ocurre dentro de la función que maneja la solicitud, como en la capa de datos anterior. Si ya es así, lee el registro de compilación antes de tocar el código: Webflow Cloud valida primero el archivo comprometido, y un error de validación registra el problema y luego despliega tu aplicación sin enlaces.wrangler.json

Un despliegue exitoso no significa que el enlace exista. Revisa la pestaña de Almacenamiento del entorno, que muestra el enlace y su estado una vez que el aprovisionamiento se ha completado realmente.

Las actualizaciones de perfil devuelven éxito, pero no cambia nada

Causa: Una actualización que no coincidió con ninguna fila. La razón más común es una discrepancia de ID entre tu proveedor de identidad y tu tabla, generalmente porque la fila del miembro nunca se creó en el primer inicio de sesión, por lo que no hay nada que actualizar.members

Solución: Comprueba los metadatos de resultado que devuelve el controlador de tu base de datos y trata las cero filas cambiadas como un error en lugar de un éxito, ya que una actualización que no afecta a nada no es lo mismo que una actualización. Luego, soluciona el problema subyacente creando la fila del miembro en la primera solicitud autenticada si no existe.

Un portal que asume que una fila existe porque alguien inició sesión fallará para cada nuevo miembro, que es el peor grupo posible con el que fallar.

Qué construir a continuación en tu portal de miembros de Webflow Cloud

Con la propiedad aplicada y un panel que muestra datos reales, las siguientes adiciones suelen centrarse en lo que los miembros pueden hacer, no en lo que pueden ver.

La facturación es lo habitual, y cambia la pregunta de acceso de "¿quién eres?" a "¿qué has pagado?". Nuestra guía sobre autenticación y pagos juntos cubre esa combinación, y la guía de membresía por niveles cubre el acceso al contenido basado en planes.

Si tu portal es interno en lugar de estar orientado al cliente, el portal de autoservicio para empleados sigue el mismo patrón, con datos de la empresa y anuncios gestionados por el CMS.

Preguntas frecuentes

¿Qué proveedor de autenticación debería usar para un portal de miembros?

Cualquier proveedor con una ruta de Webflow Cloud funcional servirá, y al código del portal anterior no le importa cuál elijas. Envuelve el que elijas en una única función de sesión para que la elección siga siendo intercambiable y tu capa de datos tenga una única fuente de verdad para el ID de miembro.

¿Necesito una base de datos o los miembros pueden vivir en el CMS?

Usa una base de datos para los registros de los miembros. El CMS está diseñado para que los editores publiquen contenido, no para filas que un miembro lee y escribe, y no permite limitar una consulta a una sola persona. Reserva el CMS para páginas y contenido de ayuda.

¿Es suficiente la seguridad a nivel de fila por sí sola?

Considérala como una segunda capa si la tienes, pero ten en cuenta que puede que no sea así. Las políticas a nivel de fila son una función de Postgres, disponible en servicios como Supabase. Sin embargo, no están en el SQLite de Webflow Cloud, por lo que en la arquitectura que usa esta guía, la condición de propietario en la consulta es tu única garantía. Donde existan políticas, vale la pena añadirlas, ya que funcionan incluso si el código de la aplicación falla.

¿Dónde debería residir el plan o el rol?

Léelo desde el token de identidad al restringir una solicitud y duplícalo en tu base de datos cuando necesites consultar o generar informes. Leerlo desde el token evita una llamada a la base de datos en cada página; duplicarlo significa que puedes obtener información sobre tus miembros.

¿Por qué restringir el acceso en un componente de servidor en lugar del cliente?

Porque una restricción en el lado del cliente renderiza la página y obtiene los datos antes de ocultarlos, por lo que los registros ya se han enviado; redirigir en el servidor significa que un visitante no autenticado no recibe nada.


Última actualización
September 12, 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.