gohorsejobs/docs/DATABASE.md

16 KiB

🗄️ Database Schema Documentation

Complete database documentation for the GoHorseJobs platform.

Last Updated: 2024-12-24
Database: PostgreSQL 15+
ID Strategy: SERIAL (INT) for core tables, UUID v7 for newer tables


📊 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"
    
    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"
    
    %% 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
    }

🏗️ 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'|'companyAdmin'|'recruiter'|'jobSeeker'
    
    -- 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 administrator
  • companyAdmin - Company administrator
  • recruiter - Job poster/recruiter
  • jobSeeker - 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 yet
  • open - Accepting applications
  • published - Live and visible
  • paused - Temporarily hidden
  • closed - No longer accepting
  • expired - Past expiration date
  • archived - Archived by employer
  • reported - Flagged for review
  • review - 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',  -- 'companyAdmin'|'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

  1. Password Hashing: BCrypt with optional PASSWORD_PEPPER environment variable
  2. Soft Deletes: Not implemented (uses CASCADE for referential integrity)
  3. Row-Level Security: Not implemented (uses application-level checks)
  4. Audit Trail: login_audits and activity_logs for compliance