A multi-tenant recruitment platform. Companies get isolated careers portals, candidates get one central job portal, and the platform is operated from a separate administration console.
apps/
web-company/ A company's own recruiting portal — {slug}.{host} (Next.js)
web-jobs/ The candidate job portal — jobs.{host} (Next.js)
web-admin/ The platform administration console — root.{host} (Next.js)
web-landing/ Marketing, pricing and both sign-ups — {host} (Next.js)
services/
gateway/ API gateway — host routing, tenancy, token verification (Go)
identity/ Accounts, sessions, roles, permissions, a company's team (Go)
jobs/ Requisitions, dual visibility, custom application forms (Go)
applications/ Pipeline, dynamic stages, one-application-per-job (Go)
candidates/ Profiles, resumes, discoverability, talent search (Go)
messaging/ Recruiter ↔ candidate conversations (Go)
notifications/ In-app, SSE stream, email fan-out (Go)
interviews/ Rounds, panels, scorecards (Go)
offers/ Packages, approval, dispatch, response (Go)
audit/ Every platform event, per tenant and platform-wide (Go)
companies/ Registration, profile, careers portal settings (NestJS)
subscriptions/ Plans, subscriptions, entitlements (NestJS)
payments/ Provider-agnostic billing, invoices, webhooks (NestJS)
support/ Support tickets, company and platform sides (NestJS)
admin/ Root console aggregation over projections (NestJS)
packages/
goshared/ Shared Go platform library
nestshared/ Shared TypeScript platform library (the NestJS counterpart)
ui/ Shared front-end layer: API client, access evaluator, transport
scripts/ dev.sh (run the stack), smoke.sh + smoke-product.sh (live suites)
deploy/ Infrastructure, service images, local orchestration
docs/ Architecture, service contract, front ends, RBAC
Both shared libraries expose the same concepts — problem+json, the gateway principal, the permission guard, cursor pagination, the migration runner, the event envelope — so a service behaves the same way whichever runtime it is written in.
No front end holds a database. There is no connection string in any app, and there is not meant to be one: a portal that could read a table could read another tenant's rows, and the boundary that stops that lives at the gateway.
Requires Go 1.26+, Node 20+, pnpm, and Docker.
make setup # signing keys, .env, and the infrastructure containers
make dev # infrastructure, every service, and the company portalmake dev runs the backend and web-company on :3000. The other three portals
are separate Next servers you start yourself when you need them:
pnpm --filter @reqruitbook/web-jobs dev # :3001
pnpm --filter @reqruitbook/web-admin dev # :3002
pnpm --filter @reqruitbook/web-landing dev # :3003make dev discovers every service in the tree, builds it, and waits for its
readiness endpoint before starting the next. Three behaviours are deliberate:
- A service that does not compile or start is skipped, loudly, by name. One service still being written then costs you its own routes — which answer 502, exactly as a deployed-but-down service would — instead of the whole platform.
- A port held by something
dev.shdid not start is left alone, and that service is skipped. Killing an unknown process would be worse than not starting one. make dev-downsweeps those ports anyway. Stopping is explicit intent, so it clears stale listeners a crash or a hand-started process left behind. Without that sweep the skip above quietly leaves an old binary serving while a freshmake devreports success — a confusing way to lose an afternoon.
make status # what is running, and on which port
make smoke-all # both test suites against the running stack
make dev-down # stop the services (infrastructure keeps running)Two suites run against the live stack:
make smoke |
Security boundaries: cross-tenant tokens, header spoofing, portal isolation, credential enumeration, refresh replay |
make smoke-product |
The product: post a job, publish it, apply, advance the pipeline, reject, discover talent, schedule a round, file scorecards, draft an offer, read the audit trail |
Migrations run automatically at service boot, so make migrate is only needed
to apply them without starting anything. It depends on make db, which creates
any service database a running Postgres is missing — the container's init script
only runs on a fresh volume, so a service added later would otherwise fail to
start with an error that says nothing about the cause.
To run the services by hand instead, in separate terminals:
make run-identity # :8081
make run-gateway # :8080make setup writes bootstrap credentials into .env. The identity service
creates that administrator on first boot and logs a warning; sign in, change the
password, then remove the BOOTSTRAP_ADMIN_* lines.
curl -X POST http://localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-H 'Host: root.reqruitbook.local' \
-d '{"realm":"platform","email":"admin@reqruitbook.local","password":"..."}'Portals are routed by hostname. For browser testing, add them to /etc/hosts:
127.0.0.1 reqruitbook.local root.reqruitbook.local jobs.reqruitbook.local acme.reqruitbook.local
With curl, a Host: header is enough. web-company does not need the hosts
entry at all: reached at localhost:3000 its hostname carries no slug, so
COMPANY_SLUG in its .env says which company the dev server is the portal
for.
Append the line as its own line. Adding it to the end of an existing one —
which is what echo ... >> /etc/hosts does when the file has no trailing
newline — makes the whole line a comment, and nothing resolves.
| Command | Description |
|---|---|
make setup |
Keys, .env, and infrastructure |
make dev / make dev-down |
Start or stop the whole stack |
make status |
Show what is running |
make smoke / make smoke-product / make smoke-all |
Test suites against a running stack |
make infra / make infra-down |
Start or stop the containers |
make infra-reset |
Destroy the volumes and start clean |
make db |
Create any service database a running Postgres is missing |
make migrate |
Apply migrations for every service |
make build / make test |
Build and test the Go services |
make verify |
Everything CI runs: build, vet, gofmt, tests, and the two generated-file checks |
make images |
Build every service image, to prove the deployment path still works |
make stack-up / make stack-down / make stack-logs |
Run the whole platform in containers |
make run-gateway / make run-identity |
Run a single service |
| Service | Address |
|---|---|
| Gateway | http://localhost:8080 |
| Company portal | http://localhost:3000 |
| Candidate portal | http://localhost:3001 |
| Admin console | http://localhost:3002 |
| Marketing site | http://localhost:3003 |
| Identity | http://localhost:8081 (services run on 8081–8094) |
| Postgres | localhost:5432 |
| Redis | localhost:6379 |
| NATS | localhost:4222 (monitor :8222) |
| MinIO | http://localhost:9001 |
| Mailpit | http://localhost:8025 |
| Jaeger | http://localhost:16686 |
- Platform architecture — services, tenancy, request path, security
- Service contract — what every service must do to compose with the platform, and the traps that cost an hour each
- Front ends — the four portals, the Host-header rule they all depend on, and what they still cannot do
- Feature-based RBAC — the permission model used by the company portal