# πŸ—„οΈ 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/` --- ## �️ 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