Skip to main content

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.

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.

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.
┌─────────────────────────────────────────┐
│             universities                │
│  id · name · slug · description         │
└───────────────┬─────────────────────────┘
                │ 1 : N
┌───────────────▼─────────────────────────┐
│               tenants  (faculties)      │
│  id · name · slug · universityId        │
│            · categoryId                 │
└──────┬────────────────┬─────────────────┘
       │ 1 : N          │ 1 : N
┌──────▼──────┐  ┌──────▼──────────────────┐
│   spaces    │  │         events          │
│ id · name   │  │ id · title · date       │
│ · capacity  │  │ · price · spaceId       │
│ · tenantId  │  │ · tenantId · managerId  │
└─────────────┘  └──────────┬─────────────┘
                             │ 1 : N
                 ┌───────────▼─────────────┐
                 │        attendees        │
                 │ id · ticketToken · status│
                 │ · paymentScreenshotUrl  │
                 └───────────┬─────────────┘
                             │ 1 : N
                 ┌───────────▼─────────────┐
                 │        scan_logs        │
                 │ id · scannedBy · scannedAt│
                 └─────────────────────────┘
Multi-tenancy enforcement is implemented at the query level — every service method that reads tenant-scoped data accepts a 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 in src/db/schema.ts using Drizzle ORM’s pgTable builder, plus five PostgreSQL enum types.

Enum Types

EnumValues
organizer_levelacademico, amateur, registrado
visibilitypublico, privado
registration_statusregistrado, confirmado, pago_pendiente
request_typesoporte_academico, patrocinio, cobertura_prensa, derechos_transmision
attendee_typeestudiante, foraneo

Table Reference

Top-level tenant grouping. Each university can have many faculties (tenants).
ColumnTypeNotes
iduuidPrimary key, auto-generated
namevarchar(255)Unique
slugvarchar(300)URL-safe identifier
descriptiontext
logoUrlvarchar(500)
createdAttimestampDefaults to now
Classifies tenants (faculties) by area — e.g. “Tecnología e Innovación”, “Arte y Cultura”. Stores a Material Symbols icon name.
ColumnTypeNotes
iduuidPrimary key
namevarchar(255)Unique
slugvarchar(300)
iconvarchar(100)Material Symbols icon name
createdAttimestamp
Represents a faculty — the core multi-tenant unit. Every event, space, and non-superadmin user belongs to a tenant.
ColumnTypeNotes
iduuidPrimary key
namevarchar(255)Unique
slugvarchar(300)
descriptiontext
universityIduuidFK → universities.id
categoryIduuidFK → categories.id
createdAttimestamp
All platform users — superadmins, faculty staff, and end users. Role is a plain varchar column; tenantId is null for superadmins.
ColumnTypeNotes
iduuidPrimary key
emailvarchar(255)Unique
passwordHashvarchar(255)bcryptjs hash
name / lastNamevarchar(255)
documentIdvarchar(50)National ID
phonevarchar(50)
rolevarchar(50)superadmin, tenant_admin, event_manager, access_control, user
tenantIduuidFK → tenants.id; null for superadmin
organizerLevelenumacademico, amateur, registrado
createdAttimestamp
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.
ColumnTypeNotes
iduuidPrimary key
namevarchar(255)
capacityintegerMax attendees
tenantIduuidFK → tenants.id (not null)
createdAttimestamp
The central table. Indexed on tenantId, visibility, status, and date for fast filtered queries.
ColumnTypeNotes
iduuidPrimary key
titlevarchar(255)
slugvarchar(350)Unique, auto-generated
descriptiontext
datetimestampEvent start time
durationintegerDuration in minutes (default 60)
pricedecimal(10,2)Default 0 (free)
imageUrlvarchar(500)Supabase or local URL
tenantIduuidFK → tenants.id
spaceIduuidFK → spaces.id
capacityintegerNullable
visibilityenumpublico or privado
statusvarchar(50)pendiente, aprobado, rechazado
requiresIpProtectionboolean
isFeaturedboolean
paymentPhone / paymentId / paymentBankvarcharMobile payment info for paid events
managerIduuidFK → users.id
createdAttimestamp
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.
ColumnTypeNotes
iduuidPrimary key
eventIduuidFK → events.id
userIduuidFK → users.id (nullable for guest registration)
name / email / phonevarchar
statusenumregistrado, confirmado, pago_pendiente
attendeeTypeenumestudiante, foraneo
ticketTokenuuidUnique; used as QR payload
scannedAttimestampSet on first valid scan
paymentReferencevarchar(50)
paymentScreenshotUrlvarchar(500)
paymentVerifiedByuuidFK → users.id
paymentVerifiedAttimestamp
createdAttimestamp
Structured requests attached to an event (sponsorship, press coverage, etc.). The metadata column is typed JSONB via Drizzle’s .$type<EventRequestMetadata>().
ColumnTypeNotes
iduuidPrimary key
eventIduuidFK → events.id
requestTypeenumsoporte_academico, patrocinio, cobertura_prensa, derechos_transmision
metadatajsonbTyped as EventRequestMetadata
createdAttimestamp
Key-value store for global platform configuration. The key column is the primary key.
ColumnTypeNotes
keyvarchar(100)Primary key
valuetext
descriptiontext
updatedAttimestamp
Immutable audit log of every QR scan attempt. Records which staff member scanned which attendee at which event.
ColumnTypeNotes
iduuidPrimary key
eventIduuidFK → events.id
attendeeIduuidFK → attendees.id
scannedByuuidFK → users.id
scannedAttimestampDefaults to now

Service Layer

UniEvents follows a clean architecture pattern where all business logic lives in static service classes inside src/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:
ServiceResponsibility
EventsServiceCRUD for events, space conflict detection, slug generation
AttendeesServiceRegistration, payment verification, QR token lookup
SpacesServiceSpace management per tenant
TenantsServiceFaculty (tenant) CRUD, scoped by university
UniversitiesServiceUniversity CRUD for the super admin
UsersServiceUser 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.
// src/services/events.service.ts

static async createEvent(data: {
  title: string;
  date: Date;
  duration: number;
  price?: string;
  tenantId: string;
  spaceId: string;
  description?: string;
  imageUrl?: string | null;
  capacity?: number;
  visibility?: "publico" | "privado";
  requiresIpProtection?: boolean;
  status?: string;
  paymentPhone?: string;
  paymentId?: string;
  paymentBank?: string;
  managerId?: string;
}) {
  // Business rule: no double-booking a space
  const hasConflict = await this.checkSpaceConflict(
    data.spaceId,
    data.date,
    data.duration
  );

  if (hasConflict) {
    throw new Error("CONF_001: El espacio ya está reservado para esa fecha y hora.");
  }

  // Auto-generate a unique, URL-safe slug from the title
  const slug = await generateUniqueSlug("events", data.title);

  const [newEvent] = await db
    .insert(events)
    .values({ ...data, slug })
    .returning();

  return newEvent;
}
The conflict check (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.
src/app/
├── (public)/                  # Public-facing app (no auth required)
│   ├── page.tsx               # Homepage — featured events
│   ├── events/[slug]/         # Event detail & registration
│   └── universities/          # University & faculty browser

├── admin/                     # Super Admin dashboard
│   ├── layout.tsx             # Guards: role === 'superadmin'
│   ├── universities/          # Manage universities
│   ├── tenants/               # Manage all faculties
│   ├── users/                 # Manage all users
│   ├── categories/            # Manage event categories
│   └── settings/              # System settings (systemSettings table)

├── faculty-admin/             # Faculty Admin / Event Manager dashboard
│   ├── layout.tsx             # Guards: role in ['tenant_admin', 'event_manager', 'access_control']
│   ├── events/                # Create, edit, and manage tenant events
│   ├── attendees/             # View registrations, verify payments
│   ├── spaces/                # Manage tenant spaces
│   └── scanner/               # QR code scanner (access_control role)

└── login/                     # Shared login page for all roles
After a successful login, the server calls 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 the jose library. There is no server-side session store — all session state is encoded in the cookie payload.
// src/lib/auth.ts (simplified)

const secretKey = process.env.JWT_SECRET || "uni-events-super-secret-key-32-chars!!";
const encodedKey = new TextEncoder().encode(secretKey);

// Cookie payload shape:
// { userId, role, tenantId, email, expiresAt }

export async function createSession(
  userId: string,
  role: string,
  tenantId?: string | null,
  email?: string
) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
  const session = await new SignJWT({ userId, role, tenantId, email, expiresAt })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("7d")
    .sign(encodedKey);

  cookieStore.set("session", session, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    expires: expiresAt,
    sameSite: "lax",
    path: "/",
  });
}

export async function getSession() {
  const sessionCookie = cookieStore.get("session")?.value;
  return await decrypt(sessionCookie); // returns null if invalid or expired
}
Key properties of the auth system:
PropertyValue
AlgorithmHS256 (HMAC-SHA256)
Cookie namesession
Expiry7 days (rolling; re-issued on login)
HttpOnlyYes — not accessible from JavaScript
Secure flagEnabled when NODE_ENV === "production"
SameSitelax
Payload fieldsuserId, 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:
BucketContentUpload Function
eventsEvent cover images (WebP, 1200×800)uploadEventImage(buffer, fileName)
paymentsPayment proof screenshots (WebP)uploadPaymentScreenshot(file, attendeeId)
Both functions upload with 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 Buffer that is passed directly to uploadEventImage
This keeps storage costs predictable and ensures consistent image dimensions across all event cards.

Local Filesystem Fallback

If NEXT_PUBLIC_SUPABASE_URL is not set (or the Supabase client fails to initialise), the supabase export from src/lib/supabase.ts is null:
// src/lib/supabase.ts
export const supabase = (supabaseUrl && supabaseKey)
  ? createClient(supabaseUrl, supabaseKey)
  : null; // triggers local fallback in the API route
API routes that call 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.

Build docs developers (and LLMs) love