Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/edgar2420/QrPermision/llms.txt

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

PermisosQR stores all its data in a PostgreSQL database composed of three tables — users, qr_codes, and permissions — that model the full lifecycle of a QR-based exit/entry permission. The schema is declared in base/schema.sql using CREATE TABLE IF NOT EXISTS, so it is safe to run against both empty and already-initialised databases. Every table carries created_at and updated_at timestamp columns, and a shared PL/pgSQL trigger function keeps updated_at in sync on every row mutation automatically.

Database Connection

The backend uses the pg library’s Pool class to manage a pool of up to 20 concurrent connections. Connection parameters are read exclusively from environment variables (see Environment Variables for the full reference):
const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME || 'permisosqr',
  user: process.env.DB_USER || 'postgres',
  password: process.env.DB_PASSWORD || '',
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});
Pool optionValueMeaning
max20Maximum simultaneous client connections
idleTimeoutMillis30000Idle connections are released after 30 s
connectionTimeoutMillis2000Acquiring a connection times out after 2 s

Tables

users

Stores every operator and administrator who can log in to the system.
ColumnTypeConstraintsDescription
idSERIALPRIMARY KEYAuto-incrementing identifier
nameVARCHAR(100)NOT NULLFull display name
emailVARCHAR(100)UNIQUE, NOT NULLLogin email address
password_hashVARCHAR(255)NOT NULLbcrypt hash of the user’s password
roleVARCHAR(20)NOT NULL, DEFAULT 'admin_operator'One of super_admin or admin_operator
is_activeBOOLEANDEFAULT trueWhether the account is enabled
created_atTIMESTAMP WITH TIME ZONEDEFAULT NOW()Row creation timestamp
updated_atTIMESTAMP WITH TIME ZONEDEFAULT NOW()Last modification timestamp (auto-updated)
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(100) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  role VARCHAR(20) NOT NULL DEFAULT 'admin_operator'
    CHECK (role IN ('super_admin', 'admin_operator')),
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

qr_codes

Represents individual QR tokens that can be assigned to a permission session. Each code has a status that transitions through its lifecycle.
ColumnTypeConstraintsDescription
idSERIALPRIMARY KEYAuto-incrementing identifier
statusVARCHAR(20)NOT NULL, DEFAULT 'available'One of available, active, expired, or disabled
created_byINTEGERFK → users.id ON DELETE SET NULLThe user who created this QR code
created_atTIMESTAMP WITH TIME ZONEDEFAULT NOW()Row creation timestamp
updated_atTIMESTAMP WITH TIME ZONEDEFAULT NOW()Last modification timestamp (auto-updated)
CREATE TABLE IF NOT EXISTS qr_codes (
  id SERIAL PRIMARY KEY,
  status VARCHAR(20) NOT NULL DEFAULT 'available'
    CHECK (status IN ('available', 'active', 'expired', 'disabled')),
  created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

permissions

The core table. Each row records a single exit-and-return permission event linked to a QR code, including timing data and compliance calculations.
ColumnTypeConstraintsDescription
idSERIALPRIMARY KEYAuto-incrementing identifier
qr_idINTEGERNOT NULL, FK → qr_codes.id ON DELETE CASCADEThe QR code used for this permission
enabled_byINTEGERFK → users.id ON DELETE SET NULLOperator who activated the permission
received_byVARCHAR(150)Name of the person who received the exit pass
returned_byINTEGERFK → users.id ON DELETE SET NULLOperator who registered the return
allowed_minutesINTEGERNOT NULL, DEFAULT 15Maximum allotted time in minutes
exit_timeTIMESTAMP WITH TIME ZONETimestamp when the person exited
return_timeTIMESTAMP WITH TIME ZONETimestamp when the person returned
time_used_minutesDECIMAL(10, 2)Actual elapsed time in minutes
delay_minutesDECIMAL(10, 2)Excess time beyond allowed_minutes (negative = early)
is_compliantBOOLEANtrue if returned within the allotted time
notesTEXTOptional free-text remarks
created_atTIMESTAMP WITH TIME ZONEDEFAULT NOW()Row creation timestamp
updated_atTIMESTAMP WITH TIME ZONEDEFAULT NOW()Last modification timestamp (auto-updated)
CREATE TABLE IF NOT EXISTS permissions (
  id SERIAL PRIMARY KEY,
  qr_id INTEGER NOT NULL REFERENCES qr_codes(id) ON DELETE CASCADE,
  enabled_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
  received_by VARCHAR(150),
  returned_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
  allowed_minutes INTEGER NOT NULL DEFAULT 15,
  exit_time TIMESTAMP WITH TIME ZONE,
  return_time TIMESTAMP WITH TIME ZONE,
  time_used_minutes DECIMAL(10, 2),
  delay_minutes DECIMAL(10, 2),
  is_compliant BOOLEAN,
  notes TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Indexes

Six indexes are created to accelerate the most common query patterns — filtering permissions by QR code, looking up codes by status, and generating time-range reports:
Index nameTableColumn(s)Purpose
idx_qr_codes_statusqr_codesstatusFast lookup of available / active codes
idx_qr_codes_created_byqr_codescreated_byFilter codes by creating operator
idx_permissions_qr_idpermissionsqr_idJoin permissions back to their QR code
idx_permissions_enabled_bypermissionsenabled_byFilter permissions by activating operator
idx_permissions_exit_timepermissionsexit_timeTime-range queries on exit events
idx_permissions_created_atpermissionscreated_atChronological report sorting
CREATE INDEX IF NOT EXISTS idx_qr_codes_status       ON qr_codes(status);
CREATE INDEX IF NOT EXISTS idx_qr_codes_created_by   ON qr_codes(created_by);
CREATE INDEX IF NOT EXISTS idx_permissions_qr_id     ON permissions(qr_id);
CREATE INDEX IF NOT EXISTS idx_permissions_enabled_by ON permissions(enabled_by);
CREATE INDEX IF NOT EXISTS idx_permissions_exit_time  ON permissions(exit_time);
CREATE INDEX IF NOT EXISTS idx_permissions_created_at ON permissions(created_at);

Auto-Update Triggers

A single PL/pgSQL function, update_updated_at_column(), sets NEW.updated_at = NOW() on every BEFORE UPDATE event. It is attached to all three tables so you never need to set updated_at manually in application code:
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_users_updated_at
  BEFORE UPDATE ON users
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_qr_codes_updated_at
  BEFORE UPDATE ON qr_codes
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_permissions_updated_at
  BEFORE UPDATE ON permissions
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

Applying the Schema

All CREATE TABLE and CREATE INDEX statements use IF NOT EXISTS, so running the migration script on a database that already has the schema is completely safe — it will not drop or overwrite existing data.
1

Create the database

Make sure a PostgreSQL server is running and create the target database if it does not yet exist:
psql -U postgres -c "CREATE DATABASE permisosqr;"
2

Run the migration

Run psql directly from the repository root (or adjust the path to match your working directory):
psql -U postgres -d permisosqr -f base/schema.sql
The npm run db:migrate script in backend/package.json references ../database/schema.sql, but that path does not exist in the repository — the SQL files live under base/, not database/. Use the psql command above instead.

Seed Data

base/seed.sql inserts two test accounts and 20 QR codes tagged as available, giving you a working dataset to log in and explore the UI immediately after a fresh install.
Seed resourceDetails
admin@permisosqr.comRole super_admin — full system access
operador@permisosqr.comRole admin_operator — day-to-day operations
20 × qr_codesStatus available, owned by user id 1 (super_admin)
Both users share the same bcrypt-hashed password defined in the seed file. Both INSERT statements use ON CONFLICT DO NOTHING / a WHERE NOT EXISTS guard so re-running the seed is idempotent.
psql -U postgres -d permisosqr -f base/seed.sql
The npm run db:seed script in backend/package.json references ../database/seed.sql, which does not exist. Use the psql command above instead.
-- Seed preview (from base/seed.sql)
INSERT INTO users (name, email, password_hash, role) VALUES
  ('Edgar Rojas Apaza',   'admin@permisosqr.com',    '$2b$10$...', 'super_admin'),
  ('Operador Principal',  'operador@permisosqr.com', '$2b$10$...', 'admin_operator')
ON CONFLICT (email) DO NOTHING;

INSERT INTO qr_codes (status, created_by)
SELECT 'available', 1 FROM generate_series(1, 20)
WHERE NOT EXISTS (SELECT 1 FROM qr_codes LIMIT 1);
After seeding, visit GET /api/health to confirm the database is reachable, then log in via the frontend with one of the seeded accounts to verify the full stack is working end-to-end.

Build docs developers (and LLMs) love