# 🗄️ Database Schema Documentation

Complete database documentation for the GoHorseJobs platform.

> **Last Updated:** 2026-01-03
> **Database:** PostgreSQL 16+ (Local `postgres-main` container)
> **Connection:** Internal `gohorsejobs_dev` database via `web_proxy` network
> **ID Strategy:** UUID v7 for core tables, SERIAL for reference tables  
> **Migrations:** 30 SQL files in `backend/migrations/`

---

## �️ Development Environment Structure

The development environment (`apolo` server) uses a **Local Containerized Strategy** to ensure isolation and speed.

### 🏗️ Topology

```mermaid
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)**
```bash
# 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:
```bash
# 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:
```bash
curl -X POST https://seeder.gohorsejobs.com/seed
```


## �📊 Entity Relationship Diagram

```mermaid
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.

```sql
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 administrator
- `admin` - Company administrator
- `recruiter` - Job poster/recruiter
- `candidate` - Candidate/job seeker

---

### `user_roles`

Additional roles per user (for multi-role support).

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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.

```sql
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).

```sql
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.

```sql
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:

```sql
-- 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

---

## 📚 Related Documentation

- [Backend README](../backend/README.md) - API documentation
- [Seeder README](../seeder-api/README.md) - Database seeding
- [Backoffice README](../backoffice/README.md) - Admin panel
