19 KiB
🗄️ Database Schema Documentation
Complete database documentation for the GoHorseJobs platform.
Last Updated: 2026-01-03 Database: PostgreSQL 16+ (Local
postgres-maincontainer) Connection: Internalgohorsejobs_devdatabase viaweb_proxynetwork ID Strategy: UUID v7 for core tables, SERIAL for reference tables
Migrations: 30 SQL files inbackend/migrations/
<EFBFBD>️ Development Environment Structure
The development environment (apolo server) uses a Local Containerized Strategy to ensure isolation and speed.
🏗️ Topology
graph TD
subgraph VPS ["Apolo Server (VPS)"]
subgraph Net ["Docker Network: web_proxy"]
PG[("postgres-main")]
BE["Backend API"]
BO["Backoffice"]
SE["Seeder API"]
end
Traefik["Traefik Proxy"]
end
Traefik --> BE
Traefik --> BO
Traefik --> SE
BE -- "internal:5432" --> PG
BO -- "internal:5432" --> PG
SE -- "internal:5432" --> PG
style PG fill:#336791,stroke:#fff,stroke-width:2px,color:#fff
🔌 Connection Details
All services connect to the database via the internal Docker network.
| Parameter | Value | Notes |
|---|---|---|
| Host | postgres-main |
Internal Container Hostname |
| Port | 5432 |
Internal Port |
| Database | gohorsejobs_dev |
Dedicated Dev DB (Isolated from main_db) |
| User | yuki |
Owner of public schema |
| Network | web_proxy |
Shared Bridge Network |
| SSL Mode | disable |
Internal traffic is unencrypted |
🚀 Access & Management
Since the database runs inside a container and is not exposed to the public internet, use the following methods for access:
1. CLI Access (via SSH)
# Connect to PostgreSQL shell
ssh root@apolo 'podman exec -it postgres-main psql -U yuki -d gohorsejobs_dev'
2. Run Migrations Migrations are applied using the Backend service or manually piped:
# Manual Pipe (from local machine)
cat backend/migrations/*.sql | ssh root@apolo 'podman exec -i postgres-main psql -U yuki -d gohorsejobs_dev'
3. Seeding Data Trigger the Seeder API (running locally) to populate data:
curl -X POST https://seeder.gohorsejobs.com/seed
<EFBFBD>📊 Entity Relationship Diagram
erDiagram
%% Core Entities
users ||--o{ user_companies : "belongs to"
users ||--o{ user_roles : "has roles"
users ||--o{ applications : "submits"
users ||--o{ favorite_jobs : "saves"
users ||--o{ notifications : "receives"
users ||--o{ tickets : "opens"
users ||--o{ ticket_messages : "sends"
users ||--o{ login_audits : "generates"
users ||--o{ activity_logs : "generates"
companies ||--o{ user_companies : "has members"
companies ||--o{ jobs : "posts"
jobs ||--o{ applications : "receives"
jobs ||--o{ favorite_jobs : "saved by"
jobs ||--o{ job_payments : "has payments"
regions ||--o{ cities : "contains"
regions ||--o{ companies : "located in"
regions ||--o{ jobs : "located in"
tickets ||--o{ ticket_messages : "contains"
%% Entities
users {
int id PK "SERIAL"
varchar identifier UK "login"
varchar password_hash
varchar role "enum"
varchar full_name
varchar email
varchar name
int tenant_id FK "nullable"
varchar status
}
user_roles {
int user_id PK,FK
varchar role PK "composite key"
}
companies {
int id PK "SERIAL"
varchar name
varchar slug UK
varchar type
varchar document
text address
int region_id FK
varchar email
varchar website
boolean verified
boolean active
}
jobs {
int id PK "SERIAL"
int company_id FK
int created_by FK
varchar title
text description
decimal salary_min
decimal salary_max
varchar salary_type
varchar currency
varchar employment_type
varchar work_mode
varchar status
boolean is_featured
}
applications {
int id PK "SERIAL"
int job_id FK
int user_id FK "nullable"
varchar status
text message
varchar resume_url
}
regions {
int id PK "SERIAL"
varchar name
varchar country_code
varchar code
}
cities {
int id PK "SERIAL"
int region_id FK
varchar name
}
notifications {
uuid id PK
int user_id FK
varchar type
varchar title
text message
boolean read
}
job_payments {
uuid id PK
int job_id FK
int user_id FK
decimal amount
varchar status
varchar stripe_session_id
}
tickets {
uuid id PK
int user_id FK
varchar subject
varchar status
varchar priority
}
ticket_messages {
uuid id PK
uuid ticket_id FK
int user_id FK
text message
boolean is_staff
}
job_posting_prices {
int id PK
varchar name
decimal price
int duration_days
}
login_audits {
int id PK
int user_id FK
varchar identifier
boolean success
varchar ip_address
}
activity_logs {
int id PK
int user_id FK
varchar entity_type
varchar action
jsonb details
}
🏗️ Table Overview
| Table | ID Type | Description | Migration |
|---|---|---|---|
users |
UUID v7 | System users (candidates, recruiters, admins) | 001/021 |
user_roles |
Composite | Additional roles per user | 020 |
companies |
UUID v7 | Employer organizations | 002/021 |
user_companies |
UUID v7 | User ↔ Company mapping | 003/021 |
regions |
SERIAL | States/Provinces/Prefectures | 004 |
cities |
SERIAL | Cities within regions | 004 |
jobs |
UUID v7 | Job postings | 005/021 |
applications |
UUID v7 | Job applications | 006/021 |
favorite_jobs |
SERIAL | Saved jobs by users | 007 |
password_resets |
SERIAL | Password reset tokens | 008 |
notifications |
UUID v7 | User notifications | 016 |
tickets |
UUID v7 | Support tickets | 017 |
ticket_messages |
UUID v7 | Ticket messages | 017 |
job_payments |
UUID v7 | Payment records | 019 |
job_posting_prices |
SERIAL | Pricing configuration | 019 |
login_audits |
SERIAL | Login audit trail | 013 |
activity_logs |
SERIAL | Activity logging | 013 |
📋 Core Tables
users
System users including Candidates, Recruiters, and Admins.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
identifier VARCHAR(100) UNIQUE NOT NULL, -- Login (username/email)
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL, -- 'superadmin'|'admin'|'recruiter'|'candidate'
-- Profile
full_name VARCHAR(255),
email VARCHAR(255),
name VARCHAR(255),
phone VARCHAR(30),
-- Candidate Profile Fields (015)
city VARCHAR(100),
state VARCHAR(50),
title VARCHAR(100),
experience VARCHAR(100),
skills TEXT[],
bio TEXT,
objective TEXT,
-- Multi-tenancy (020)
tenant_id INT REFERENCES companies(id),
status VARCHAR(20) DEFAULT 'active',
-- Settings
language VARCHAR(5) DEFAULT 'en',
active BOOLEAN DEFAULT true,
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login_at TIMESTAMP
);
Roles:
superadmin- Platform administratoradmin- Company administratorrecruiter- Job poster/recruitercandidate- Candidate/job seeker
user_roles
Additional roles per user (for multi-role support).
CREATE TABLE user_roles (
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL,
PRIMARY KEY (user_id, role)
);
companies
Employer organizations that post jobs.
CREATE TABLE companies (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL, -- URL-friendly name
type VARCHAR(50) DEFAULT 'company', -- 'company'|'agency'|'system'
document VARCHAR(100), -- CNPJ/Tax ID
-- Location
address TEXT,
region_id INT REFERENCES regions(id),
city_id INT REFERENCES cities(id),
-- Contact
phone VARCHAR(30),
email VARCHAR(255),
website VARCHAR(255),
-- Branding
logo_url TEXT,
description TEXT, -- JSON or plain text
-- Status
active BOOLEAN DEFAULT true,
verified BOOLEAN DEFAULT false,
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
jobs
Job postings created by companies.
CREATE TABLE jobs (
id SERIAL PRIMARY KEY,
company_id INT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
created_by INT NOT NULL REFERENCES users(id),
-- Job Details
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
-- Salary
salary_min DECIMAL(12,2),
salary_max DECIMAL(12,2),
salary_type VARCHAR(20), -- 'hourly'|'daily'|'weekly'|'monthly'|'yearly'
currency VARCHAR(3), -- 'BRL'|'USD'|'EUR'|'GBP'|'JPY'
-- Employment
employment_type VARCHAR(30), -- 'full-time'|'part-time'|'contract'|'dispatch'|...
work_mode VARCHAR(10), -- 'onsite'|'hybrid'|'remote'
working_hours VARCHAR(100),
-- Location
location VARCHAR(255),
region_id INT REFERENCES regions(id),
city_id INT REFERENCES cities(id),
-- Requirements & Benefits
requirements JSONB, -- Array of skills/requirements
benefits JSONB, -- Array of benefits
-- Visa & Language
visa_support BOOLEAN DEFAULT false,
language_level VARCHAR(20), -- 'N5'|'N4'|'N3'|'N2'|'N1'|'beginner'|'none'
-- Status
status VARCHAR(20) DEFAULT 'open', -- 'draft'|'open'|'closed'|'published'|...
is_featured BOOLEAN DEFAULT false,
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Status Values:
draft- Not published yetopen- Accepting applicationspublished- Live and visiblepaused- Temporarily hiddenclosed- No longer acceptingexpired- Past expiration datearchived- Archived by employerreported- Flagged for reviewreview- Under admin review
applications
Job applications from candidates.
CREATE TABLE applications (
id SERIAL PRIMARY KEY,
job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
user_id INT REFERENCES users(id), -- Nullable for guest applications
-- Applicant Info
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
phone VARCHAR(30),
line_id VARCHAR(100),
whatsapp VARCHAR(30),
-- Application Content
message TEXT,
resume_url VARCHAR(500),
documents JSONB,
-- Status
status VARCHAR(20) DEFAULT 'pending', -- 'pending'|'reviewed'|'shortlisted'|'rejected'|'hired'
notes TEXT, -- Recruiter notes
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
🌍 Geographic Tables
regions
States, provinces, or prefectures.
CREATE TABLE regions (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
country_code VARCHAR(2) NOT NULL, -- 'BR'|'US'|'JP'
code VARCHAR(10) NOT NULL -- 'SP'|'CA'|'13'
);
Seeded Regions:
- 🇧🇷 Brazil: São Paulo (SP), Rio de Janeiro (RJ), Minas Gerais (MG)
- 🇺🇸 USA: California (CA), New York (NY), Texas (TX)
- 🇯🇵 Japan: Tokyo (13), Osaka (27)
cities
Cities within regions.
CREATE TABLE cities (
id SERIAL PRIMARY KEY,
region_id INT NOT NULL REFERENCES regions(id),
name VARCHAR(100) NOT NULL
);
💰 Payment Tables
job_payments (UUID)
Payment records for job postings.
CREATE TABLE job_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
user_id INT REFERENCES users(id) ON DELETE SET NULL,
-- Stripe
stripe_session_id VARCHAR(255),
stripe_payment_intent VARCHAR(255),
stripe_customer_id VARCHAR(255),
-- Amount
amount DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
-- Status
status VARCHAR(20) DEFAULT 'pending', -- 'pending'|'completed'|'failed'|'refunded'|'expired'
-- Billing
billing_type VARCHAR(20), -- 'company'|'individual'
billing_name VARCHAR(255),
billing_email VARCHAR(255),
-- Job Details
duration_days INT DEFAULT 30,
is_featured BOOLEAN DEFAULT false,
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
paid_at TIMESTAMP,
expires_at TIMESTAMP
);
job_posting_prices
Pricing configuration for job postings.
CREATE TABLE job_posting_prices (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
price DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
duration_days INT DEFAULT 30,
is_featured BOOLEAN DEFAULT false,
stripe_price_id VARCHAR(255),
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
🔔 Notification Tables
notifications (UUID)
User notifications.
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50),
title VARCHAR(255),
message TEXT,
read BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
🎫 Support Tables
tickets (UUID)
Support tickets.
CREATE TABLE tickets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INT NOT NULL,
subject VARCHAR(255) NOT NULL,
status VARCHAR(20) DEFAULT 'open', -- 'open'|'in_progress'|'resolved'|'closed'
priority VARCHAR(10) DEFAULT 'medium', -- 'low'|'medium'|'high'|'urgent'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
ticket_messages (UUID)
Messages within tickets.
CREATE TABLE ticket_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
user_id INT NOT NULL,
message TEXT NOT NULL,
is_staff BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
📝 Audit Tables
login_audits
Login attempt tracking.
CREATE TABLE login_audits (
id SERIAL PRIMARY KEY,
user_id INT,
identifier VARCHAR(100),
success BOOLEAN,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
activity_logs
General activity logging.
CREATE TABLE activity_logs (
id SERIAL PRIMARY KEY,
user_id INT,
entity_type VARCHAR(50),
entity_id INT,
action VARCHAR(50),
details JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
🔗 Junction Tables
user_companies
Maps users to companies (N:M relationship).
CREATE TABLE user_companies (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
company_id INT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
role VARCHAR(30) DEFAULT 'recruiter', -- 'admin'|'recruiter'
permissions JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, company_id)
);
favorite_jobs
Saved jobs by users.
CREATE TABLE favorite_jobs (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, job_id)
);
🔄 Migrations History
| # | File | Description |
|---|---|---|
| 001 | 001_create_users_table.sql |
Core users table |
| 002 | 002_create_companies_table.sql |
Companies/employers |
| 003 | 003_create_user_companies_table.sql |
User ↔ Company mapping |
| 004 | 004_create_prefectures_cities_tables.sql |
Geographic data |
| 005 | 005_create_jobs_table.sql |
Job postings |
| 006 | 006_create_applications_table.sql |
Job applications |
| 007 | 007_create_favorite_jobs_table.sql |
Saved jobs |
| 008 | 008_create_password_resets_table.sql |
Password reset tokens |
| 010 | 010_seed_super_admin.sql |
Default superadmin |
| 011 | 011_add_is_featured_to_jobs.sql |
Featured jobs flag |
| 012 | 012_add_work_mode.sql |
Work mode column |
| 013 | 013_create_backoffice_tables.sql |
Audit tables |
| 014 | 014_update_job_status_constraint.sql |
Status enum update |
| 015 | 015_add_candidate_profile_fields.sql |
Candidate profile |
| 016 | 016_create_notifications_table.sql |
Notifications |
| 017 | 017_create_tickets_table.sql |
Support tickets |
| 018 | 018_add_currency_to_jobs.sql |
Currency support |
| 019 | 019_create_job_payments_table.sql |
Payment tracking |
| 020 | 020_unify_schema.sql |
Schema unification |
| 999 | 999_fix_gohorse_schema.sql |
Schema fixes |
⚠️ ID Strategy Notes
The database uses a hybrid ID strategy:
| Strategy | Tables | Rationale |
|---|---|---|
| UUID v7 | users, companies, jobs, applications, notifications, tickets, job_payments | Time-ordered, distributed-friendly, sortable, scalable |
| SERIAL (INT) | regions, cities, job_posting_prices, job_tags | Reference/Static data, low volume |
UUID v7 (RFC 9562)
Starting from migration 021_create_uuid_v7_function.sql, the database uses UUID v7 instead of UUID v4:
-- UUID v7 is generated by uuid_generate_v7() function
SELECT uuid_generate_v7();
-- Returns: 019438a1-2b3c-7abc-8123-4567890abcdef
-- ^^^^^^^^ time component (sortable)
Benefits of UUID v7:
- ⏱️ Time-ordered (sortable by creation time)
- 🌐 Distributed-friendly (no coordination needed)
- 📊 Better index performance than UUID v4
- 🔒 Contains embedded timestamp
🔒 Security Notes
- Password Hashing: BCrypt with optional
PASSWORD_PEPPERenvironment variable - Soft Deletes: Not implemented (uses
CASCADEfor referential integrity) - Row-Level Security: Not implemented (uses application-level checks)
- Audit Trail:
login_auditsandactivity_logsfor compliance
📚 Related Documentation
- Backend README - API documentation
- Seeder README - Database seeding
- Backoffice README - Admin panel