Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/juadariasmar/inventory_project/llms.txt

Use this file to discover all available pages before exploring further.

Inventory System delegates all authentication to Neon Auth — not NextAuth or a custom credential system. Neon Auth manages the full identity lifecycle: sign-up, sign-in, password resets, magic links, and OTPs. The application only stores a lightweight Usuario record that mirrors the Neon Auth identity and enriches it with tenant membership, role, and permission data.

How Neon Auth works

Neon Auth is initialised once in src/lib/auth/server.ts using @neondatabase/auth/next/server and reused across every server component and API route.
// src/lib/auth/server.ts
import { createNeonAuth } from '@neondatabase/auth/next/server'

export const auth = createNeonAuth({
  baseUrl: process.env.NEON_AUTH_BASE_URL!,
  cookies: {
    secret: process.env.NEON_AUTH_COOKIE_SECRET!,
    sameSite: 'lax',
  },
})
On the client side, @neondatabase/auth/next exposes a thin React client used to drive the sign-in UI:
// src/lib/auth/client.ts
'use client'
import { createAuthClient } from '@neondatabase/auth/next'

export const authClient = createAuthClient()
ConcernImplementation
Session storageSigned, lax HTTP-only cookie encrypted with NEON_AUTH_COOKIE_SECRET
Session retrieval (server)auth.getSession() — called inside obtenerSesion() on every authenticated request
Sign-in UIProvided by @neondatabase/auth-ui at /auth/sign-in
Middleware redirectUnauthenticated requests are redirected to /auth/sign-in by auth.middleware()
In production, both NEON_AUTH_BASE_URL and NEON_AUTH_COOKIE_SECRET are required. The server throws an error at startup if either is missing. During the Next.js build phase (NEXT_PHASE === 'phase-production-build') placeholder values are used so the build does not fail.

Webhook integration

Neon Auth publishes lifecycle events to your application via HTTP webhooks. Inventory System registers the endpoint POST /api/webhooks/neon to receive those events.
// src/app/api/webhooks/neon/route.ts (simplified)
const rawBody = await req.text()
await WebhooksService.validarFirma(rawBody, req.headers) // verifies NEON_WEBHOOK_SECRET

const { event_type } = JSON.parse(rawBody)

if (event_type === 'user.created') {
  await WebhooksService.procesarEventoUsuarioCreado(payload)
} else if (event_type === 'send.otp') {
  await WebhooksService.procesarEventoSendOtp(userEmail, eventData)
} else if (event_type === 'send.magic_link') {
  await WebhooksService.procesarEventoSendMagicLink(userEmail, eventData)
}
Every incoming webhook is validated by verifying the signature against NEON_WEBHOOK_SECRET. Requests with an invalid signature return 401 before any business logic runs.
The webhook endpoint must be publicly reachable. During local development use a tunneling tool such as ngrok or the Vercel CLI’s dev command so Neon can reach your local instance.

User sync

When Neon Auth fires user.created, the webhook handler creates a matching Usuario row in Neon Postgres and, if the user signed up without an invitation, provisions a new Empresa for them. If the webhook does not arrive in time (e.g. a network delay), obtenerSesion() has a built-in fallback: on the very first authenticated page request it checks whether a Usuario exists for the neonAuthId returned by auth.getSession(). If none exists, it creates the Empresa and Usuario automatically and logs a warning:
// src/lib/permisos.ts (fallback in obtenerSesion)
if (!usuario) {
  console.warn(`[Auth Fallback] Creando usuario ${data.user.email} en obtenerSesion.`)
  const empresa = await prisma.empresa.create({
    data: { nombre: `Empresa de ${data.user.email ?? 'usuario'}` },
  })
  usuario = await prisma.usuario.create({
    data: {
      neonAuthId: data.user.id,
      email: data.user.email || '',
      nombre: data.user.name || data.user.email || 'Usuario',
      estado: 'ACTIVO',
      rol: 'ADMIN',
      empresaId: empresa.id,
    },
  })
}
If ALLOWLIST_REGISTRO is set in the environment, the fallback auto-creation is restricted to the comma-separated list of emails in that variable. Emails not on the list (and not in ADMIN_EMAILS) have their auto-creation blocked and the session resolves to null.

Middleware protection

src/middleware.ts runs on every request matched by the Next.js matcher. Its two responsibilities are rate limiting and authentication gating.
// src/middleware.ts — matcher config
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|auth/|invitacion|sitemap.xml|robots.txt).*)',
  ],
}
Routes that bypass authentication:
PatternReason
/auth/*Public sign-in, sign-up, password-reset pages
/invitacion*Invitation acceptance landing page (token in URL)
/_next/static, /_next/imageStatic asset pipeline
/favicon.ico, /sitemap.xml, /robots.txtSEO and browser files
/api/*API routes handle their own auth via obtenerSesion()
All other paths are passed through auth.middleware({ loginUrl: '/auth/sign-in' }), which redirects unauthenticated users to the sign-in page.

Rate limiting

Sensitive POST endpoints are rate-limited to 5 requests per minute per IP address to prevent brute-force and abuse. When Upstash Redis credentials (UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN) are configured, the limiter uses a sliding window counter in Redis. Without Redis, an in-memory Map serves as a fallback (suitable for local development). Rate-limited endpoints include:
POST /api/auth/*
POST /api/invitaciones
POST /api/invitaciones/aceptar
POST /api/usuarios
POST /api/productos
POST /api/categorias
POST /api/movimientos
POST /api/ventas
POST /api/cotizaciones
POST /api/proveedores
POST /api/ordenes-compra
POST /api/empresas
POST /api/admin/restablecer
When the limit is exceeded the middleware returns:
{
  "error": "Demasiados intentos de inicio de sesión. Espera 42 segundos."
}
with HTTP status 429 and a Retry-After-equivalent wait time in the message.
For production deployments on Vercel (serverless), always configure Upstash Redis. The in-memory fallback resets on every cold start and cannot enforce limits across concurrent function instances.

Admin email auto-promotion

Any email listed in the ADMIN_EMAILS environment variable is automatically promoted to the SUPER_ADMIN role on login. This happens inside obtenerSesion() after the Usuario record is resolved:
// src/lib/permisos.ts
if (
  usuario &&
  esEmailAdministrador(usuario.email) &&
  (usuario.rol !== 'SUPER_ADMIN' || usuario.estado !== 'ACTIVO')
) {
  usuario = await prisma.usuario.update({
    where: { id: usuario.id },
    data: { rol: 'SUPER_ADMIN', estado: 'ACTIVO' },
  })
}
esEmailAdministrador() reads the ADMIN_EMAILS environment variable, splits it on commas, and performs a case-insensitive comparison:
// src/lib/adminEmails.ts
export function esEmailAdministrador(email: string | null | undefined): boolean {
  if (!email) return false
  const lista = (process.env.ADMIN_EMAILS || '')
    .split(',')
    .map((e) => e.trim().toLowerCase())
    .filter(Boolean)
  return lista.includes(email.toLowerCase())
}
Setting ADMIN_EMAILS in .env is the recommended way to bootstrap the first platform administrator without a database migration.

Invitation flow

Users can be added to an existing company through a token-based invitation flow instead of self-registration.
1

Admin sends invitation

An ADMIN calls POST /api/invitaciones with the invitee’s email and an optional role (ADMIN or USUARIO). The request is authenticated and scoped to the admin’s own company. A unique Invitacion record is written to the database with a 7-day expiry, and an email containing the token link is dispatched via Resend.
// Request body
{
  "email": "[email protected]",
  "rol": "USUARIO"  // optional, defaults to USUARIO
}
2

Invitee receives email

The invitee receives a transactional email with a unique link containing the invitation token. The link points to the public /invitacion?token=<token> page.
3

Token validation

When the invitee opens the link, the frontend calls GET /api/invitaciones/validar?token=<token>. The endpoint uses InvitacionesService.obtenerPorToken() to verify that the invitation exists, is in PENDIENTE state, and has not expired. If the token has passed its expiraEn date the state is updated to EXPIRADA and the endpoint returns an error.
4

User completes registration

After authenticating with Neon Auth, the frontend calls POST /api/invitaciones/aceptar with the token and the Neon Auth identity:
// Request body
{
  "token": "<invitation-token>",
  "neonAuthId": "<neon-auth-user-id>",
  "email": "[email protected]",
  "nombre": "Jane Doe"
}
5

User record created and linked

InvitacionesService.aceptar() validates the token one final time, then atomically creates the Usuario record (with estado: ACTIVO and the role specified in the invitation) and marks the Invitacion as ACEPTADA with an aceptadaEn timestamp. The new user is immediately active inside the inviting company.

Invitation states

PENDIENTE

Default state. The invitation has been sent and is awaiting acceptance. Valid until expiraEn.

ACEPTADA

The invitee successfully registered and the Usuario record has been created.

EXPIRADA

The invitation was not accepted before its 7-day expiry. Set automatically when the token is validated after the deadline.

CANCELADA

An admin cancelled the invitation before it was accepted via DELETE /api/invitaciones/[id].

User states after registration

StateMeaning
PENDIENTEUser self-registered (not via invitation). An admin must approve the account before the user can operate.
ACTIVOAccount is approved. The user can sign in and use the application normally.
SUSPENDIDOAccount has been manually blocked by an admin. Sign-in succeeds but all protected routes return 403.

Environment variables

NEON_AUTH_BASE_URL
string
required
The base URL of your Neon Auth project, e.g. https://your-project.neonauth.tech/dbname/auth.
A long, random secret used to sign the session cookie. Rotate this value to invalidate all active sessions.
NEON_WEBHOOK_SECRET
string
required
The signing secret provided by Neon Auth for webhook signature verification. Set this in the Neon Auth dashboard and copy the value here.
ADMIN_EMAILS
string
Comma-separated list of email addresses that are automatically promoted to SUPER_ADMIN on login. Example: [email protected],[email protected].
UPSTASH_REDIS_REST_URL
string
REST URL of an Upstash Redis database. Required for persistent, cross-instance rate limiting in production.
UPSTASH_REDIS_REST_TOKEN
string
Authentication token for the Upstash Redis database.
ALLOWLIST_REGISTRO
string
Optional comma-separated list of emails allowed to self-register. When set, all other emails are blocked from the fallback auto-creation path unless they appear in ADMIN_EMAILS.

Build docs developers (and LLMs) love