A production-inspired distributed job scheduling platform for reliably executing asynchronous background jobs across multiple workers, with authentication, project/queue management, retries, dead-letter handling, and a live dashboard.
- Quick Start
- Tech Stack
- Architecture
- Project Structure
- Manual Setup
- Testing
- Deployment
- Rolling Back Migrations
- Documentation
Docker is the only prerequisite. One command brings up the database, cache, migrations, API, worker fleet and dashboard:
make upThen open http://localhost:3000 and sign in with admin@example.com /
password123 (set in .env).
To verify the whole system really works — submit a job and watch a worker execute it:
make smokeOther common commands:
make logs # tail every service
make logs SVC=worker # tail just the workers
make scale-workers N=5 # run five workers
make down # stop, keeping data
make clean # stop and delete the database
make help # everything elseFull details, including how to deploy to a server, are in DEPLOYMENT.md.
| Layer | Technology |
|---|---|
| Backend API + Worker | Go |
| Frontend | React |
| Database | PostgreSQL (via pgx/v5 + pgxpool) |
| Rate Limiting | Redis |
| Migrations | golang-migrate |
| Router | chi |
| Auth | JWT (golang-jwt) + bcrypt |
| Cron parsing | robfig/cron |
| Distributed locking | Postgres advisory locks (scheduler leader election) |
| Containers | Docker + Docker Compose |
| CI/CD | GitHub Actions, images published to GHCR |
Two independent Go binaries share a common internal/ package:
cmd/api: the REST API server. Auth, projects, queues, jobs, dashboard data.cmd/worker: belongs to exactly one org (WORKER_ORG_ID), polls that org's queues, atomically claims jobs withSELECT ... FOR UPDATE SKIP LOCKED, executes them concurrently, sends heartbeats, and shuts down gracefully onSIGTERM.
This split mirrors a real deployment, where the API and each org's worker fleet need to scale independently of each other.
The job lifecycle state machine lives in the backend architecture doc.
server/ # Go API + worker, see server.md
Dockerfile # builds both binaries into one image
ui-interface/ # React dashboard (Vite), see ui_interface.md
Dockerfile # builds the SPA, serves it with nginx
deploy/scripts/ # bootstrap and smoke-test helpers
.github/workflows/ # CI, CD and CodeQL pipelines
docker-compose.yml # the stack
docker-compose.override.yml # local development (applied automatically)
docker-compose.prod.yml # production overlay
Makefile # every task; run `make help`
make up handles all of this for you — these steps are for running the
services directly on your machine instead.
cd server
go mod tidycp .env.example .env # from server/
cp ../ui-interface/.env.example ../ui-interface/.env # frontendMake sure both are running locally, or via Docker:
docker run -d --name js-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:15
docker run -d --name js-redis -p 6379:6379 redis:7docker exec js-postgres createdb -U postgres jobschedulerFrom server/:
migrate -path migrations -database "$DATABASE_URL" upFrom server/:
go run ./cmd/apicd ui-interface
npm install
npm run devOpen the frontend, register an org, and copy the org_id from the
response. It's visible in your browser's dev tools: the Network tab on the
register call, or Local Storage under the user key. A worker belongs to
exactly one organization, so this ID is required before one can start.
From server/, with the org ID from the previous step:
WORKER_ORG_ID=<your-org-id> go run ./cmd/workerOr set WORKER_ORG_ID in server/.env instead of passing it inline.
Either way, cmd/worker refuses to start without it.
From server/:
go test ./...See the backend doc for what runs unconditionally vs. what needs a real Postgres/Redis, and how to point tests at a throwaway database.
From ui-interface/, there's no test runner configured, only lint:
npm run lintThe project ships as two Docker images (api and web) built and published
by GitHub Actions. docker-compose.prod.yml runs them on any Linux box with
Docker installed — no orchestrator needed.
DEPLOYMENT.md covers it end to end: how the containers fit together, every configuration variable, deploying to a server, what each CI/CD workflow does, rollbacks, backups and troubleshooting.
From server/:
migrate -path migrations -database "$DATABASE_URL" down 1| Doc | Covers |
|---|---|
server.md |
Backend structure, cmd//internal/ breakdown |
server/docs/architecture.md |
Component diagram, job lifecycle state machine |
server/docs/er-diagram.md |
Full ER diagram, keys, indexes, cascade behavior |
server/docs/api.md |
Every REST endpoint: request/response shapes, error codes, pagination |
server/docs/design-decisions.md |
Trade-offs: SKIP LOCKED vs. an external queue, per-queue concurrency locking, Redis rate limiting, the advisory-lock scheduler, cascade-vs-soft-delete |
ui_interface.md |
Frontend feature list, structure, env vars |
ui-interface/docs/getting-started.md |
Registering an org, starting a worker, creating a project/queue, all four job types, cron schedules, concurrency |
DEPLOYMENT.md |
Docker images, compose stack, configuration, deploying to a server, CI/CD pipelines, rollbacks, troubleshooting |
