Práctico

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, sessions y verifications está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 sessions en src/db/schema.ts
  • El plugin nextCookies() está habilitado
  • No hay plugin JWT habilitado como mecanismo principal

Implicación práctica:

  1. El navegador guarda una cookie HttpOnly de auth (token de sesión)
  2. El servidor valida ese token contra sessions
  3. 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:

  • Email
  • 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_id
  • account_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_id
  • token
  • expires_at
  • ip_address
  • user_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

  1. El cliente envía el formulario de registro
  2. Better Auth crea fila en users
  3. databaseHooks.user.create.before en src/lib/auth.ts asegura uuid
  4. Better Auth crea fila en accounts para email/contraseña
  5. Better Auth crea fila en sessions
  6. Se envía cookie de sesión en la respuesta
  7. databaseHooks.user.create.after dispara email de bienvenida de forma asíncrona

Resultado: el usuario queda autenticado inmediatamente.

3.2 Login con email + contraseña

  1. El cliente envía credenciales
  2. Better Auth valida el hash en accounts
  3. Better Auth crea/refresca sesión en sessions
  4. La cookie de sesión se crea/actualiza

3.3 Login con Google OAuth

  1. El usuario hace click en Google
  2. Ocurre el flujo redirect + callback OAuth
  3. Google devuelve identidad de cuenta (sub) y claims de perfil
  4. Better Auth encuentra o crea usuario
  5. Better Auth crea/actualiza fila accounts para google
  6. Better Auth crea fila de sesión
  7. 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:

  • user
  • admin_ro
  • admin_rw

Los checks de rol están en src/lib/authz.ts:

  • requireAdminRead() -> permite admin_ro y admin_rw
  • requireAdminWrite() -> permite admin_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 usuarios
  • GET /api/admin/users/[uuid] -> detalle usuario + uso mensual + resumen de créditos
  • PATCH /api/admin/users/[uuid] -> actualiza monthlyCreditsQuota
  • GET /api/admin/users/[uuid]/credits -> resumen de créditos + ledger
  • POST /api/admin/credits/grant -> otorga créditos
  • POST /api/admin/credits/adjust -> ajuste positivo/negativo de créditos
  • GET /api/admin/orders -> pedidos paginados con filtro de estado (all|paid|created|deleted)

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:

  1. Opción secret explícita en config
  2. BETTER_AUTH_SECRET
  3. AUTH_SECRET
  4. 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/rol
  • accounts: proveedores de login + credenciales/tokens
  • sessions: estado de sesión
  • verifications: tokens one-shot

Lleva estado de producto/facturación a tablas de dominio

Patrón recomendado:

  1. Tabla de estado actual (lectura rápida)
  2. Ledger/eventos append-only (auditoría/rebuild)
  3. Tabla de entitlements (permisos por feature)

Ejemplos de estructura:

  • user_balance

    • user_id (PK)
    • credits_balance
    • quota_limit
    • quota_used
    • plan_id
    • billing_period_start
    • billing_period_end
    • updated_at
  • usage_events

    • id
    • user_id
    • type
    • amount
    • cost_credits
    • metadata (JSON)
    • created_at
  • entitlements

    • user_id
    • key
    • value
    • source (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_URL
  • NEXT_PUBLIC_AUTH_BASE_URL
  • BETTER_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.ts

Resumen 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.