gohorsejobs/docs/DATABASE.md
Tiago Yamamoto 6ec54460f8 docs: unify documentation structure
- Create docs/AGENTS.md for AI assistants context
- Create docs/WORKFLOWS.md consolidating deployment workflows
- Remove redundant docs/root/ folder
- Remove .agent/ folder (consolidated into docs/)
- Update dates in all documentation files
- Simplify README.md documentation section
2026-02-16 05:57:02 -06:00

751 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🗄️ Database Schema Documentation
Complete database documentation for the GoHorseJobs platform.
> **Last Updated:** 2026-02-16
> **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/`
---
## <20> 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
```
## <20>📊 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