UniEvents is designed around a clean, layered architecture: a PostgreSQL relational database modelled with Drizzle ORM holds all multi-tenant data, a service layer encapsulates every business rule independently of the web framework, and the Next.js 16 App Router exposes three distinct dashboard surfaces — each protected by JWT session cookies validated on the server. This page explains each layer in detail.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Edfermachado/proyectoSistemas/llms.txt
Use this file to discover all available pages before exploring further.
Multi-Tenant Data Model
The entire platform is organised in a strict three-level ownership hierarchy. Data at every level is isolated by foreign key — a faculty can only see its own spaces, events, and users; a university owns only its own faculties.tenantId parameter and passes it as a where clause filter. There is no row-level security at the database layer; isolation is enforced entirely in the application service layer.
Database Schema
UniEvents uses 10 tables defined insrc/db/schema.ts using Drizzle ORM’s pgTable builder, plus five PostgreSQL enum types.
Enum Types
| Enum | Values |
|---|---|
organizer_level | academico, amateur, registrado |
visibility | publico, privado |
registration_status | registrado, confirmado, pago_pendiente |
request_type | soporte_academico, patrocinio, cobertura_prensa, derechos_transmision |
attendee_type | estudiante, foraneo |
Table Reference
universities
universities
Top-level tenant grouping. Each university can have many faculties (tenants).
| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key, auto-generated |
name | varchar(255) | Unique |
slug | varchar(300) | URL-safe identifier |
description | text | |
logoUrl | varchar(500) | |
createdAt | timestamp | Defaults to now |
categories
categories
Classifies tenants (faculties) by area — e.g. “Tecnología e Innovación”, “Arte y Cultura”. Stores a Material Symbols icon name.
| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
name | varchar(255) | Unique |
slug | varchar(300) | |
icon | varchar(100) | Material Symbols icon name |
createdAt | timestamp |
tenants
tenants
Represents a faculty — the core multi-tenant unit. Every event, space, and non-superadmin user belongs to a tenant.
| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
name | varchar(255) | Unique |
slug | varchar(300) | |
description | text | |
universityId | uuid | FK → universities.id |
categoryId | uuid | FK → categories.id |
createdAt | timestamp |
users
users
All platform users — superadmins, faculty staff, and end users. Role is a plain
varchar column; tenantId is null for superadmins.| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
email | varchar(255) | Unique |
passwordHash | varchar(255) | bcryptjs hash |
name / lastName | varchar(255) | |
documentId | varchar(50) | National ID |
phone | varchar(50) | |
role | varchar(50) | superadmin, tenant_admin, event_manager, access_control, user |
tenantId | uuid | FK → tenants.id; null for superadmin |
organizerLevel | enum | academico, amateur, registrado |
createdAt | timestamp |
spaces
spaces
Physical or virtual venues owned by a tenant. Events must be assigned to a space; the service layer uses capacity and schedule data to detect conflicts.
| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
name | varchar(255) | |
capacity | integer | Max attendees |
tenantId | uuid | FK → tenants.id (not null) |
createdAt | timestamp |
events
events
The central table. Indexed on
tenantId, visibility, status, and date for fast filtered queries.| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
title | varchar(255) | |
slug | varchar(350) | Unique, auto-generated |
description | text | |
date | timestamp | Event start time |
duration | integer | Duration in minutes (default 60) |
price | decimal(10,2) | Default 0 (free) |
imageUrl | varchar(500) | Supabase or local URL |
tenantId | uuid | FK → tenants.id |
spaceId | uuid | FK → spaces.id |
capacity | integer | Nullable |
visibility | enum | publico or privado |
status | varchar(50) | pendiente, aprobado, rechazado |
requiresIpProtection | boolean | |
isFeatured | boolean | |
paymentPhone / paymentId / paymentBank | varchar | Mobile payment info for paid events |
managerId | uuid | FK → users.id |
createdAt | timestamp |
attendees
attendees
One row per registration. The
ticketToken UUID is rendered into a QR code for the attendee. Payment screenshot URL points to Supabase Storage or a local path.| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
eventId | uuid | FK → events.id |
userId | uuid | FK → users.id (nullable for guest registration) |
name / email / phone | varchar | |
status | enum | registrado, confirmado, pago_pendiente |
attendeeType | enum | estudiante, foraneo |
ticketToken | uuid | Unique; used as QR payload |
scannedAt | timestamp | Set on first valid scan |
paymentReference | varchar(50) | |
paymentScreenshotUrl | varchar(500) | |
paymentVerifiedBy | uuid | FK → users.id |
paymentVerifiedAt | timestamp | |
createdAt | timestamp |
eventRequests
eventRequests
Structured requests attached to an event (sponsorship, press coverage, etc.). The
metadata column is typed JSONB via Drizzle’s .$type<EventRequestMetadata>().| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
eventId | uuid | FK → events.id |
requestType | enum | soporte_academico, patrocinio, cobertura_prensa, derechos_transmision |
metadata | jsonb | Typed as EventRequestMetadata |
createdAt | timestamp |
systemSettings
systemSettings
Key-value store for global platform configuration. The
key column is the primary key.| Column | Type | Notes |
|---|---|---|
key | varchar(100) | Primary key |
value | text | |
description | text | |
updatedAt | timestamp |
scanLogs
scanLogs
Immutable audit log of every QR scan attempt. Records which staff member scanned which attendee at which event.
| Column | Type | Notes |
|---|---|---|
id | uuid | Primary key |
eventId | uuid | FK → events.id |
attendeeId | uuid | FK → attendees.id |
scannedBy | uuid | FK → users.id |
scannedAt | timestamp | Defaults to now |
Service Layer
UniEvents follows a clean architecture pattern where all business logic lives in static service classes insidesrc/services/, completely independent of the Next.js request/response cycle. API routes and server actions call service methods — they never query the database directly.
The six service classes are:
| Service | Responsibility |
|---|---|
EventsService | CRUD for events, space conflict detection, slug generation |
AttendeesService | Registration, payment verification, QR token lookup |
SpacesService | Space management per tenant |
TenantsService | Faculty (tenant) CRUD, scoped by university |
UniversitiesService | University CRUD for the super admin |
UsersService | User creation, password hashing, role management |
Example: EventsService.createEvent
The createEvent method illustrates the pattern — it runs a business-rule check (space conflict) before persisting, and auto-generates a unique slug, so callers never need to think about either concern.
checkSpaceConflict) queries all events in the same space within a ±1-day window and tests for overlap using the interval logic startA < endB && startB < endA, where end times are computed from date + duration * 60 * 1000.
Next.js App Router Structure
UniEvents uses the Next.js 16 App Router with three top-level dashboard segments, each protected by server-side session guards.createSession() which writes an HTTP-only JWT cookie. Each dashboard layout calls getSession() and redirects to /login if the session is absent or the role does not match.
Authentication
UniEvents implements stateless session management using signed JWT cookies via thejose library. There is no server-side session store — all session state is encoded in the cookie payload.
| Property | Value |
|---|---|
| Algorithm | HS256 (HMAC-SHA256) |
| Cookie name | session |
| Expiry | 7 days (rolling; re-issued on login) |
| HttpOnly | Yes — not accessible from JavaScript |
| Secure flag | Enabled when NODE_ENV === "production" |
| SameSite | lax |
| Payload fields | userId, role, tenantId, email, expiresAt |
getSession() is the single entry point for any server component or route handler that needs to know who the current user is. It reads the session cookie, verifies the JWT signature, and returns the decoded payload — or null if the session is missing, expired, or tampered with.
Storage
UniEvents stores two categories of user-uploaded files: event cover images and attendee payment screenshots. Both use the same two-tier storage strategy.Supabase Storage (Primary)
When the Supabase environment variables are configured,src/lib/supabase.ts initialises a Supabase client and uploads files to dedicated buckets:
| Bucket | Content | Upload Function |
|---|---|---|
events | Event cover images (WebP, 1200×800) | uploadEventImage(buffer, fileName) |
payments | Payment proof screenshots (WebP) | uploadPaymentScreenshot(file, attendeeId) |
contentType: 'image/webp' and upsert: true, then return the public URL from supabase.storage.from(bucket).getPublicUrl(fileName).
Image Processing with sharp
Before uploading event images, the application uses sharp to normalise all uploads:
- Resize to 1200 × 800 pixels (fit: cover)
- Convert to WebP format at quality 80
- Output is a
Bufferthat is passed directly touploadEventImage
Local Filesystem Fallback
IfNEXT_PUBLIC_SUPABASE_URL is not set (or the Supabase client fails to initialise), the supabase export from src/lib/supabase.ts is null:
uploadEventImage or uploadPaymentScreenshot check for a null return value and fall back to writing files into public/uploads/, serving them at a relative /uploads/filename URL. This means the full application runs without any Supabase account during local development.
Ensure the
events and payments buckets are created in your Supabase project and are set to public read access before deploying to production. The application does not auto-create buckets.