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 option | Value | Meaning |
|---|
max | 20 | Maximum simultaneous client connections |
idleTimeoutMillis | 30000 | Idle connections are released after 30 s |
connectionTimeoutMillis | 2000 | Acquiring a connection times out after 2 s |
Tables
users
Stores every operator and administrator who can log in to the system.
| Column | Type | Constraints | Description |
|---|
id | SERIAL | PRIMARY KEY | Auto-incrementing identifier |
name | VARCHAR(100) | NOT NULL | Full display name |
email | VARCHAR(100) | UNIQUE, NOT NULL | Login email address |
password_hash | VARCHAR(255) | NOT NULL | bcrypt hash of the user’s password |
role | VARCHAR(20) | NOT NULL, DEFAULT 'admin_operator' | One of super_admin or admin_operator |
is_active | BOOLEAN | DEFAULT true | Whether the account is enabled |
created_at | TIMESTAMP WITH TIME ZONE | DEFAULT NOW() | Row creation timestamp |
updated_at | TIMESTAMP WITH TIME ZONE | DEFAULT 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.
| Column | Type | Constraints | Description |
|---|
id | SERIAL | PRIMARY KEY | Auto-incrementing identifier |
status | VARCHAR(20) | NOT NULL, DEFAULT 'available' | One of available, active, expired, or disabled |
created_by | INTEGER | FK → users.id ON DELETE SET NULL | The user who created this QR code |
created_at | TIMESTAMP WITH TIME ZONE | DEFAULT NOW() | Row creation timestamp |
updated_at | TIMESTAMP WITH TIME ZONE | DEFAULT 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.
| Column | Type | Constraints | Description |
|---|
id | SERIAL | PRIMARY KEY | Auto-incrementing identifier |
qr_id | INTEGER | NOT NULL, FK → qr_codes.id ON DELETE CASCADE | The QR code used for this permission |
enabled_by | INTEGER | FK → users.id ON DELETE SET NULL | Operator who activated the permission |
received_by | VARCHAR(150) | — | Name of the person who received the exit pass |
returned_by | INTEGER | FK → users.id ON DELETE SET NULL | Operator who registered the return |
allowed_minutes | INTEGER | NOT NULL, DEFAULT 15 | Maximum allotted time in minutes |
exit_time | TIMESTAMP WITH TIME ZONE | — | Timestamp when the person exited |
return_time | TIMESTAMP WITH TIME ZONE | — | Timestamp when the person returned |
time_used_minutes | DECIMAL(10, 2) | — | Actual elapsed time in minutes |
delay_minutes | DECIMAL(10, 2) | — | Excess time beyond allowed_minutes (negative = early) |
is_compliant | BOOLEAN | — | true if returned within the allotted time |
notes | TEXT | — | Optional free-text remarks |
created_at | TIMESTAMP WITH TIME ZONE | DEFAULT NOW() | Row creation timestamp |
updated_at | TIMESTAMP WITH TIME ZONE | DEFAULT 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 name | Table | Column(s) | Purpose |
|---|
idx_qr_codes_status | qr_codes | status | Fast lookup of available / active codes |
idx_qr_codes_created_by | qr_codes | created_by | Filter codes by creating operator |
idx_permissions_qr_id | permissions | qr_id | Join permissions back to their QR code |
idx_permissions_enabled_by | permissions | enabled_by | Filter permissions by activating operator |
idx_permissions_exit_time | permissions | exit_time | Time-range queries on exit events |
idx_permissions_created_at | permissions | created_at | Chronological 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.
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;"
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 resource | Details |
|---|
admin@permisosqr.com | Role super_admin — full system access |
operador@permisosqr.com | Role admin_operator — day-to-day operations |
20 × qr_codes | Status 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.