Arquitectura de autenticación y administración
Guía completa de sesiones con Better Auth, OAuth, RBAC y modelado de datos de negocio (créditos y cuotas) en esta plantilla.
Autenticación y Admin
Este documento es la referencia principal para entender cómo funciona la autenticación y la autorización admin en esta plantilla.
Incluye:
- El modelo exacto de Better Auth usado aquí
- Cómo email/contraseña y OAuth de Google mantienen sesiones
- Por qué
users,accounts,sessionsyverificationsestán separadas - Qué datos son de auth y cuáles son de negocio (créditos, cuotas, permisos)
- Cómo está implementado el RBAC admin en este código
1) Modelo de autenticación usado en esta plantilla
Este proyecto usa sesiones gestionadas en servidor, basadas en cookies y respaldadas por Postgres. No es un modelo JWT-first por defecto.
Por qué:
- Better Auth está configurado con adaptador Drizzle + Postgres en
src/lib/auth.ts - El almacenamiento de sesiones está mapeado a la tabla
sessionsensrc/db/schema.ts - El plugin
nextCookies()está habilitado - No hay plugin JWT habilitado como mecanismo principal
Implicación práctica:
- El navegador guarda una cookie HttpOnly de auth (token de sesión)
- El servidor valida ese token contra
sessions - La fila de sesión apunta a la identidad del usuario
Better Auth sí soporta JWT, pero es opt-in y no reemplaza el modelo de sesión en DB que se usa aquí.
2) Tablas de auth principales y responsabilidades
Better Auth separa de forma intencional identidad, credenciales y estado de sesión.
users (perfil de identidad)
Representa quién es la persona en tu producto.
Campos típicos:
- Perfil (
nickname,avatar_url) - Rol (
role) - Flags de verificación/estado
- Timestamps de creación/actualización
En esta plantilla, también se mapean:
uuid(generado al crear)role
Esta tabla debe mantenerse estable.
accounts (métodos de autenticación)
Representa cómo puede iniciar sesión.
- Un usuario puede tener múltiples cuentas/proveedores
- Guarda identidad del proveedor y tokens del proveedor
Campos típicos:
provider_idaccount_id- Hash de contraseña (email/contraseña)
- Access token / refresh token OAuth
- Expiración de tokens OAuth
Esta tabla es sensible y centrada en credenciales.
sessions (estado de login)
Representa sesiones activas de tu app.
Campos típicos:
user_idtokenexpires_atip_addressuser_agent
Propiedades:
- La expiración de sesión controla si el usuario sigue autenticado
- Revocar sesiones implica borrar/invalidar filas
- La renovación deslizante depende de la policy de sesión
verifications (flujos one-time)
Se usa para tokens temporales en flujos como:
- Reset de contraseña
- Verificación de email
- Magic link / OTP (si se habilita)
El login/logout normal no escribe continuamente aquí, así que ver pocas filas es normal.
3) Procedimiento de autenticación end-to-end en este código
3.1 Registro con email + contraseña
- El cliente envía el formulario de registro
- Better Auth crea fila en
users databaseHooks.user.create.beforeensrc/lib/auth.tsasegurauuid- Better Auth crea fila en
accountspara email/contraseña - Better Auth crea fila en
sessions - Se envía cookie de sesión en la respuesta
databaseHooks.user.create.afterdispara email de bienvenida de forma asíncrona
Resultado: el usuario queda autenticado inmediatamente.
3.2 Login con email + contraseña
- El cliente envía credenciales
- Better Auth valida el hash en
accounts - Better Auth crea/refresca sesión en
sessions - La cookie de sesión se crea/actualiza
3.3 Login con Google OAuth
- El usuario hace click en Google
- Ocurre el flujo redirect + callback OAuth
- Google devuelve identidad de cuenta (
sub) y claims de perfil - Better Auth encuentra o crea usuario
- Better Auth crea/actualiza fila
accountsparagoogle - Better Auth crea fila de sesión
- Se envía la cookie de sesión
4) Regla importante sobre identidad OAuth
Iniciar sesión con Google no requiere Gmail.
- El usuario puede usar Gmail, Outlook, Yahoo o dominio propio vinculado a una cuenta Google
- La identidad debe basarse en identidad del proveedor (
provider_id,account_id/sub), no en dominio de email
Nunca uses el dominio del email como prueba de proveedor.
5) Expiración de sesión vs expiración de tokens OAuth
Son dos ciclos de vida independientes.
Ciclo de sesión app (sessions)
- Controla el estado autenticado en tu app
- Si la sesión expira,
getSession()devuelve null - El usuario debe iniciar sesión otra vez
Ciclo de token de proveedor (accounts)
- Controla si tu backend puede llamar APIs del proveedor
- Los access tokens expiran rápido
- Los refresh tokens pueden renovarlos
Punto crítico:
- La expiración de sesión de Better Auth no reloguea automáticamente usando refresh token de Google
- El refresh token es para llamadas a APIs del proveedor, no para auth de tu app
6) Por qué sesiones y tokens OAuth deben estar separados
Esta separación es necesaria para una seguridad predecible:
- Un refresh token OAuth no es prueba de identidad de la app
- La revocación de sesión debe tener efecto real
- Debe existir reautenticación explícita
- Los límites de seguridad/auditoría deben ser claros
Recrear sesiones automáticamente desde refresh tokens debilita revocación y auditabilidad.
7) Autorización (RBAC) en esta plantilla
La autenticación responde "quién eres". La autorización responde "qué puedes hacer".
Los roles vienen de DB en users.role:
useradmin_roadmin_rw
Los checks de rol están en src/lib/authz.ts:
requireAdminRead()-> permiteadmin_royadmin_rwrequireAdminWrite()-> permiteadmin_rw
Guard de layout admin: src/app/(admin)/layout.tsx.
8) Superficies admin y APIs actuales
Páginas admin
- Dashboard:
src/app/(admin)/admin/page.tsx - Lista de usuarios:
src/app/(admin)/admin/users/page.tsx - Detalle usuario/cuota/créditos:
src/app/(admin)/admin/users/[uuid]/page.tsx - Lista de pedidos/estado:
src/app/(admin)/admin/orders/page.tsx - Feedbacks:
src/app/(admin)/admin/feedbacks/page.tsx - Reservas:
src/app/(admin)/admin/reservations/page.tsx - Afiliados:
src/app/(admin)/admin/affiliates/page.tsx
Rutas API admin
GET /api/admin/users-> lista paginada de usuariosGET /api/admin/users/[uuid]-> detalle usuario + uso mensual + resumen de créditosPATCH /api/admin/users/[uuid]-> actualizamonthlyCreditsQuotaGET /api/admin/users/[uuid]/credits-> resumen de créditos + ledgerPOST /api/admin/credits/grant-> otorga créditosPOST /api/admin/credits/adjust-> ajuste positivo/negativo de créditosGET /api/admin/orders-> pedidos paginados con filtro de estado (all|paid|created|deleted)
9) Sesiones con cookie vs JWT (lo que tienes de verdad)
Configuración actual:
- La cookie de sesión contiene un token de sesión
- El servidor valida el token contra la tabla
sessions - El usuario se resuelve desde sesión respaldada en DB
Es un modelo de sesión en DB, no JWT stateless puro.
Modelo JWT puro (no es el default actual):
- El token contiene claims
- El servidor valida firma en cada request
- Normalmente no requiere lookup en DB para validación básica
10) Por qué BETTER_AUTH_SECRET sigue siendo necesario
Aunque el token de sesión sea aleatorio, el secreto sigue siendo necesario para flujos criptográficos de firma/verificación.
Better Auth resuelve el secreto en este orden:
- Opción
secretexplícita en config BETTER_AUTH_SECRETAUTH_SECRET- Fallback interno (útil en dev, inseguro en producción)
Por eso la app puede arrancar sin secret en env, pero no es una postura segura para prod.
Usa un secreto fuerte y estable por entorno. Cambiarlo puede invalidar artefactos de auth firmados y cerrar sesiones.
11) Modelado de datos de negocio: qué debe quedar fuera de auth
No cargues tablas de auth con contadores de negocio de alto cambio.
Mantén tablas de auth enfocadas en auth
users: identidad/perfil/rolaccounts: proveedores de login + credenciales/tokenssessions: estado de sesiónverifications: tokens one-shot
Lleva estado de producto/facturación a tablas de dominio
Patrón recomendado:
- Tabla de estado actual (lectura rápida)
- Ledger/eventos append-only (auditoría/rebuild)
- Tabla de entitlements (permisos por feature)
Ejemplos de estructura:
-
user_balanceuser_id(PK)credits_balancequota_limitquota_usedplan_idbilling_period_startbilling_period_endupdated_at
-
usage_eventsiduser_idtypeamountcost_creditsmetadata(JSON)created_at
-
entitlementsuser_idkeyvaluesource(plan/admin/promo)expires_at
Esto mantiene auth estable y la lógica de negocio auditable.
12) Cuotas, créditos y seguridad transaccional
Al gastar créditos/cuotas:
- Usa transacciones DB
- Verifica balance/cuota disponible
- Inserta evento de uso en ledger
- Aplica update de balance de forma condicional
Esto evita race conditions y balances negativos bajo concurrencia.
En esta plantilla:
- Uso de tareas en
tasks - Ledger de créditos en
credits - Cuota mensual (
users.monthly_credits_quota) se valida antes de gastar créditos
13) Compatibilidad futura: organizaciones
Si planeas cuentas de equipo:
- Agrega
organizations - Mueve balance/cuota/entitlements a nivel organización
- Trata cuentas personales como organizaciones de un solo miembro
Diseñar esto desde temprano evita refactors dolorosos.
14) Checklist de entorno y setup
Variables mínimas de auth:
BETTER_AUTH_URLNEXT_PUBLIC_AUTH_BASE_URLBETTER_AUTH_SECRET(fuerte y aleatorio)GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET(si Google está activo)
Si cambió el schema, corre migraciones:
pnpm drizzle-kit generate --config src/db/config.ts
pnpm drizzle-kit migrate --config src/db/config.tsResumen final
- Este proyecto usa sesiones en DB con cookies (no JWT-first)
- La identidad OAuth se basa en cuenta del proveedor, no en dominio de email
- El ciclo de sesión app y el ciclo de token del proveedor son independientes
- Las tablas auth deben mantenerse solo para auth
- Créditos/cuotas/permisos deben vivir en tablas de dominio con ledger
- El RBAC admin está basado en rol en DB y protegido en servidor
Esta arquitectura da límites de seguridad sólidos, buen control operativo y una base clara para escalar.
Inicio rápido
Ejecuta la plantilla Sushi SaaS en local con pnpm, recorre rutas i18n, health checks y blogs MDX, y aprende dónde configurar autenticación, pagos y documentación.
Configuración de Stripe
Usa Stripe Checkout para pagos y finaliza vía webhooks para otorgar créditos. Configura variables de entorno, crea sesiones, maneja callbacks y procesa eventos firmados.