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 is a multi-tenant SaaS application where every company operates in complete isolation. The root tenant entity is the Empresa model. Every other model in the schema holds an empresaId foreign key, and every Prisma query that reads or writes business data filters on that key — making cross-company data leaks structurally impossible in normal operation.

The Empresa model

Empresa is the anchor of the entire data graph. It has a minimal footprint by design: only an auto-generated id, a human-readable nombre, and a creation timestamp. All complexity lives in the models that reference it.
// prisma/schema.prisma
model Empresa {
  id       String   @id @default(cuid())
  nombre   String
  creadoEn DateTime @default(now())

  usuarios         Usuario[]
  categorias       Categoria[]
  productos        Producto[]
  ventas           Venta[]
  cotizaciones     Cotizacion[]
  auditorias       Auditoria[]
  Movimiento       Movimiento[]
  proveedores      Proveedor[]
  ordenesCompra    OrdenCompra[]
  configuracion    ConfiguracionEmpresa?
  invitaciones     Invitacion[]
  notificaciones   Notificacion[]
  clientes         Cliente[]
  historialPrecios HistorialPrecio[]
}

Tenant isolation

Isolation is enforced in every query by extracting empresaId from the authenticated session and passing it as a where clause condition. The session helper validarAccesoEmpresa() in src/lib/permisos.ts provides the verified empresaId to every route handler:
// Pattern used in every data-access API route
import { validarAccesoEmpresa } from '@/lib/permisos'
import { prisma } from '@/lib/db'

export async function GET() {
  // empresaId comes from the session — never from request input
  const { empresaId } = await validarAccesoEmpresa()

  const productos = await prisma.producto.findMany({
    where: { empresaId }, // tenant boundary enforced here
  })

  return Response.json(productos)
}
Because empresaId is always sourced from the server-side session and never from a request body or query parameter, a user cannot escalate access to another company’s data by manipulating the request.
Never trust empresaId from a request body or URL parameter for data reads or writes. Always derive it from validarAccesoEmpresa(), which reads from the signed session cookie.

Tenant provisioning

Self-registration (new tenant)

When a user signs up without an invitation, a brand-new Empresa is provisioned for them automatically. This happens in two places, both following the same logic:
  1. Webhook (POST /api/webhooks/neon, user.created event) — the primary path; triggered by Neon Auth as soon as the user completes registration.
  2. Fallback in obtenerSesion() — runs on the first authenticated page request if the webhook has not yet arrived.
// Tenant provisioning logic (src/lib/permisos.ts fallback)
const empresa = await prisma.empresa.create({
  data: { nombre: `Empresa de ${data.user.email ?? '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',       // founder is always ADMIN of their own company
    empresaId: empresa.id,
  },
})
The founding user is created with rol: 'ADMIN' and estado: 'ACTIVO' so they can immediately configure their company without waiting for an approval step.

Invitation-based joining (existing tenant)

Users who receive an invitation via POST /api/invitaciones join an existing Empresa rather than creating a new one. When POST /api/invitaciones/aceptar is called with a valid token, the new Usuario record is created with the empresaId stored in the Invitacion row:
// src/services/InvitacionesService.ts
await prisma.usuario.create({
  data: {
    neonAuthId,
    email,
    nombre,
    empresaId: invitacion.empresaId, // joined to the inviting company
    estado: 'ACTIVO',
    rol: invitacion.rol,             // role specified at invitation time
  },
})
Invited users start as ACTIVO immediately — no admin approval is needed because the invitation itself is the explicit approval act.

Company configuration

Each Empresa has a one-to-one ConfiguracionEmpresa record that stores localisation and branding settings:
FieldDefaultDescription
moneda"COP"ISO 4217 currency code used in prices and reports
simboloMoneda"$"Symbol rendered in the UI next to monetary values
impuestos0.19Default tax rate (19 %) applied to sales
logoUrlnullURL of the company logo, uploaded by the admin
direccionnullPhysical address, printed on invoices and quotes
telefononullContact phone number
emailnullContact email shown on documents
nombrePersonalizadonullDisplay name override (defaults to Empresa.nombre)

Configuration endpoints

# Read the current company configuration
GET /api/empresa/configuracion

# Update one or more configuration fields (ADMIN only)
PUT /api/empresa/configuracion
// Example PUT body
{
  "moneda": "USD",
  "simboloMoneda": "$",
  "impuestos": 0.08,
  "nombrePersonalizado": "Acme Corp"
}

Company member management

# List all users in the authenticated company
GET /api/empresa/usuarios

Onboarding flow

Newly provisioned tenants are directed to an onboarding wizard at /onboarding. Once the wizard is completed, the frontend calls the onboarding endpoint to persist the flag:
POST /api/usuarios/onboarding
This sets onboardingCompletado = true on the Usuario record. Application logic can read this flag to decide whether to show the onboarding wizard on subsequent visits.

Data model ownership

Every model listed below holds a required empresaId column that references Empresa. Deleting an Empresa cascades to all of them (Prisma onDelete: Cascade).
ModelWhat it represents
UsuarioCompany members and their roles/permissions
CategoriaProduct categories unique to the company
ProductoProduct catalogue with stock levels and pricing
MovimientoIndividual stock movements (entries and exits)
VentaSales transactions processed at the terminal
CotizacionCustomer quotes that can be converted to sales
OrdenCompraPurchase orders sent to suppliers
ProveedorSupplier directory
ClienteCustomer directory
AuditoriaImmutable audit log of all actions
NotificacionIn-app notifications (low stock, events, etc.)
HistorialPrecioPrice change log per product

SUPER_ADMIN cross-company access

Users with the SUPER_ADMIN role are not bound to a single empresaId. They can browse and manage all tenants through the platform admin panel at /admin/empresas. The following API endpoints are reserved for SUPER_ADMIN:
# List all companies on the platform
GET /api/empresas

# Create a company manually (e.g. for enterprise provisioning)
POST /api/empresas

# Read, update, or delete a specific company
GET    /api/empresas/[id]
PUT    /api/empresas/[id]
DELETE /api/empresas/[id]
SUPER_ADMIN access is granted automatically to emails listed in the ADMIN_EMAILS environment variable. No database migration or manual role update is needed — the promotion happens on login inside obtenerSesion(). See Authentication for details.

Tenant lifecycle summary

User signs up (no invitation)
  └─ Neon Auth webhook fires user.created
       └─ WebhooksService creates Empresa + Usuario (ADMIN, ACTIVO)
            └─ User lands on /onboarding
                 └─ POST /api/usuarios/onboarding → onboardingCompletado = true

Admin invites colleague
  └─ POST /api/invitaciones (email, optional rol)
       └─ Invitacion created (PENDIENTE, 7-day expiry)
            └─ Email sent via Resend
                 └─ Invitee clicks link → /invitacion?token=...
                      └─ POST /api/invitaciones/aceptar
                           └─ Usuario created (linked to existing Empresa, ACTIVO)

Build docs developers (and LLMs) love