Volver a proyectos

4Korners Member Portal

Una plataforma de membresías en producción para una organización sin fines de lucro de Montreal: registro e inicio de sesión, membresías con Stripe, donaciones, catálogo de actividades y panel de administración. La construí y la opero yo solo, desde la base de datos hasta el pipeline de despliegue.

Arquitectura

MiembrosPortal Next.jsPostgres
Pagos con Stripe y Zeffy. CRM sincronizado desde Airtable mediante una caché de lectura diaria.

Una aplicación Next.js respaldada por Postgres con Drizzle. Se sincroniza con el CRM Airtable de la organización mediante una caché de lectura que se refresca a diario, cobra pagos con Stripe y donaciones con Zeffy, y protege cada pantalla de administración con control de acceso por roles.

Decisiones clave

Una caché SQL en vez de llamadas en vivo a Airtable

El límite de la API de Airtable no podía servir el portal directamente. Reflejo los datos del CRM, que cambian poco, en Postgres una vez al día y leo de ahí: las páginas para miembros se mantienen rápidas y nunca tocan el límite.

Estado de membresía derivado, calculado y no almacenado

Si un miembro está activo, vencido o en periodo de gracia se calcula a partir de los pagos al leer, no se guarda como bandera. Una sola fuente de verdad y ningún estado obsoleto que reconciliar.

La parte difícil

Los escáneres de correo quemaban los enlaces de un solo uso

Los escáneres de seguridad de correo precargan los enlaces para revisarlos. Abrían el enlace de confirmación antes que el usuario y consumían el token de un solo uso, haciendo fallar registros reales. La solución: dejar de tratar un GET simple como confirmación y exigir una acción explícita del usuario.

Seguridad y rendimiento

  • La autenticación de dos factores del admin (TOTP con códigos de respaldo) protege toda pantalla que muestre datos personales.
  • Los endpoints de inicio de sesión y correo tienen límite de tasa, y las server actions de producción están bloqueadas a su origen.
  • Cumplimiento de la Ley 25: los consentimientos se capturan al registrarse, y una solicitud de borrado se cumple tanto en el portal como en el CRM.

Stack

Next.js 15TypeScriptPostgreSQLDrizzle ORMAuth.js v5StripeZeffyDockerGitHub Actions