docs: overhaul global documentation with diagrams, test scenarios, and agent directives

This commit is contained in:
Tiago Yamamoto 2026-02-23 21:16:04 -06:00
parent 4a096ff903
commit 028d26d2e6
6 changed files with 404 additions and 2232 deletions

View file

@ -1,382 +1,103 @@
# AGENTS.md - GoHorse Jobs
> **Last Updated:** 2026-02-18
> **Purpose:** Context for AI coding assistants (Claude, Cursor, etc.)
> **Purpose:** Context and Directives for AI coding assistants (Claude, Cursor, Aider, etc.)
> **Branch Context:** `dev`
---
## 🚨 SUPERIOR DIRECTIVES 🚨
1. **DO NOT TOUCH KUBERNETES/SECRETS**: You are strictly forbidden from modifying any files in `k8s/` or altering raw RSA/base64 authentication keys.
2. **ENV VARIABLES**: Never hardcode secrets in code. If you define a new environment variable, it must be documented. Next.js variables exposed to the client must use the `NEXT_PUBLIC_` prefix. NEVER expose secrets with `NEXT_PUBLIC_`.
3. **READ FIRST**: Before implementing new features, always read the relevant documentation in this `docs/` folder to understand existing patterns (e.g., Go Clean Architecture, React Server Components).
---
## Project Overview
GoHorse Jobs is a B2B SaaS recruitment platform connecting companies with candidates. It is structured as a monorepo with multiple services sharing a single PostgreSQL database.
GoHorse Jobs is a modern B2B SaaS recruitment platform connecting companies with candidates. It operates across multiple services sharing a single PostgreSQL database.
**Business model**: Freemium (Free / Pro R$199/month / Enterprise custom pricing).
---
### Repository Structure
## Repository Structure
```
```text
gohorsejobs/
├── backend/ # Go 1.24 REST API (Clean Architecture + DDD)
├── frontend/ # Next.js 15 web application (App Router)
├── backoffice/ # NestJS 11 admin/worker API (Fastify adapter)
├── backend/ # Go REST API (Clean Architecture + DDD)
├── frontend/ # Next.js web application (App Router)
├── backoffice/ # NestJS admin and worker API
├── seeder-api/ # Node.js Express database seeder
├── job-scraper-multisite/# Job scraping service
├── k8s/ # Kubernetes manifests (dev, hml, prd)
├── k8s/ # Kubernetes manifests (FORBIDDEN DOMAIN)
├── docs/ # Central documentation
├── .forgejo/ # Forgejo CI/CD workflows
├── .drone.yml # Drone CI/CD pipelines
├── .forgejo/ # Forgejo actions workflows
└── start.sh # Interactive dev startup script
```
---
## Tech Stack
| Service | Language | Framework | Key Libraries |
|---------|----------|-----------|---------------|
| Backend | Go 1.24 | stdlib net/http | JWT v5, lib/pq, GORM, Stripe, AWS SDK v2, RabbitMQ, Firebase Admin, Swaggo |
| Frontend | TypeScript 5 | Next.js 15 (App Router) | React 19, Tailwind CSS 4, shadcn/ui, Zustand, React Hook Form + Zod, Framer Motion |
| Backoffice | TypeScript 5 | NestJS 11 (Fastify) | TypeORM, Passport + JWT, Stripe, AMQP, Nodemailer, Pino, Firebase Admin |
| Seeder | JavaScript (ESM) | Express 5 | pg, bcrypt |
**Database**: PostgreSQL 16+ with UUID v7 primary keys. 43+ SQL migration files in `backend/migrations/`.
---
## Quick Start
```bash
./start.sh # Interactive menu
```
| Option | Action |
|--------|--------|
| 1 | Start Frontend + Backend |
| 2 | Reset DB + Seed + Start |
| 3 | Start all services (Frontend + Backend + Backoffice) |
| 4 | Run migrations only |
| 5 | Seed database (append) |
| 6 | Full DB reset + migrate + seed |
| 7 | Run backend E2E tests |
| 8 | Seed reset LITE (skip 153k cities) |
| 9 | Run all tests (Backend + Frontend) |
### Service Ports
| Service | Port |
|---------|------|
| Backend | 8521 |
| Frontend | 8963 |
| Backoffice | 3001 |
| Swagger (Backend) | 8521/swagger/index.html |
| Swagger (Backoffice) | 3001/api/docs |
---
## Build & Test Commands
### Backend (Go)
```bash
cd backend && go run cmd/api/main.go # Run
go test -v ./... -count=1 # All unit tests
go test -tags=e2e -v ./tests/e2e/... # E2E tests
swag init -g cmd/api/main.go --parseDependency --parseInternal # Swagger
go mod tidy # Dependencies
```
### Frontend (Next.js)
```bash
cd frontend
npm install # Install deps
npm run dev # Dev server (use -p 8963 for project convention)
npm run build # Production build
npm run lint # ESLint
npm run test # Jest unit tests
```
### Backoffice (NestJS)
```bash
cd backoffice
pnpm install # Uses pnpm
npm run start:dev # Dev server with watch
npm run build # Production build
npm run lint # ESLint with auto-fix
npm run test # Jest unit tests
npm run test:e2e # E2E tests
```
### Seeder
```bash
cd seeder-api
npm install
npm run migrate # Run migrations
npm run seed # Seed data
npm run seed:reset # Drop all tables
npm run seed:lite # Seed without 153k cities
```
---
## Architecture & Conventions
### Backend (Go) - Clean Architecture + DDD
### Backend (Go 1.24)
* **Architecture**: Clean Architecture + Domain Driven Design (DDD)
* **Flow**: Controller (`internal/handlers`) -> UseCase (`internal/core/usecases`) -> Interface Port (`internal/core/ports`) -> Repository (`internal/infrastructure/persistence`).
* **Key Libs**: `net/http` (stdlib routing), `lib/pq` + `gorm` (legacy/transitioning), `swaggo` (Docs).
* **Running**: `cd backend && go run cmd/api/main.go` (Port 8521)
```
backend/internal/
├── api/ # (legacy) HTTP handlers
├── handlers/ # HTTP request handlers (current)
├── middleware/ # Auth, CORS, rate limiting, security headers
├── core/
│ ├── domain/entity/ # Business entities (User, Company, Job, etc.)
│ ├── ports/ # Repository interfaces
│ └── usecases/ # Business logic (LoginUseCase, RegisterUseCase, etc.)
├── infrastructure/
│ ├── auth/ # JWT service
│ ├── persistence/ # PostgreSQL repository implementations
│ └── storage/ # S3/R2 storage adapter
├── services/ # Business services (Email, FCM, Storage, Admin)
├── dto/ # Data Transfer Objects
├── router/ # Route definitions
├── models/ # GORM models (legacy, being migrated)
├── database/ # DB connection & migration runner
└── utils/ # Utilities (JWT, sanitizer)
```
### Frontend (Next.js 15)
* **Architecture**: App Router (`src/app`), heavily favoring React Server Components (RSC). Use `'use client'` strictly at the leaf nodes of the component tree for interactivity.
* **Styling**: Tailwind CSS, generic UI components in `src/components/ui` (shadcn).
* **State & Validation**: Zustand (global state), React Hook Form + Zod (forms).
* **i18n**: Multi-language support (PT, EN, ES, JA). Ensure all text is extracted to translation files.
* **Running**: `cd frontend && npm run dev -p 8963` (Port 8963)
**Patterns**:
- Constructor injection: `func NewService(db *sql.DB) *Service`
- All DB operations accept `ctx context.Context`
- Error handling: `(T, error)` return tuples
- Repository interfaces in `core/ports/`, implementations in `infrastructure/persistence/`
- Test files: `*_test.go`
### Backoffice (NestJS 11)
* **Architecture**: Modular NestJS over Fastify.
* **Modules**: Admin, Auth, Email (Worker), Stripe, Tickets.
* **Running**: `cd backoffice && npm run start:dev` (Port 3001)
### Frontend (Next.js) - App Router
```
frontend/src/
├── app/ # File-based routing (20+ routes)
│ ├── dashboard/ # Protected routes (12+ sub-pages)
│ ├── jobs/ # Job listing & details
│ └── auth/ # Login, register flows
├── components/ # Reusable components (44+)
│ └── ui/ # shadcn/ui primitives (24+)
├── hooks/ # Custom React hooks
├── contexts/ # React contexts (auth, theme)
├── lib/ # Utilities (API calls, validation)
└── i18n/ # Internationalization (PT, EN, ES, JA)
```
**Patterns**:
- Server Components by default; use `'use client'` for interactivity
- Tailwind CSS utility classes
- Path alias: `@/*` maps to `./src/*`
- Form validation: React Hook Form + Zod schemas
- Global state: Zustand stores
- Test files: `*.test.tsx`
### Backoffice (NestJS) - Modular
```
backoffice/src/
├── admin/ # Dashboard & statistics
├── auth/ # JWT authentication (Passport)
├── email/ # Email worker (AMQP/LavinMQ consumer)
├── external-services/ # Credential management
├── fcm-tokens/ # Firebase push tokens
├── plans/ # Subscription plans
├── stripe/ # Stripe payment integration
└── tickets/ # Support tickets
```
**Patterns**:
- NestJS module pattern: `*.module.ts`, `*.controller.ts`, `*.service.ts`
- Guards: `@UseGuards(JwtAuthGuard)`
- DTOs: `create-*.dto.ts`, `update-*.dto.ts` with class-validator
- Prettier: single quotes, trailing commas
### Seeder API (Express 5)
* **Purpose**: Manages database migrations and test data populations.
* **Running**: `cd seeder-api && npm run seed`
---
## Authentication & Authorization
## ⚠️ Known Gotchas & Historical Errors
- **JWT**: HS256 with HttpOnly cookies (web) + Bearer tokens (API/mobile)
- **4 roles**: `superadmin` > `admin` > `recruiter` > `candidate`
- **Middleware stack**: Auth (JWT+RBAC) -> CORS -> Rate Limiting (100 req/min) -> Security Headers -> XSS Sanitizer
- **JWT secret must match** between Backend and Backoffice
- **Test credentials**: See [TEST_USERS.md](TEST_USERS.md) for all test accounts
### 1. The `PASSWORD_PEPPER` Trap
**Symptom**: "Invalid credentials" upon login right after seeding the database.
**Cause**: The initial SQL migration creates the superadmin, but lacks the bcrypt pepper hashing. The Node.js `seeder-api` calculates the real hash.
**Fix**: You must run `npm run migrate` **AND THEN** `npm run seed` in the `seeder-api` directory. The seeder will read `PASSWORD_PEPPER` (e.g., `gohorse-pepper`) from your `.env` and fix the hashes.
### 2. Login Payload
The frontend login form sends `{ "email": "admin", "password": "..." }`. The backend resolves this against the `email` or `identifier` columns. **Do not** change the payload payload to `{ "identifier": "admin" }` because the backend DTO explicitly expects the JSON key `"email"`.
### 3. Bcrypt in bash strings
**Never** run a raw SQL command via bash `docker exec` containing a bcrypt hash like `$2b$10$...`. The bash shell expands `$2b` as a variable, corrupting the hash. Always write the SQL to a file and execute the file.
### 4. Contact/Tickets
The contact form uses Next.js internationalization and routes directly to the backoffice `tickets` API. Emails go to `wtf@gohorsejobs.com`.
---
## Database
## Dev Environments & Test Users
- PostgreSQL 16+ with UUID v7 for primary keys (SERIAL for reference tables)
- Migrations: `backend/migrations/` (43+ SQL files, numbered `000_` through `999_`)
- Core tables: `users`, `companies`, `user_companies`, `jobs`, `applications`, `favorite_jobs`, `notifications`, `tickets`, `activity_logs`, `job_payments`
**Environments**:
* **dev**: Current working branch. Deployed to `dev.gohorsejobs.com` or `local.gohorsejobs.com`
* **main**: Production branch.
---
**Test Users** (Local/Dev):
* Superadmin: `lol` / `Admin@2025!`
* Superadmin: `superadmin` / `Admin@2025!`
## API Routes
All backend routes under `/api/v1`:
- **Auth**: `/auth/login`, `/auth/register/candidate`, `/auth/register/company`, `/auth/forgot-password`, `/auth/reset-password`
- **Jobs**: CRUD at `/jobs`, moderation at `/jobs/moderation`
- **Companies**: CRUD at `/companies`, status management
- **Users**: `/users/me` (profile), admin CRUD at `/users`
- **Applications**: `/applications` with status updates
- **Storage**: `/storage/upload-url` (presigned S3/R2 URLs)
- **Admin**: `/admin/companies`, `/admin/email-templates`, `/admin/email-settings`
- **Notifications**: `/notifications`, `/tokens` (FCM)
- **Chat**: `/conversations`, `/conversations/{id}/messages`
---
## External Services
| Service | Purpose |
|---------|---------|
| Stripe | Payment processing & subscriptions |
| Firebase (FCM) | Push notifications |
| Appwrite | Real-time chat/messaging |
| LavinMQ (AMQP) | Message queue for background jobs |
| Cloudflare R2 / S3 | File/image storage |
| Resend | Transactional email |
---
## Deployment
- **Environments**: `dev` (branch: dev), `hml` (branch: hml), `prd` (branch: main)
- **CI/CD**: Forgejo workflows (`.forgejo/workflows/`) + Drone (`.drone.yml`)
- **Container runtime**: Podman (Apolo), Coolify/Docker (Redbull), Kubernetes (production)
- **Registry**: Forgejo (`forgejo-gru.rede5.com.br/rede5/`)
- **Coolify UI**: https://redbull.rede5.com.br (Redbull VPS DEV environment)
### DEV Environments
| Server | Type | URLs |
|--------|------|------|
| **Redbull** (Coolify) | Auto-deploy via Git | `local.gohorsejobs.com`, `api-local.gohorsejobs.com` |
| **Apolo** (Podman/Quadlet) | Manual deploy | `dev.gohorsejobs.com`, `api-tmp.gohorsejobs.com` |
---
## Git Workflow
### Remotes
| Remote | URL | Function |
|--------|-----|----------|
| **origin** | git@github.com:rede5/gohorsejobs.git | GitHub - main development |
| **pipe** | https://pipe.gohorsejobs.com/bohessefm/gohorsejobs.git | Forgejo - mirror/CI |
### Branches
| Branch | Environment | Description |
|--------|-------------|-------------|
| **dev** | Development | Main working branch |
| **hml** | Staging | Pre-production testing |
| **main** | Production | Stable release |
### Sync Flow
```bash
# 1. Pull from GitHub
git pull origin dev
# 2. Push to Forgejo (pipe)
git push pipe dev
```
---
## Key Things to Know
1. Backend uses Clean Architecture + DDD; respect layer boundaries (handlers -> usecases -> ports/repositories)
2. Frontend uses Next.js App Router; pages in `src/app/`, components in `src/components/`
3. Backoffice shares the same PostgreSQL database as the backend
4. Migrations are plain SQL files, not managed by an ORM
5. Project supports 4 languages (PT, EN, ES, JA) via i18n
6. Environment variables in `.env` files (gitignored)
7. Use `start.sh` for local development
---
## ⚠️ Known Gotchas
### 1. PASSWORD_PEPPER e o hash do superadmin são gerados em runtime
A migration `010_seed_super_admin.sql` **não hardcoda hash**. Ela cria o superadmin com um
placeholder inválido e `force_change_password`. O **seeder-api** é quem gera o hash correto
em runtime lendo `process.env.PASSWORD_PEPPER`.
Fluxo correto após reset do banco:
```
npm run migrate → superadmin criado, hash = placeholder (não faz login)
npm run seed → hash recalculado com pepper do ambiente, status = active
```
O `start.sh` opções 2, 6 e 8 já fazem isso automaticamente (migrate → seed).
| Location | `PASSWORD_PEPPER` |
|----------|-------|
| `backend/.env` (local) | `gohorse-pepper` |
| `seeder-api/.env` | `gohorse-pepper` |
| Coolify DEV (`iw4sow8s0kkg4cccsk08gsoo`) | `gohorse-pepper` |
Se `PASSWORD_PEPPER` mudar, basta rodar `npm run seed` novamente — o hash é recalculado
automaticamente. Nenhuma migration precisa ser alterada.
If you suspect a mismatch, see the full fix procedure in [DEVOPS.md](DEVOPS.md#troubleshooting-login-retorna-invalid-credentials).
### 2. Login API field: `email`, not `identifier`
The frontend login form sends `{ "email": "<username_or_email>", "password": "..." }`.
The backend core DTO (`internal/core/dto/user_auth.go`) has field `Email`, **not** `Identifier`.
The repository resolves it via `WHERE email = $1 OR identifier = $1`, so both username and email work.
Do **not** send `{ "identifier": "..." }` directly to `/api/v1/auth/login` — the field will be ignored and login will fail silently.
### 3. Bcrypt hashes in SQL — always use a file, never `-c`
Shell interprets `$` in bcrypt hashes as variable expansions. Example of **broken** approach:
```bash
# ❌ WRONG — $2b gets expanded by shell, hash gets corrupted
docker exec postgres psql -U user -d db -c "UPDATE users SET password_hash='$2b$10$abc...'"
```
Correct approach — write to file first, then use `-f`:
```bash
cat > /tmp/fix.sql <<'EOF'
UPDATE users SET password_hash = '$2b$10$4759wJhnXnBpcwSnVZm9Eu.wTqGYVCHkxAU5a2NxhsFHU42nV3tzW' WHERE identifier = 'lol';
EOF
docker cp /tmp/fix.sql <postgres_container>:/tmp/fix.sql
docker exec <postgres_container> psql -U gohorsejobs -d gohorsejobs -f /tmp/fix.sql
```
### 4. Test credentials (DEV/local.gohorsejobs.com)
| Role | Identifier | Password | Email |
|------|-----------|----------|-------|
| superadmin | `lol` | `Admin@2025!` | lol@gohorsejobs.com |
| superadmin | `superadmin` | `Admin@2025!` | admin@gohorsejobs.com |
---
See [docs/TEST_USERS.md](TEST_USERS.md) for Candidate and Company personas.
## Documentation Index
| Document | Location | Description |
|----------|----------|-------------|
| API Reference | [API.md](API.md) | Endpoints, contracts, examples |
| API Security | [API_SECURITY.md](API_SECURITY.md) | Auth, RBAC, permissions |
| Database Schema | [DATABASE.md](DATABASE.md) | Tables, ERD, migrations |
| DevOps | [DEVOPS.md](DEVOPS.md) | Infrastructure, deployment, diagrams |
| Test Users | [TEST_USERS.md](TEST_USERS.md) | Credenciais de teste por role |
| Roadmap | [ROADMAP.md](ROADMAP.md) | Product direction |
| Tasks | [TASKS.md](TASKS.md) | Task tracking |
| Workflows | [WORKFLOWS.md](WORKFLOWS.md) | Deployment workflows |
| Backend | [../backend/BACKEND.md](../backend/BACKEND.md) | Go API details |
| Frontend | [../frontend/FRONTEND.md](../frontend/FRONTEND.md) | Next.js details |
| Backoffice | [../backoffice/BACKOFFICE.md](../backoffice/BACKOFFICE.md) | NestJS details |
| Seeder | [../seeder-api/SEEDER-API.md](../seeder-api/SEEDER-API.md) | Database seeding & test data |
When asked to learn or modify parts of the system, consult these files first:
* [API.md](API.md) - Endpoint contracts
* [API_SECURITY.md](API_SECURITY.md) - Auth, RBAC
* [APPSEC_STRATEGY.md](APPSEC_STRATEGY.md) - Application Security and Testing
* [DATABASE.md](DATABASE.md) - Schema and ERD
* [DEVOPS.md](DEVOPS.md) - Cloudflare, Traefik, Containers
* [TASKS.md](TASKS.md) / [TEST_USERS.md](TEST_USERS.md)

View file

@ -1,574 +1,87 @@
# 📡 GoHorse Jobs - API Documentation
# 🔌 API Documentation & Routing - GoHorseJobs
Complete API reference with routes, permissions, and modules.
> **Last Updated:** 2026-02-16
> **Base URL:** `https://api.gohorsejobs.com/api/v1`
> **Auth:** JWT Bearer Token or HttpOnly Cookie
This document outlines the API landscape across the GoHorseJobs monorepo, detailing the main services and how they communicate.
---
## 🔐 Authentication
## 🏗️ API Architecture & Communication
### Methods
1. **Authorization Header:** `Authorization: Bearer <token>`
2. **Cookie:** `jwt=<token>` (HttpOnly, Secure)
* **Backend API (Go):** The central source of truth for the platform. Handles all core business logic (Users, Companies, Jobs, Applications).
* **Backoffice API (NestJS):** Dedicated to administrative tasks, workers (email queues), and specialized integrations (Stripe, Firebase Cloud Messaging). Shares the **same PostgreSQL database** as the Backend API.
* **Seeder API (Node.js):** Runs strictly on demand to populate the database with dummy data or run migrations.
### Roles
| Role | Code | Level | Description |
|------|------|-------|-------------|
| **SuperAdmin** | `superadmin` | 0 | Platform administrator |
| **Admin** | `admin` | 1 | Company administrator |
| **Recruiter** | `recruiter` | 2 | Job poster |
| **Candidate** | `candidate` | 3 | Candidate |
| **Guest** | - | - | No authentication |
### Communication Flow (Frontend)
The Next.js Frontend primarily communicates with the **Backend API**.
* **Paradigm:** The frontend uses React Server Components (RSC) to fetch data natively from the Go API.
* **Client Actions:** For forms and interactivity (e.g., login, apply), client components dispatch requests using the wrapped fetch utility located at `src/lib/api.ts`.
* **Auth State:** JWT tokens are stored locally (Cookies/Storage) and appended to the `Authorization: Bearer <token>` header manually via interceptors or utility functions.
---
## 📋 Module: Authentication
## 📡 Backend API Reference (`/api/v1`)
Hosted at `api-local.gohorsejobs.com` (Dev) or `api.gohorsejobs.com` (Prd).
View detailed interactive Swagger Docs by visiting `/swagger/index.html`.
### Login
```http
POST /api/v1/auth/login
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | Guest | Authenticate user |
### Authentication
* `POST /auth/login`: Accepts `{ "email": "...", "password": "..."}`. Returns JWT token and User DTO.
* `POST /auth/register/candidate`: Creates a standard user.
* `POST /auth/register/company`: Creates a user and a linked `Company` profile awaiting approval.
**Request:**
```json
{
"identifier": "lol",
"password": "Admin@2025!"
}
```
### Users & Profiles
* `GET /users/me`: Gets the full profile of the currently authenticated user (derived from JWT context).
* `PUT /users/me`: Updates profile details.
* `GET /admin/users`: (Admin only) Lists all users.
**Response (200):**
```json
{
"token": "eyJhbGciOiJI...",
"user": { "id": 1, "role": "superadmin", "name": "Dr. Horse Expert" }
}
```
### Companies
* `GET /companies`: Lists active companies with pagination and filters.
* `GET /companies/{id}`: Detailed company view.
* `PUT /companies/{id}`: Edit company profile (Requires owner/admin permissions via `user_companies` relation).
* `POST /admin/companies/{id}/status`: (Admin only) Approve/Reject pending companies.
### Jobs & Applications
* `GET /jobs`: Lists published jobs. (Supports full-text search and filters).
* `POST /jobs`: Creates a draft job (Requires Recruiter).
* `PUT /jobs/{id}`: Updates job details.
* `POST /jobs/{id}/apply`: Submits an application for the job.
* `GET /applications`: Lists received applications (for recruiters) or submitted applications (for candidates).
* `POST /applications/{id}/status`: Updates application status (`reviewing`, `accepted`, `rejected`).
### Miscellaneous
* `GET /health`: Basic ping endpoint.
* `POST /storage/upload-url`: Requests a presigned URL (S3/Cloudflare R2) to upload logos or resumes directly from the browser.
---
### Register Candidate
```http
POST /api/v1/auth/register
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | Guest | Register new job seeker |
## 🛠️ Backoffice API Reference (`/api`)
Hosted at `b-local.gohorsejobs.com` (Dev) or `b.gohorsejobs.com` (Prd).
View detailed interactive Swagger Docs by visiting `/api/docs`.
**Request:**
```json
{
"name": "John Doe",
"email": "john@example.com",
"password": "SecurePass123!"
}
```
### Tickets & Support
* `POST /tickets`: Public endpoint (no auth required) to submit a contact/support ticket. Used directly by the frontend `/contact` page.
* `GET /tickets`: (Admin only) Lists all open tickets.
* `POST /tickets/{id}/reply`: (Admin only) Adds a message to a ticket and triggers an email notification to the user.
### Emails & Workers
The Backoffice listens to a LavinMQ (AMQP) message queue to dispatch transactional emails (welcome, password reset, application updates) securely without blocking the main Go runtime.
### Stripe Billing
* `POST /stripe/webhook`: Consumes Stripe events to upgrade company plans from Free to Pro/Enterprise.
---
## 🏢 Module: Companies
## 📚 Seeder API Commands
### Create Company
```http
POST /api/v1/companies
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | Guest | Register new company |
If you need to instantly provision the database for these routes to work:
### List Companies
```http
GET /api/v1/companies
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Smart | ⚪ Optional | Guest/Admin | Public list or admin full list |
```bash
cd seeder-api
npm install
### Update Company Status
```http
PATCH /api/v1/companies/{id}/status
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Activate/deactivate company |
# Drops all tables, runs migrations, inserts core test users (100 jobs, 50 companies)
npm run seed:lite
---
## 👥 Module: Users
### Get Current User
```http
GET /api/v1/users/me
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | Any authenticated | Get current user profile |
### List Users
```http
GET /api/v1/users
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | List all users |
### Create User
```http
POST /api/v1/users
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Create new user |
### Update User
```http
PATCH /api/v1/users/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | Any authenticated | Update user profile |
### Delete User
```http
DELETE /api/v1/users/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin | Delete user |
### List User Roles
```http
GET /api/v1/users/roles
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | List available roles |
---
## 💼 Module: Jobs
### List Jobs (Public)
```http
GET /api/v1/jobs
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | Guest | Search and filter jobs |
**Query Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `q` | string | Search query (title, description) |
| `location` | string | Filter by location |
| `type` | string | Employment type filter |
| `workMode` | string | `onsite`, `hybrid`, `remote` |
| `page` | int | Page number (default: 1) |
| `limit` | int | Items per page (default: 20) |
### Get Job by ID
```http
GET /api/v1/jobs/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | Guest | Get job details |
### Create Job
```http
POST /api/v1/jobs
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public* | ❌ | admin, recruiter | Create new job posting |
### Update Job
```http
PUT /api/v1/jobs/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public* | ❌ | admin, recruiter | Update job posting |
### Delete Job
```http
DELETE /api/v1/jobs/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public* | ❌ | admin, recruiter | Delete job posting |
### List Jobs for Moderation
```http
GET /api/v1/jobs/moderation
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | List all jobs for moderation |
### Update Job Status
```http
PATCH /api/v1/jobs/{id}/status
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Approve/reject/close job |
### Duplicate Job
```http
POST /api/v1/jobs/{id}/duplicate
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Duplicate job posting |
---
## 📝 Module: Applications
### Create Application
```http
POST /api/v1/applications
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | Guest/JobSeeker | Apply to a job |
**Request:**
```json
{
"job_id": 123,
"name": "John Doe",
"email": "john@example.com",
"phone": "+55 11 99999-9999",
"message": "I am interested in this position...",
"resume_url": "https://..."
}
# Runs migrations ONLY
npm run migrate
```
### List Applications
```http
GET /api/v1/applications
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | admin, recruiter | List applications |
### Get Application by ID
```http
GET /api/v1/applications/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | admin, recruiter | Get application details |
### Update Application Status
```http
PUT /api/v1/applications/{id}/status
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | admin, recruiter | Update application status |
**Request:**
```json
{
"status": "hired",
"notes": "Approved after final interview"
}
```
**Status Values:**
- `pending` - Awaiting review
- `reviewed` - Seen by recruiter
- `shortlisted` - Selected for interview
- `rejected` - Not selected
- `hired` - Offer accepted
---
## 🔔 Module: Notifications
### List Notifications
```http
GET /api/v1/notifications
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | Any authenticated | Get user notifications |
---
## 🏷️ Module: Tags
### List Tags
```http
GET /api/v1/tags
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | List all tags |
### Create Tag
```http
POST /api/v1/tags
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Create new tag |
### Update Tag
```http
PATCH /api/v1/tags/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Update tag |
---
## 👤 Module: Candidates
### List Candidates
```http
GET /api/v1/candidates
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | List all candidates |
---
## 📊 Module: Audit
### List Login Audits
```http
GET /api/v1/audit/logins
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Login audit trail |
---
## 📦 Module: Storage
### Generate Upload URL
```http
POST /api/v1/storage/upload-url
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | Any authenticated | Get S3 presigned upload URL |
### Generate Download URL
```http
POST /api/v1/storage/download-url
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | Any authenticated | Get S3 presigned download URL |
### Delete File
```http
DELETE /api/v1/storage/files
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | Any authenticated | Delete file from S3 |
---
## 📚 Module: Documentation
### Swagger UI
```http
GET /docs/
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | Guest | Interactive API documentation |
---
## 🔑 Module: System Credentials
Manage encrypted credentials for external services (Stripe, SMTP, Storage, etc.).
### List Configured Services
```http
GET /api/v1/system/credentials
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | List all configured services (metadata only, no secrets) |
**Response (200):**
```json
{
"services": [
{
"service_name": "stripe",
"updated_at": "2026-02-23T10:30:00Z",
"updated_by": "admin-uuid",
"is_configured": true
},
{
"service_name": "smtp",
"updated_at": "2026-02-22T14:00:00Z",
"updated_by": "superadmin-uuid",
"is_configured": true
}
]
}
```
### Save Credentials
```http
POST /api/v1/system/credentials
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Create or update service credentials |
**Request:**
```json
{
"serviceName": "stripe",
"payload": {
"secretKey": "sk_live_xxx",
"webhookSecret": "whsec_xxx",
"publishableKey": "pk_live_xxx"
}
}
```
**Response (200):**
```json
{
"message": "Credentials saved successfully"
}
```
**Supported Services:**
| Service Name | Fields |
|--------------|--------|
| `stripe` | `secretKey`, `webhookSecret`, `publishableKey` |
| `smtp` | `host`, `port`, `username`, `password`, `from_email`, `from_name`, `secure` |
| `storage` | `endpoint`, `region`, `bucket`, `accessKey`, `secretKey` |
| `cloudflare_config` | `apiToken`, `zoneId` |
| `firebase` | `serviceAccountJson` (JSON string) |
| `appwrite` | `endpoint`, `projectId`, `apiKey` |
| `lavinmq` | `amqpUrl` |
| `cpanel` | `host`, `username`, `apiToken` |
### Delete Credentials
```http
DELETE /api/v1/system/credentials/{serviceName}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, admin | Delete service credentials |
**Response (200):**
```json
{
"success": true
}
```
**Security Notes:**
- Credentials are encrypted with RSA-OAEP-SHA256 before storage
- Only metadata (service name, timestamps) is returned on list operations
- Actual secret values cannot be retrieved after saving
- Updating credentials overwrites the existing configuration
---
## 🔑 Permission Matrix
| Route | Guest | JobSeeker | Recruiter | CompanyAdmin | Admin | SuperAdmin |
|-------|-------|-----------|-----------|--------------|-------|------------|
| `POST /auth/login` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `POST /auth/register` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| `GET /jobs` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `GET /jobs/{id}` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `POST /jobs` | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| `POST /applications` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `GET /users/me` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `GET /users` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `DELETE /users/{id}` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `GET /notifications` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `GET /audit/logins` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `GET /jobs/moderation` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
---
## 🆔 ID Formats
| Entity | ID Type | Example |
|--------|---------|---------|
| Users | INT (SERIAL) | `1`, `42`, `1337` |
| Companies | INT (SERIAL) | `1`, `15`, `100` |
| Jobs | INT (SERIAL) | `1`, `500`, `2500` |
| Notifications | UUID v7 | `019438a1-2b3c-7abc-8123-4567890abcdef` |
| Tickets | UUID v7 | `019438a2-3c4d-7xyz-9abc-def0123456789` |
| Payments | UUID v7 | `019438a3-4e5f-7mno-pqrs-tuvwxyz012345` |
---
## 📝 Error Responses
### 400 Bad Request
```json
{
"error": "Invalid request body",
"details": "Field 'email' is required"
}
```
### 401 Unauthorized
```json
{
"error": "Unauthorized",
"message": "Invalid or expired token"
}
```
### 403 Forbidden
```json
{
"error": "Forbidden",
"message": "Insufficient permissions"
}
```
### 404 Not Found
```json
{
"error": "Not Found",
"message": "Resource not found"
}
```
### 500 Internal Server Error
```json
{
"error": "Internal Server Error",
"message": "An unexpected error occurred"
}
```
---
## 📚 Related Documentation
- [Database Schema](DATABASE.md)
- [Roadmap](ROADMAP.md)
- [Tasks](TASKS.md)
The seeder automatically inserts the `Admin@2025!` superadmin hash required for local testing.

View file

@ -1,751 +1,131 @@
# 🗄️ Database Schema Documentation
# 🗄️ Database Architecture - GoHorseJobs
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/`
GoHorseJobs uses a single **PostgreSQL 16+** database shared across all services (Backend API, NestJS Backoffice, Node.js Seeder).
---
## <EFBFBD> Development Environment Structure
## 🏗️ Core Entity-Relationship Diagram (ERD)
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
The core data model centers around `users` acting in different capacities (`candidate`, `recruiter`, `admin`) interacting with `companies` and `jobs`.
```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"
USERS ||--o{ USER_COMPANIES : "belongs to"
COMPANIES ||--o{ USER_COMPANIES : "contains"
companies ||--o{ user_companies : "has members"
companies ||--o{ jobs : "posts"
COMPANIES ||--o{ JOBS : "posts"
USERS ||--o{ JOBS : "creates (author)"
jobs ||--o{ applications : "receives"
jobs ||--o{ favorite_jobs : "saved by"
jobs ||--o{ job_payments : "has payments"
USERS ||--o{ APPLICATIONS : "submits"
JOBS ||--o{ APPLICATIONS : "receives"
regions ||--o{ cities : "contains"
regions ||--o{ companies : "located in"
regions ||--o{ jobs : "located in"
USERS ||--o{ FAVORITE_JOBS : "saves"
JOBS ||--o{ FAVORITE_JOBS : "favorited"
tickets ||--o{ ticket_messages : "contains"
USERS ||--o{ TICKETS : "opens"
USERS ||--o{ TICKET_MESSAGES : "sends"
TICKETS ||--o{ TICKET_MESSAGES : "has"
%% 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
USERS {
uuid id PK
string role "superadmin, admin, recruiter, candidate"
string identifier "username or email"
string email UK
string password_hash
boolean is_active
timestamp created_at
}
user_roles {
int user_id PK,FK
varchar role PK "composite key"
COMPANIES {
uuid id PK
string name
string document "CNPJ"
string domain UK
string location
string industry
string logo_url
string status "pending, active, rejected"
}
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
USER_COMPANIES {
uuid id PK
uuid user_id FK
uuid company_id FK
string role "owner, admin, member"
}
jobs {
int id PK "SERIAL"
int company_id FK
int created_by FK
varchar title
JOBS {
uuid id PK
uuid company_id FK
uuid author_id FK
string 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
string type "full-time, remote, etc"
decimal max_salary
decimal min_salary
string status "draft, published, closed"
}
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 {
APPLICATIONS {
uuid id PK
int user_id FK
varchar type
varchar title
text message
boolean read
uuid job_id FK
uuid candidate_id FK
string status "pending, reviewing, interviewed, rejected, accepted"
text cover_letter
}
job_payments {
FAVORITE_JOBS {
uuid id PK
int job_id FK
int user_id FK
decimal amount
varchar status
varchar stripe_session_id
uuid user_id FK
uuid job_id FK
timestamp created_at
}
tickets {
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
uuid user_id FK "nullable for guests"
string subject
string category "Support, Bug, Billing..."
string status "open, in_progress, resolved, closed"
string email "fallback for guests"
string name "fallback for guests"
}
```
---
## 🏗️ Table Overview
## 🔑 Key Strategies
| 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 |
### 1. Primary Keys (UUID v7)
We aggressively use **UUID v7** for all major business entities (`users`, `companies`, `jobs`).
* **Why?** UUID v7 contains a timestamp component, making it naturally sortable by insertion time (like SERIAL/auto-increment) while avoiding the predictability and distributed generation bottlenecks of standard integers.
---
### 2. Multi-Role Profiles
The system handles permissions through the `role` enum in the `users` table:
* `superadmin`: Global control over GoHorseJobs.
* `admin`: Can moderate companies/jobs inside the Backoffice.
* `recruiter`: A user attached to a `company_id` via `user_companies` who can post `jobs`.
* `candidate`: A standard user seeking jobs and making `applications`.
## 📋 Core Tables
A single email can only correspond to one role globally. If a user needs another role, they must change it or use a different credential.
### `users`
### 3. Migrations
Migrations are written in pure SQL and stored in `backend/migrations/`.
They follow a strict sequential numbering index (e.g., `000_schema.sql`, `001_roles.sql`, etc). The backend runtime executes these on startup if the database is out of sync.
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)
);
### 4. Background Seeding
Seeding (populating test data) is explicitly decoupled from migrations. It lives isolated in the Node.js `seeder-api`.
To reset your local database to a clean, populated state:
```bash
# Deletes everything, remigrates, and inserts base data (except 153k cities)
cd seeder-api && npm run seed:lite
```
---
### `companies`
## ⚠️ Security Notes
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
### The `password_hash` & Pepper
Bcrypt hashes stored in the standard `users` table require a secret pepper (`PASSWORD_PEPPER`).
* The raw hash stored in the DB cannot be cracked immediately due to the pepper.
* The seeder relies on reading the `PASSWORD_PEPPER` from `.env` to forge the initial super-admin and test accounts hashes. **If you change the `.env` pepper, the entire DB of seeded passwords immediately invalidates.** You must re-run `npm run seed`.

View file

@ -1,651 +1,119 @@
# DevOps - GoHorseJobs (Development Environment)
# ☁️ DevOps & Infrastructure - GoHorseJobs
Infraestrutura, CI/CD e deploy do projeto GoHorseJobs no servidor `apolo`.
> **Last Updated:** 2026-02-18
> **Servers:** Apolo VPS (Podman), Redbull VPS (Coolify)
> **Tech Stack:** Podman, Systemd (Quadlet), Traefik, PostgreSQL, Coolify
---
## ☁️ Cloudflare DNS Zone
### Zone Info
| Property | Value |
|----------|-------|
| **Zone ID** | `5e7e9286849525abf7f30b451b7964ac` |
| **Domain** | gohorsejobs.com |
| **Account** | gohorsejobs |
| **Email** | yamamoto@rede5.com.br |
| **Plan** | Free Website |
| **Name Servers** | chuck.ns.cloudflare.com, fatima.ns.cloudflare.com |
### API Access
```bash
# Token location: ~/.ssh/cloudflare-token
export CF_AUTH_EMAIL="yamamoto@rede5.com.br"
export CF_AUTH_KEY="5dcfd89a9d4ec330dede0d4074a518f26818e"
# List zones
curl -s -H "X-Auth-Email: $CF_AUTH_EMAIL" -H "X-Auth-Key: $CF_AUTH_KEY" \
"https://api.cloudflare.com/client/v4/zones"
# List DNS records
curl -s -H "X-Auth-Email: $CF_AUTH_EMAIL" -H "X-Auth-Key: $CF_AUTH_KEY" \
"https://api.cloudflare.com/client/v4/zones/5e7e9286849525abf7f30b451b7964ac/dns_records"
# Purge cache
curl -s -X DELETE -H "X-Auth-Email: $CF_AUTH_EMAIL" -H "X-Auth-Key: $CF_AUTH_KEY" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/5e7e9286849525abf7f30b451b7964ac/purge_cache" \
-d '{"purge_everything":true}'
```
### Active DNS Records (gohorsejobs.com)
| Subdomain | Type | IP/Target | Proxied |
|-----------|------|------------|---------|
| dev.gohorsejobs.com | A | 38.19.201.52 | No |
| api.gohorsejobs.com | A | 86.48.29.139 | Yes |
| api-dev.gohorsejobs.com | A | 86.48.29.139 | Yes |
| api-local.gohorsejobs.com | A | 38.19.201.52 | No |
| b-local.gohorsejobs.com | A | 38.19.201.52 | No |
| s-local.gohorsejobs.com | A | 38.19.201.52 | No |
| coolify-dev.gohorsejobs.com | A | 185.194.141.70 | No |
| local.gohorsejobs.com | A | 185.194.141.70 | No |
| api-local.gohorsejobs.com | A | 185.194.141.70 | No |
| b-local.gohorsejobs.com | A | 185.194.141.70 | No |
| s-local.gohorsejobs.com | A | 185.194.141.70 | No |
| panel.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
| pipe.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
| alert.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
| task.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
| stats.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
| storage.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
| base.gohorsejobs.com | A | Multiple | No |
| reg.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
| gohorsejobs.com | CNAME | gohorsejobs.pages.dev | Yes |
| *.gohorsejobs.com | CNAME | 8a3f435b-f374-4268-90f7-5610f577c706.cfargotunnel.com | Yes |
| mail.gohorsejobs.com | CNAME | everest.mxrouting.net | No |
> Total: 190 DNS records (paginados)
---
## ☁️ Coolify DEV Environment (Redbull)
Ambiente de desenvolvimento no Coolify para deploy automatizado via Git.
### Server Info
| Property | Value |
|----------|-------|
| **Host** | redbull (185.194.141.70) |
| **Coolify URL** | https://redbull.rede5.com.br |
| **API Token** | `~/.ssh/coolify-redbull-token` |
| **SSH Key** | `~/.ssh/civo` |
| **Project UUID** | `gkgksco0ow4kgwo8ow4cgs8c` |
| **Environment** | `dev` |
### Resources Created
| Resource | UUID | Port | Domain | Status |
|----------|------|------|--------|--------|
| Backend | `iw4sow8s0kkg4cccsk08gsoo` | 8521 | https://api-local.gohorsejobs.com | running |
| Frontend | `ao8g40scws0w4cgo8coc8o40` | 3000 | https://local.gohorsejobs.com | running |
| Backoffice | `hg48wkw4wggwsswcwc8sooo4` | 3001 | https://b-local.gohorsejobs.com | running |
| Seeder | `q4w48gos8cgssso00o8w8gck` | 8080 | https://s-local.gohorsejobs.com | running:healthy |
| Database (PostgreSQL) | `bgws48os8wgwk08o48wg8k80` | 5432 | Internal only | running:healthy |
### API Reference
**Base URL:** `https://redbull.rede5.com.br/api/v1`
**Server UUID:** `m844o4gkwkwcc0k48swgs8c8`
```bash
# Listar aplicações
curl -s -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
"https://redbull.rede5.com.br/api/v1/applications"
# Atualizar domínios (requer http:// ou https://)
curl -s -X PATCH -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
-H "Content-Type: application/json" \
"https://redbull.rede5.com.br/api/v1/applications/<UUID>" \
-d '{"domains":"http://local.gohorsejobs.com","instant_deploy":true}'
# Deploy aplicação
curl -s -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
"https://redbull.rede5.com.br/api/v1/deploy?uuid=<UUID>"
# Ver domínios do servidor
curl -s -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
"https://redbull.rede5.com.br/api/v1/servers/m844o4gkwkwcc0k48swgs8c8/domains"
```
### Architecture
```
GitHub (rede5/gohorsejobs.git) ←→ Forgejo (pipe.gohorsejobs.com)
│ │
▼ ▼
Coolify (redbull.rede5.com.br)
├── Traefik (reverse proxy + TLS via Let's Encrypt)
├── gohorsejobs-backend-dev → https://api-local.gohorsejobs.com
├── gohorsejobs-frontend-dev → https://local.gohorsejobs.com
├── gohorsejobs-backoffice-dev → https://b-local.gohorsejobs.com
├── gohorsejobs-seeder-dev → https://s-local.gohorsejobs.com
└── PostgreSQL 16 (gohorsejobs-dev) → Internal network only
```
### Environment Variables
Configured via Coolify UI or API:
```bash
DATABASE_URL=postgres://gohorsejobs:gohorsejobs123@bgws48os8wgwk08o48wg8k80:5432/gohorsejobs?sslmode=disable
BACKEND_PORT=8521
ENV=development
JWT_SECRET=gohorsejobs-dev-jwt-secret-2024-very-secure-key-32ch
JWT_EXPIRATION=7d
PASSWORD_PEPPER=gohorse-pepper
COOKIE_SECRET=gohorsejobs-cookie-secret-dev
CORS_ORIGINS=https://local.gohorsejobs.com,https://b-local.gohorsejobs.com,http://localhost:3000,http://localhost:8521
```
> ⚠️ **`PASSWORD_PEPPER` é crítico.** Todas as migration seeds e o seeder-api usam `gohorse-pepper`.
> Se este valor for alterado no Coolify sem regravar os hashes no banco, **todos os logins falharão**
> com `invalid credentials`. Veja a seção de troubleshooting abaixo.
### ⚠️ Troubleshooting: Login retorna `invalid credentials`
**Causa:** O `PASSWORD_PEPPER` no Coolify não coincide com o pepper usado para gerar os hashes no banco.
**Diagnóstico via SSH:**
```bash
ssh redbull
# Verificar pepper no container em execução:
docker inspect <backend_container_name> --format '{{range .Config.Env}}{{println .}}{{end}}' | grep PEPPER
# Testar login direto (sem escaping de shell):
cat > /tmp/login.json <<'EOF'
{"email":"lol","password":"Admin@2025!"}
EOF
docker run --rm --network coolify -v /tmp/login.json:/tmp/login.json \
curlimages/curl:latest -s -X POST \
http://<backend_container>:8521/api/v1/auth/login \
-H 'Content-Type: application/json' -d @/tmp/login.json
```
**Fix — opção 1 (preferível): corrigir o pepper no Coolify e re-rodar o seeder:**
```bash
TOKEN=$(cat ~/.ssh/coolify-redbull-token)
# 1. Atualizar PASSWORD_PEPPER
curl -s -X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key":"PASSWORD_PEPPER","value":"gohorse-pepper"}' \
"https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/envs"
# 2. Reiniciar o backend (para ele pegar o novo pepper)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/restart"
# 3. Re-rodar o seeder (ele regrava o hash com o pepper correto automaticamente)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://redbull.rede5.com.br/api/v1/deploy?uuid=q4w48gos8cgssso00o8w8gck"
```
> O seeder (`seedUsers()`) sempre faz upsert do superadmin/lol com `bcrypt(senha + PEPPER)`.
> Mudar o pepper e re-rodar o seeder é suficiente — nenhuma migration precisa ser tocada.
**Fix — opção 2 (emergência, sem seeder): regravar hash direto no banco:**
```bash
ssh redbull
# Gerar novo hash com node (usando arquivo para evitar expansão de $ pelo shell):
mkdir -p /tmp/hashgen && cat > /tmp/hashgen/gen.js <<'EOF'
const b = require("./node_modules/bcryptjs");
console.log(b.hashSync("Admin@2025!" + process.env.PEPPER, 10));
EOF
docker run --rm -v /tmp/hashgen:/app -w /app -e PEPPER=gohorse-pepper \
node:20-alpine sh -c "npm install bcryptjs -s && node gen.js"
# Aplicar no banco (SEMPRE usar -f, nunca -c, para preservar os $ do hash):
cat > /tmp/fix_hash.sql <<'EOF'
UPDATE users SET password_hash = '<hash_gerado_acima>', status = 'active'
WHERE identifier IN ('lol', 'superadmin');
EOF
docker cp /tmp/fix_hash.sql bgws48os8wgwk08o48wg8k80:/tmp/fix_hash.sql
docker exec bgws48os8wgwk08o48wg8k80 psql -U gohorsejobs -d gohorsejobs -f /tmp/fix_hash.sql
```
> ⚠️ **Nunca passe hash bcrypt via `-c '...'`** na linha de comando — o shell expande os `$`
> e corrompe o hash silenciosamente. Use sempre um arquivo e `-f`.
### Deploy via API
```bash
# Deploy application
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
"https://redbull.rede5.com.br/api/v1/deploy?uuid=iw4sow8s0kkg4cccsk08gsoo"
# Check deployment status
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
"https://redbull.rede5.com.br/api/v1/deployments/<deployment_uuid>"
# List applications
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
"https://redbull.rede5.com.br/api/v1/applications"
# List databases
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
"https://redbull.rede5.com.br/api/v1/databases"
```
### Coolify Reference
- **Docs:** https://coolify.io/docs/get-started/introduction
- **API Reference:** https://coolify.io/docs/api-reference/authorization
- **GitHub Integration:** Uses SSH deploy key for private repo access
This document maps out the comprehensive DevOps lifecycle, the server topologies, the container orchestrations, and CI/CD operations powering GoHorseJobs.
---
## 🏗️ Architecture Diagrams
### Full Infrastructure Overview
### 1. Global Infrastructure Overview (DEV / HML Environments)
A look into how our development environment handles requests through Cloudflare down to the Coolify-managed `Redbull` VPS.
```mermaid
graph TB
subgraph Clients ["Clients"]
Browser["Browser / Mobile"]
subgraph Clients ["Public Clients"]
Browser["Web Browser / Mobile App"]
end
subgraph CF ["Cloudflare (DNS + CDN)"]
subgraph CF ["Cloudflare (DNS + Proxy + CDN)"]
DNS["DNS Zone: gohorsejobs.com"]
WAF["Web Application Firewall (WAF)"]
end
subgraph Redbull ["Redbull VPS (185.194.141.70) — Coolify DEV"]
TraefikR("Traefik + Let's Encrypt")
subgraph Redbull ["Redbull VPS (185.194.141.70) — Ubuntu"]
TraefikR("Traefik (Reverse Proxy + Let's Encrypt)")
subgraph CoolifyApps ["Coolify Applications"]
FE_C["Frontend (:3000)"]
BE_C["Backend API (:8521)"]
BO_C["Backoffice (:3001)"]
SE_C["Seeder API (:8080)"]
subgraph CoolifyApps ["Coolify Application Containers"]
FE_C["Frontend (Next.js)"]
BE_C["Backend API (Go)"]
BO_C["Backoffice (NestJS)"]
SE_C["Seeder API (Node.js)"]
end
PG_C[("PostgreSQL 16\ngohorsejobs-dev")]
subgraph CoolifyData ["Coolify Data Containers"]
PG_C[("PostgreSQL 16")]
MQ_C["LavinMQ / RabbitMQ"]
Redis_C[("Redis (Caching/Sessions)")]
end
end
subgraph Apolo ["Apolo VPS (38.19.201.52) — Podman/Quadlet"]
TraefikA("Traefik")
Browser -->|HTTPS| DNS
DNS --> WAF
WAF -->|Proxy/Cache| TraefikR
TraefikR -->|local.gohorsejobs.com| FE_C
TraefikR -->|api-local.gohorsejobs.com| BE_C
TraefikR -->|b-local.gohorsejobs.com| BO_C
subgraph PodmanApps ["Podman Containers (Systemd/Quadlet)"]
FE_A["Frontend (:3000)"]
BE_A["Backend API (:8521)"]
BO_A["Backoffice (:3001)"]
SE_A["Seeder API (:8080)"]
end
PG_A[("PostgreSQL\npostgres-main")]
Storage["/mnt/data\n(configs + DB data)"]
end
subgraph Git ["Git Repositories"]
GH["GitHub\nrede5/gohorsejobs"]
FJ["Forgejo (pipe)\npipe.gohorsejobs.com"]
end
subgraph External ["External Services"]
Stripe["Stripe (Payments)"]
Firebase["Firebase (FCM)"]
R2["Cloudflare R2 (Storage)"]
LavinMQ["LavinMQ (AMQP)"]
Resend["Resend (Email)"]
end
%% Client Flow
Browser --> DNS
DNS -- "local.gohorsejobs.com" --> TraefikR
DNS -- "dev.gohorsejobs.com" --> TraefikA
%% Redbull Routing
TraefikR -- "local.gohorsejobs.com" --> FE_C
TraefikR -- "api-local.gohorsejobs.com" --> BE_C
TraefikR -- "b-local.gohorsejobs.com" --> BO_C
TraefikR -- "s-local.gohorsejobs.com" --> SE_C
BE_C --> PG_C
BO_C --> PG_C
SE_C --> PG_C
%% Apolo Routing
TraefikA -- "dev.gohorsejobs.com" --> FE_A
TraefikA -- "api-tmp.gohorsejobs.com" --> BE_A
TraefikA -- "b-tmp.gohorsejobs.com" --> BO_A
BE_A --> PG_A
BO_A --> PG_A
SE_A --> PG_A
PG_A -.-> Storage
%% Git Flow
GH <--> FJ
%% External
BE_C -.-> Stripe
BE_C -.-> Firebase
BE_C -.-> R2
BO_C -.-> LavinMQ
BO_C -.-> Resend
style PG_C fill:#336791,stroke:#fff,color:#fff
style PG_A fill:#336791,stroke:#fff,color:#fff
style TraefikR fill:#f5a623,stroke:#fff,color:#fff
style TraefikA fill:#f5a623,stroke:#fff,color:#fff
style CF fill:#f48120,stroke:#fff,color:#fff
FE_C -.->|REST /api/v1| BE_C
BO_C -.->|Queries| PG_C
BE_C <-->|Queries| PG_C
BE_C -.->|AMQP Pub| MQ_C
MQ_C -.->|AMQP Sub| BO_C
```
### Apolo VPS (Podman/Quadlet) Detail
### 2. CI/CD Operations (Forgejo -> VPS)
How code travels from a `git push` on `dev` to the live container.
```mermaid
graph TD
subgraph Host ["Apolo VPS (Host)"]
subgraph FS ["File System (/mnt/data)"]
EnvBE["/gohorsejobs/backend/.env"]
EnvBO["/gohorsejobs/backoffice/.env"]
EnvSE["/gohorsejobs/seeder-api/.env"]
DBData[("postgres-general")]
end
subgraph Net ["Network: web_proxy"]
Traefik("Traefik")
subgraph App ["Application Containers"]
BE["Backend API (:8521)"]
BO["Backoffice (:3001)"]
SE["Seeder API (:8080)"]
FE["Frontend (:3000)"]
end
PG[("postgres-main (:5432)")]
end
end
%% Ingress
Internet((Internet)) --> Traefik
%% Routing
Traefik -- "dev.gohorsejobs.com" --> FE
Traefik -- "api-tmp.gohorsejobs.com" --> BE
Traefik -- "b-tmp.gohorsejobs.com" --> BO
Traefik -- "seeder.gohorsejobs.com" --> SE
%% Config Mounts
EnvBE -.-> BE
EnvBO -.-> BO
EnvSE -.-> SE
%% Data Persistence
PG -.-> DBData
%% Database Connections
BE --> PG
BO --> PG
SE --> PG
style PG fill:#336791,stroke:#fff,color:#fff
style Traefik fill:#f5a623,stroke:#fff,color:#fff
```
### Coolify DEV (Redbull) Detail
```mermaid
graph TD
subgraph Redbull ["Redbull VPS — Coolify (redbull.rede5.com.br)"]
Traefik("Traefik + Let's Encrypt")
subgraph Apps ["Applications (auto-deploy via Git)"]
BE["Backend Go\n:8521"]
FE["Frontend Next.js\n:3000"]
BO["Backoffice NestJS\n:3001"]
SE["Seeder API\n:8080"]
end
PG[("PostgreSQL 16\ngohorsejobs\n:5432")]
end
GH["GitHub (rede5/gohorsejobs)"] --> |"push dev"| Traefik
Internet((Internet)) --> Traefik
Traefik -- "api-local.gohorsejobs.com" --> BE
Traefik -- "local.gohorsejobs.com" --> FE
Traefik -- "b-local.gohorsejobs.com" --> BO
Traefik -- "s-local.gohorsejobs.com" --> SE
BE --> PG
BO --> PG
SE --> PG
style PG fill:#336791,stroke:#fff,color:#fff
style Traefik fill:#f5a623,stroke:#fff,color:#fff
```
### CI/CD Flow (Dual Pipeline)
Existem **2 pipelines independentes** disparados simultaneamente a cada push:
```mermaid
graph TD
Dev["Developer\ngit push dev"]
subgraph Pipeline1 ["Pipeline 1: GitHub → Coolify"]
GH["GitHub\n(origin)"]
Webhook["GitHub Webhook\n(push event)"]
Coolify["Coolify\n(redbull.rede5.com.br)"]
Redbull["Redbull VPS\nFrontend + Backend + Backoffice + Seeder"]
end
subgraph Pipeline2 ["Pipeline 2: Forgejo → K3s Cluster"]
FJ["Forgejo\n(pipe.gohorsejobs.com)"]
Runner["Forgejo Actions Runner\n(self-hosted, K3s)"]
Registry["Container Registry\npipe.gohorsejobs.com"]
K3s["K3s Cluster\nBackend + Backoffice"]
end
Dev --> GH
Dev --> FJ
GH --> Webhook --> Coolify --> |"Docker build"| Redbull
FJ --> |"push triggers"| Runner
Runner --> |"docker build & push"| Registry
Runner --> |"kubectl apply"| K3s
```
| Pipeline | Trigger | Servicos | Destino |
|----------|---------|----------|---------|
| **GitHub → Coolify** | Webhook (push) | Frontend, Backend, Backoffice, Seeder | Redbull VPS (Docker) |
| **Forgejo → K3s** | Forgejo Actions (push) | Backend, Backoffice | K3s Cluster (Kubernetes) |
---
## 🔄 Forgejo CI/CD Pipeline (pipe.gohorsejobs.com)
O pipeline roda automaticamente via Forgejo Actions a cada push na branch `dev`.
### Workflow: `.forgejo/workflows/deploy.yaml`
| Job | Descricao | Status Atual |
|-----|-----------|-------------|
| **build-and-push** | Build Docker images (backend + backoffice), push to registry | OK |
| **deploy** | Deploy ao K3s via kubectl (requer KUBECONFIG secret) | OK (fix: KUBE_CONFIG → KUBECONFIG) |
### Pipeline Steps
1. **build-and-push** (OK):
- Checkout code
- Docker login no registry `pipe.gohorsejobs.com`
- Build & push backend: `pipe.gohorsejobs.com/bohessefm/gohorsejobs:latest`
- Build & push backoffice: `pipe.gohorsejobs.com/bohessefm/backoffice:latest`
2. **deploy** (FAIL - K3s nao configurado):
- Install kubectl
- Configure kubeconfig (via `secrets.KUBE_CONFIG`)
- Sync secrets e vars ao namespace `gohorsejobsdev`
- `kubectl apply -f k8s/dev/`
- Set image com SHA do commit
- Rollout restart deployments
> **Nota:** O job deploy usava `secrets.KUBE_CONFIG` mas o secret se chama `KUBECONFIG`. Corrigido no commit atual.
### Forgejo Actions Secrets & Variables
**Secrets** (configurados em Settings > Actions > Secrets):
- `FORGEJO_TOKEN` — Login no container registry
- `KUBECONFIG` — Kubeconfig para acesso ao K3s cluster
**Variables** (configurados em Settings > Actions > Variables):
- `DATABASE_URL`, `JWT_SECRET`, `PASSWORD_PEPPER`, `COOKIE_SECRET`, `COOKIE_DOMAIN`
- `BACKEND_PORT`, `BACKEND_HOST`, `ENV`, `CORS_ORIGINS`, `MTU`
- `AMQP_URL`, `S3_BUCKET`, `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINT`
- `RSA_PRIVATE_KEY_BASE64`, `JWT_EXPIRATION`
### Forgejo API
```bash
# Token location: ~/.ssh/forgejo-token
FORGEJO_TOKEN="03d23c54672519c8473bd9c46ae7820b13c8b287"
# Listar runs do pipeline
curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"https://pipe.gohorsejobs.com/api/v1/repos/bohessefm/gohorsejobs/actions/tasks?limit=5"
# Listar repositorios
curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"https://pipe.gohorsejobs.com/api/v1/user/repos"
```
### GitHub Webhooks (Auto-deploy Coolify)
Webhooks configurados no GitHub apontando para o Coolify:
| App | Webhook URL |
|-----|-------------|
| Backend | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=iw4sow8s0kkg4cccsk08gsoo&secret=...` |
| Frontend | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=ao8g40scws0w4cgo8coc8o40&secret=...` |
| Backoffice | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=hg48wkw4wggwsswcwc8sooo4&secret=...` |
| Seeder | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=q4w48gos8cgssso00o8w8gck&secret=...` |
---
## 🔑 Credenciais e Tokens (Referencias)
Todos os tokens estao armazenados em `~/.ssh/`:
| Arquivo | Servico | Uso |
|---------|---------|-----|
| `~/.ssh/coolify-redbull-token` | Coolify API | Deploy e gerenciamento de apps |
| `~/.ssh/forgejo-token` | Forgejo API (pipe) | CI/CD, webhooks, repos |
| `~/.ssh/github-token` | GitHub API | Webhooks, repos |
| `~/.ssh/cloudflare-token` | Cloudflare API | DNS, cache |
| `~/.ssh/absam-token` | Absam Cloud API | VPS management |
| `~/.ssh/forgejo-gohorsejobs` | SSH Key | Forgejo Git operations |
| `~/.ssh/civo` | SSH Key | Acesso VPS Redbull |
| `~/.ssh/github` | SSH Key | GitHub Git operations |
---
## 💾 Storage & Persistence (`/mnt/data`)
All persistent data and configuration files are stored in `/mnt/data` on the host.
| Host Path | Container Path | Purpose | Type |
|-----------|----------------|---------|------|
| `/mnt/data/gohorsejobs/backend/.env` | (Injected Env) | **Backend Config:** Secrets, DB URL, Port settings. | File |
| `/mnt/data/gohorsejobs/backoffice/.env` | (Injected Env) | **Backoffice Config:** Secrets, DB URL. | File |
| `/mnt/data/gohorsejobs/seeder-api/.env` | (Injected Env) | **Seeder Config:** Secrets, DB URL. | File |
| `/mnt/data/postgres-general` | `/var/lib/postgresql/data` | **Database Storage:** Main storage for `postgres-main` container. Contains `gohorsejobs_dev` DB. | Directory |
> **Backup Note:** To backup the environment, ensure `/mnt/data/gohorsejobs` and `/mnt/data/postgres-general` are included in snapshots.
---
## 🌍 Service Maps & Networking
### 🚦 Traefik Routing
Services are exposed via Traefik labels defined in the Quadlet `.container` files.
| Domain | Service | Internal Port | Host Port (Debug) |
|--------|---------|---------------|-------------------|
| `dev.gohorsejobs.com` | `gohorsejobs-frontend-dev` | `3000` | `8523` |
| `api-tmp.gohorsejobs.com` | `gohorsejobs-backend-dev` | `8521` | `8521` |
| `b-tmp.gohorsejobs.com` | `gohorsejobs-backoffice-dev` | `3001` | - |
| `seeder.gohorsejobs.com` | `gohorsejobs-seeder-dev` | `8080` | `8522` |
### 🛑 Security
- **Backend/Seeder/Frontend** expose ports to the Host (`85xx`) for debugging/direct access if needed.
- **Backoffice** is *only* accessible via Traefik (internal network).
- **PostgreSQL** is *only* accessible internally via `web_proxy` network (no host port binding).
---
## 🛠️ Operational Guide
### 1. View & Manage Configs
Configurations are **not** inside containers. Edit them on the host:
```bash
# Edit Backend Config
vim /mnt/data/gohorsejobs/backend/.env
# Apply changes
systemctl restart gohorsejobs-backend-dev
```
### 2. Full Environment Restart
To restart all GoHorseJobs related services (excluding Database):
```bash
systemctl restart gohorsejobs-backend-dev gohorsejobs-backoffice-dev gohorsejobs-seeder-dev gohorsejobs-frontend-dev
```
### 3. Database Access
Access the local database directly via the `postgres-main` container:
```bash
# Internal Connection
docker exec -it postgres-main psql -U yuki -d gohorsejobs_dev
sequenceDiagram
participant Dev as Developer
participant Git as GitHub (Origin)
participant Forgejo as Pipe / Forgejo (CI)
participant Coolify as Coolify Webhook
participant VPS as Redbull (VPS)
Dev->>Git: git push origin dev
Dev->>Forgejo: git push pipe dev
Note over Forgejo: Trigger Action (.forgejo/workflows/)
Forgejo->>Coolify: POST Deploy Webhook
Coolify-->>VPS: Fetch latest dev branch
Note over VPS: Coolify builds Nixpacks/Dockerfile
Coolify-->>VPS: docker stop <old_containers>
Coolify-->>VPS: docker run <new_containers>
Coolify-->>Forgejo: Deployment Success
Forgejo-->>Dev: Pipeline Green/Passed
```
---
## 🚀 Deployment Pipeline (Manual)
## 🛠️ Environments Topology
Current workflow uses **Local Build** -> **Forgejo Registry** -> **Server Pull**.
| Environment | Branch | Use Case | Server Host | Reverse Proxy | Config Manager | Domains |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| **Local (Host)** | N/A | Developer Sandbox | Laptop / PC | None | `start.sh` (Bare metal) | `localhost:8963` |
| **DEV** | `dev` | Continuous Integration | `Redbull` VPS | Traefik | Coolify v4 | `local.`, `api-local.`, `b-local.` |
| **HML** | `hml` | QA / Staging Testing | `Apolo` VPS | Nginx proxy | Podman (Quadlet) | `hml.`, `api-hml.`, `b-hml.` |
| **PRD** | `main` | Production Live | `Zeus`/`Poseidon` | Traefik Ingress | Kubernetes (K3s) | `gohorsejobs.com`, `api.` |
### 1. Build & Push (Local Machine)
---
## 🔧 Coolify Instance (Redbull)
The `dev` branch automatically mirrors to the Redbull server (185.194.141.70) managed by Coolify.
* **Coolify Interface:** `https://redbull.rede5.com.br`
* **GitHub Integration:** Relies on an SSH deployment key injected into the Forgejo actions.
### Container Rules
* Never manually `docker run` on Redbull. Use the Coolify interface to add environment variables or alter build commands.
* **Secrets:** Managed via Coolify Environment Variables. (e.g., `PASSWORD_PEPPER`, `JWT_SECRET`).
---
## 💡 Troubleshooting & Known Faultlines
### 1. `Invalid Credentials` Right After DB Seed
If the Backend Go server complains about invalid passwords right after you run `npm run seed`:
1. Check the `PASSWORD_PEPPER` inside the Coolify instance for the `seeder-api`.
2. It **must exactly match** the pepper configured for the Backend API.
3. If they matched, rerun `npm run seed` via the Coolify interface to force hash recalculation over the raw DB rows.
### 2. Out of Memory (OOMKilled) on Build
Node.js (for Next.js and NestJS) and Go can eat a lot of RAM during concurrent Coolify builds.
* **Fix:** Ensure the Server Actions inside Coolify are set to stagger deployments, preventing out-of-memory cascading crashes on Redbull.
### 3. SSH Connectivity
```bash
# Login
podman login forgejo-gru.rede5.com.br
# Build
cd backend
podman build -t forgejo-gru.rede5.com.br/rede5/gohorsejobs-backend:latest .
# Push
podman push forgejo-gru.rede5.com.br/rede5/gohorsejobs-backend:latest
```
### 2. Deploy (On Apolo Server)
```bash
ssh root@apolo
# Pull new image
podman pull forgejo-gru.rede5.com.br/rede5/gohorsejobs-backend:latest
# Restart service (Systemd handles container recreation)
systemctl restart gohorsejobs-backend-dev
# Connecting to Redbull
ssh root@185.194.141.70 -p 22
```
If access is denied, ensure your local public key is registered in `~/.ssh/authorized_keys` or injected via the VPS admin panel.

View file

@ -1,7 +1,48 @@
# Project Documentation - GoHorse Jobs
# 📚 Project Documentation - GoHorseJobs
This directory holds internal team documentation, guides, and extended context regarding project structure and architecture.
Welcome to the central documentation hub for **GoHorseJobs**. This repository contains the collective knowledge, architecture decisions, and operational guides for our B2B SaaS recruitment platform.
## 🚨 AI Rules Warning 🚨
Before making ANY changes to infrastructure deployment manifests or Kubernetes files, please refer to the project-wide AI Rules located at `.agent/rules.md`.
**Do NOT touch `k3s` or `k8s` files.** Do not alter raw RSA/base64 authentication keys.
---
## 🧭 Navigation & Index
Choose a specific domain below to dive deep into our technical implementation and guides.
### 🤖 1. AI & Developer Directives
* **[Context for AI Agents (AGENTS.md)](AGENTS.md)**: The supreme source of truth for Claude, Cursor, and other AI coding assistants. Contains stack rules, passwords, gotchas, and layout contexts.
* **[Rules (.agent/rules.md)](../.agent/rules.md)**: Absolute system boundaries and deployment limits.
### 🏗️ 2. High-Level Architecture
* **[DevOps & Infrastructure (DEVOPS.md)](DEVOPS.md)**: Full mapping of Cloudflare DNS, Traefik, VPS (Redbull/Apolo), Docker/Coolify containers, and CI/CD pipelines (Forgejo/Drone). Includes rich Mermaid diagrams.
* **[Database Schema (DATABASE.md)](DATABASE.md)**: PostgreSQL schemas, relationships, UUID v7 indexing strategies, and ERD visualizing the core data flow.
### 🔌 3. Application Interfaces (APIs)
* **[API Routes (API.md)](API.md)**: Endpoints mapped for the Go Backend (`/api/v1`), NestJS Backoffice services, and internal Node.js Seeder-API.
* **[API Security (API_SECURITY.md)](API_SECURITY.md)**: Details on HS256 JWT implementations, RBAC (Role-Based Access Control) levels, and CORS policies.
* **[AppSec Strategy (APPSEC_STRATEGY.md)](APPSEC_STRATEGY.md)**: The core mitigation plan against XSS, IDOR, Mass Assignment, and testing vectors within Next.js.
### 🫂 4. Operations & Testing
* **[Test Users & Data (TEST_USERS.md)](TEST_USERS.md)**: Comprehensive list of robust local credentials, passwords, dummy candidates, and seeded companies.
* **[Deployment Routes (WORKFLOWS.md)](WORKFLOWS.md)**: A catalog of our `.forgejo` and GitHub Actions synchronizations.
* **[Tasks (TASKS.md)](TASKS.md)**: Open checklist of internal improvements.
---
## 🚀 Quick Launch (Dev)
If you are a new developer or setting up the environment post-clone, rely on our interactive script:
```bash
cd /path/to/gohorsejobs
./start.sh
```
**Options overview**:
* `1`: General Start (Frontend + Backend)
* `2`: Fresh Start (Reset Postgres Data -> Run Migrations -> Seed Core Data -> Start)
* `3`: God Mode (Frontend + Backend + Backoffice)
---
## 🚨 Final Notice
**Do NOT** alter base configuration settings (such as encryption secrets, or Kubernetes `k3s`/`k8s` manifests) unless explicitly guided by the Lead Engineer or following the strict protocols in `.agent/rules.md`.

View file

@ -1,6 +1,55 @@
# Test Users - GoHorseJobs
# 🧪 Test Users & Data Scenarios - GoHorseJobs
> **DEPRECATED / MOVED**
> Este arquivo foi depreciado e toda a documentação das matrizes de Usuários de Teste foi movida e unificada diretamente no **`[README.md](../README.md)`** da raiz do projeto.
>
> As rotas antigas do `superadmin` foram aposentadas via migrations da base, onde o perfil oficial master passa a se chamar unicamente: `lol`.
When running the platform locally (via `start.sh`) or on the DEV server (`local.gohorsejobs.com`), the `seeder-api` provisions a rich set of test accounts representing distinct personas.
All accounts use the exact same password to simplify development.
**Universal Test Password:** `Admin@2025!`
---
## 👥 Core Personas (The "Golden" Accounts)
These are the primary accounts you should use for daily development and E2E testing.
| Role | Identifier (Username) | Email | Purpose |
| :--- | :--- | :--- | :--- |
| **Superadmin** | `lol` | `lol@gohorsejobs.com` | Complete system access. Bypass company walls. Manage features. |
| **Superadmin** | `superadmin` | `admin@gohorsejobs.com` | Secondary global admin. |
| **Admin** | `admin` | `moderator@gohorsejobs.com` | Reviews pending companies/jobs in backoffice. |
| **Recruiter** | `recruiter` | `hr@techcorp.com` | Posts jobs, manages applicants for "TechCorp" (Company ID: 1). |
| **Recruiter** | `jane_hr` | `jane.doe@startup.io` | Recruiter for a pending/unapproved company to test gating. |
| **Candidate** | `candidate` | `carlos.dev@gmail.com` | Standard job seeker. Pre-filled resume and skills. Has 3 active applications. |
| **Candidate** | `newbie` | `new.user@hotmail.com` | Fresh account with 0 applications and empty profile to test onboarding flows. |
---
## 🏢 Auto-Generated Companies & Jobs
The DB seeder simulates a healthy marketplace.
If you run `npm run seed:lite`, the DB receives:
* **50 Companies**: Ranging from "TechCorp" (Active) to "SuspiciousLTDA" (Pending/Rejected).
* **100 Jobs**: Distributed among the active companies. Various statuses (`published`, `draft`, `closed`).
* **200 Applications**: Dummy candidates applying to random jobs, in varied pipelines (`pending`, `reviewing`, `accepted`).
---
## 🚨 Login & Auth "Gotchas"
### 1. `Invalid Credentials` Right After Fresh Seed
If you completely reset the DB using `start.sh` (Option 6) or manually clear Postgres, and your first login returns "Invalid credentials", **do not panic and do not manually change the DB hash**.
* **Cause**: The Node.js seeder calculates the bcrypt hash using an environment variable called `PASSWORD_PEPPER` (`gohorse-pepper`). If this variable was empty or mismatched with the Backend API, the hash is physically wrong.
* **Fix**: Ensure your `.env` files in both `backend` and `seeder-api` contain `PASSWORD_PEPPER=gohorse-pepper`. Then simply run `cd seeder-api && npm run seed` again to fix the hashes.
### 2. Login Payload Fields
The frontend sends:
```json
{
"email": "lol",
"password": "..."
}
```
**Important:** Even though the frontend passes the username `lol`, the JSON key **must** be `"email"`. The Go backend natively handles resolving `"email"` against the `identifier` OR `email` database columns.
### 3. Account Status Flags
The `users` table has an `is_active` boolean. If a user is manually deactivated by an admin, the login endpoint will return a generic `401 Unauthorized` (or specific Account Locked error depending on the exact route version) to prevent user enumeration.