Microservices-based job aggregation platform with intelligent incremental sync
OpenJobs aggregates job listings from multiple sources into a unified API, featuring:
- π Incremental Sync - Only fetch new jobs, avoid duplicates
- π³ Microservices Architecture - Each connector runs independently
- β° Cron Scheduling - Precise daily sync at 6:00 AM
- π Database-backed State - No file-based persistence needed
- π High Limits - Fetch up to 500 jobs per connector
Production Deployment: https://api.openjobs.ink
Legacy URL: https://app-openjobs.katsu6.easypanel.host (deprecated)
| Connector | Jobs | Sync Method | Limit |
|---|---|---|---|
| ArbetsfΓΆrmedlingen | 50+ | API date filter | 500 |
| EURES (Adzuna) | 1+ | API date filter | 100 |
| Remotive | 100+ | Client filter | 100 |
| RemoteOK | 168+ | Client filter | All |
| Total | 333+ | Daily at 6 AM | - |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main API (Port 8080) β
β - REST API β
β - Dashboard β
β - Scheduler (Cron) β
β - HTTP Plugin Orchestrator β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β
β HTTP POST /sync
β
ββββββββββ΄βββββββββ¬βββββββββββ¬βββββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββ ββββββββββ ββββββββββ ββββββββββ
β AF β β EURES β βRemotiveβ βRemoteOKβ
β :8081 β β :8082 β β :8083 β β :8084 β
ββββββ¬ββββ ββββββ¬ββββ ββββββ¬ββββ ββββββ¬ββββ
β β β β
βββββββββββββββββ΄ββββββββββββ΄ββββββββββββ
β
βΌ
βββββββββββββββββββ
β Supabase DB β
β - job_posts β
β - sync_logs β
βββββββββββββββββββ
- Database-backed state: Tracks last sync via
posted_datein database - API date filtering: ArbetsfΓΆrmedlingen & EURES use API parameters
- Client-side filtering: Remotive & RemoteOK filter locally
- Zero duplicates: All syncs show 0 new when no updates
CRON_SCHEDULE=0 6 * * * # Daily at 6:00 AM- Precise timing (not interval-based)
- Configurable per environment
- Fallback to interval mode if not set
- ArbetsfΓΆrmedlingen: 500 jobs/sync (up from 20)
- EURES: 100 jobs/sync (up from 10)
- Remotive: 100 jobs/sync (up from 10)
- RemoteOK: All jobs (client-filtered)
- Docker containerized
- Easypanel deployment
- Health checks on all services
- Comprehensive logging
# Health & Status
GET /health # System health check
GET / # Dashboard UI
# Jobs (Public)
GET /jobs # List all jobs
GET /jobs/:id # Get specific job
# Jobs (Protected - Requires API Key)
POST /jobs # Post a new job (requires X-API-Key header)
# Sync
POST /sync/manual # Trigger manual sync
GET /sync/history # View sync logs
# Plugins
GET /plugins # List registered pluginsπ Note: To post jobs via API, you need an API key. Register at OpenJobs_Web to get your free API key instantly.
1. Get Your API Key (30 seconds)
# Visit the registration page
https://openjobs-web.vercel.app/register
# Fill in:
- Company name
- Email
- Website (optional)
# β
Your API key is generated instantly!2. Post Your First Job
Option A: Use the Web Form (Easiest)
# Login at OpenJobs_Web
https://openjobs-web.vercel.app/login
# Click "Post a Job"
# Fill the form and submitOption B: Use the API (For Integration)
curl -X POST https://api.openjobs.ink/jobs \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR-API-KEY-HERE" \
-d '{
"title": "Senior React Developer",
"company": "Your Company",
"description": "We are looking for an experienced developer...",
"location": "Stockholm, Sweden",
"employment_type": "full-time",
"salary_min": 50000,
"salary_max": 70000,
"salary_currency": "SEK",
"is_remote": false,
"url": "https://yourcompany.com/apply"
}'3. Your Job is Live!
- β
Immediately available via
/jobsAPI - β Visible to all job aggregators
- β Included in OpenJobs ecosystem
title- Job titlecompany- Company namedescription- Job description
location,employment_type,salary_min,salary_max,salary_currencyis_remote,url,expires_daterequirements[],benefits[]
- π Full API docs: See SETUP_GUIDE.md
- π¬ Questions: Open a GitHub issue
- π Web interface: OpenJobs_Web
GET /jobs
Returns a list of job postings with optional filtering.
Query Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
limit |
integer | Number of jobs to return (default: 20, max: 500) | ?limit=100 |
offset |
integer | Number of jobs to skip for pagination (default: 0) | ?offset=20 |
is_active |
boolean | Filter by active status (default: all) | ?is_active=true |
created_after |
ISO 8601 | Return jobs created after this timestamp | ?created_after=2025-11-17T06:00:00Z |
Examples:
# Get first 100 active jobs
GET /jobs?is_active=true&limit=100
# Get jobs created in the last 24 hours
GET /jobs?created_after=2025-11-16T15:00:00Z&is_active=true
# Incremental sync - get only new jobs since last check
GET /jobs?created_after=2025-11-17T06:00:00Z&is_active=true&limit=500
# Pagination - get next page
GET /jobs?is_active=true&limit=100&offset=100Response Format:
{
"success": true,
"data": [
{
"id": "af-12345",
"title": "Senior Developer",
"company": "Tech AB",
"description": "Job description...",
"location": "Stockholm",
"salary_min": 50000,
"salary_max": 70000,
"salary_currency": "SEK",
"is_remote": false,
"is_active": true,
"url": "https://...",
"posted_date": "2025-11-15T10:00:00Z",
"created_at": "2025-11-17T06:15:23Z",
"updated_at": "2025-11-17T06:15:23Z",
"requirements": ["Python", "React"],
"benefits": ["Remote work", "Health insurance"]
}
]
}Key Fields:
posted_date: When the employer originally posted the jobcreated_at: When OpenJobs added the job to the database (use for incremental sync)updated_at: When the job was last modified in OpenJobsis_active: Whether the job is currently active (not expired)
GET /health # Plugin health check
POST /sync # Trigger plugin sync1. Deploy Main API
Image: ghcr.io/magnusfroste/openjobs:latest
Port: 8080Environment Variables:
SUPABASE_URL=https://supabase.froste.eu
SUPABASE_ANON_KEY=your-key-here
USE_HTTP_PLUGINS=true
CRON_SCHEDULE=0 6 * * *
PLUGIN_ARBETSFORMEDLINGEN_URL=http://plugin-arbetsformedlingen:8081
PLUGIN_EURES_URL=http://plugin-eures:8082
PLUGIN_REMOTIVE_URL=http://plugin-remotive:8083
PLUGIN_REMOTEOK_URL=http://plugin-remoteok:80842. Deploy Each Plugin
Create 4 services with these images:
ghcr.io/magnusfroste/openjobs-arbetsformedlingen:latest(Port 8081)ghcr.io/magnusfroste/openjobs-eures:latest(Port 8082)ghcr.io/magnusfroste/openjobs-remotive:latest(Port 8083)ghcr.io/magnusfroste/openjobs-remoteok:latest(Port 8084)
Each needs:
SUPABASE_URL=https://supabase.froste.eu
SUPABASE_ANON_KEY=your-key-here
PORT=808X # Respective portEURES also needs:
ADZUNA_APP_ID=your-adzuna-id
ADZUNA_APP_KEY=your-adzuna-keySee EASYPANEL_ENV_SETUP.md for detailed instructions.
| Connector | Source | Type | Jobs |
|---|---|---|---|
| ArbetsfΓΆrmedlingen | Swedish Employment Service | Government | 50+ |
| EURES | Adzuna API (European jobs) | Commercial | 1+ |
| Remotive | Remotive.com | Platform | 100+ |
| RemoteOK | RemoteOK.com | Platform | 168+ |
All connectors implement:
type PluginConnector interface {
GetID() string
GetName() string
FetchJobs() ([]JobPost, error)
SyncJobs() error
}- Create connector in
connectors/yourname/ - Implement
PluginConnectorinterface - Add Dockerfile
- Register in main scheduler
- Deploy as new microservice
See existing connectors for examples.
To post jobs via the API, you need an API key from OpenJobs_Web:
- Register: Visit OpenJobs_Web
- Create Account: Fill in your company details
- Get API Key: Your unique API key is generated instantly
- Use It: Include in
X-API-Keyheader when posting jobs
# Example: Post a job with your API key
curl -X POST https://api.openjobs.ink/jobs \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key-here" \
-d '{"title":"Developer","company":"Acme","description":"..."}'Features:
- β Free forever
- β Instant generation
- β No credit card required
- β Post unlimited jobs
- Schedule: Daily at 6:00 AM (configurable via
CRON_SCHEDULE) - Method: HTTP POST to each plugin container
- Logging: All syncs logged to
sync_logstable
curl -X POST https://api.openjobs.ink/sync/manualAPI Date Filtering (ArbetsfΓΆrmedlingen, EURES):
- Query database for most recent job's
posted_date - Add
?published-after=YYYY-MM-DDto API request - API returns only new jobs
Client-Side Filtering (Remotive, RemoteOK):
- Fetch all jobs from API
- Query database for most recent job's
posted_date - Filter locally to only process new jobs
- Skip transformation/insertion of duplicates
- Go 1.21+
- Docker & Docker Compose
- Supabase account (or self-hosted)
1. Clone & Setup
git clone https://github.com/magnusfroste/openjobs.git
cd openjobs
cp .env.example .env
# Edit .env with your Supabase credentials2. Run Database Migrations
-- In Supabase SQL Editor, run:
migrations/001_create_job_posts.sql
migrations/002_add_job_fields.sql3. Start All Services
docker-compose -f docker-compose.plugins.yml upThis starts:
- Main API: http://localhost:8080
- ArbetsfΓΆrmedlingen: http://localhost:8081
- EURES: http://localhost:8082
- Remotive: http://localhost:8083
- RemoteOK: http://localhost:8084
4. Trigger Sync
curl -X POST http://localhost:8080/sync/manualopenjobs/
βββ cmd/
β βββ openjobs/ # Main API
β βββ plugin-arbetsformedlingen/ # AF plugin
β βββ plugin-eures/ # EURES plugin
β βββ plugin-remotive/ # Remotive plugin
β βββ plugin-remoteok/ # RemoteOK plugin
βββ connectors/
β βββ arbetsformedlingen/ # AF connector logic
β βββ eures/ # EURES connector logic
β βββ remotive/ # Remotive connector logic
β βββ remoteok/ # RemoteOK connector logic
βββ pkg/
β βββ models/ # Data models
β βββ storage/ # Database operations
βββ internal/
β βββ api/ # HTTP handlers
β βββ scheduler/ # Cron scheduler
βββ migrations/ # Database migrations
βββ docs/ # Documentation
βββ Dockerfile # Main API container
βββ docker-compose.plugins.yml # All services
βββ EASYPANEL_ENV_SETUP.md # Deployment guide
- QUICKSTART.md - 5-minute setup guide
- SETUP_GUIDE.md - Detailed setup instructions
- EASYPANEL_ENV_SETUP.md - Production deployment
- docs/ - Architecture and API documentation
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
MIT License - see LICENSE file for details.
Built with β€οΈ by @magnusfroste