Enterprise baseline for a C++20 REST service. Fork it, point it at your Postgres/Redis, and start writing endpoints instead of reinventing auth, rate limiting, tracing, and DLQs.
Status: stable template (v1.x) — the structure and APIs are settled and meant to be forked as a base for real services; breaking changes follow SemVer. Issues and PRs welcome (see CONTRIBUTING.md).
A throwaway public demo runs at app.demo.tarassov.me —
register your own account, or sign in as admin@demo.tarassov.me /
DemoAdmin-2026 to explore the admin, RBAC, audit and content (posts +
uploads) features. Outgoing mail lands in a public
Mailpit inbox; requests are traced to the
cluster's Tempo and deep-linked from the admin UI into Grafana Explore. It
holds no real data and is reset periodically. Stood up with
scripts/deploy-demo.sh
(helm/cpp-env/values-demo.yaml) on shared cluster infra (Postgres + Redis
from a common namespace, isolated by role/database and REDIS_DB).
- Live demo
- Why this template
- What's in the box
- Quick start
- Authentication
- Adding an endpoint
- Ops CLI · Configuration · Observability
- Kubernetes
- Repo layout · Dev workflow
- Contributing · Security · License
C++ gives you the latency budget; this template gives you everything around the handler so you don't spend the first month rebuilding it:
| You could start from… | What you'd still have to build |
|---|---|
| Bare Drogon / Crow / oatpp | Auth, sessions, rate limiting, idempotency, migrations, DLQ, tracing, CI, charts — the framework ends where this template begins |
| userver | A batteries-included monolith framework with its own idioms; great inside Yandex-scale infra, heavyweight to adopt piecemeal |
| A Go/Python service | The 10x latency/footprint you came to C++ to avoid |
Good fit: an HTTP/JSON service that needs C++ performance plus the boring-but-mandatory production middleware, deployed via Docker/K8s. Poor fit: gRPC-first systems, GUI apps, embedded targets without Docker — the value here is the integration glue, and that glue assumes containers.
Performance is the reason to be here, and it's hardware-specific — measure it on
yours with make bench; docs/BENCHMARKS.md has the
methodology and a results template.
HTTP layer
- Drogon async HTTP server, multi-threaded event loops.
- Unified JSON error body (
{error, status, message, ...extras}) across every controller and middleware — no five-shape drift. - Composable request validators (
Api::Validation::*) that accumulate errors and return a single 400 with a structurederrorsarray. - One-line JSON responses (
Response::ok/Response::created) andValidation::parse_body— no hand-rolled HttpResponse boilerplate per endpoint. Cache-aside reads go throughCache::get()and are fail-open by convention (a Redis hiccup never blocks a request).
Security
- JWT (HS256) auth with
exp/nbf/iss/audvalidation and RBAC (Security::Auth::require_role). Static Bearer mode for dev;noneby default. - Redis-backed sliding-window rate limiter (atomic Lua, immune to the
2x fixed-window boundary burst) with per-IP / per-user scope,
X-RateLimit-*headers, fail-open / fail-closed choice. - Idempotency-Key middleware: body-hash keyed replay cache with 422 on same-key-different-body conflicts.
- Secrets never live in committed JSON — values are
${VAR}placeholders interpolated from env at load time.
Reliability
- Retry-with-backoff wrapper (
Retry::run) transparently applied toDatabase::execute_read/writewith pqxx / redis transient-error classifiers. - Dead-letter queue for jobs (
jobs:dlq:<type>), with/api/v1/jobs/dlqand/api/v1/jobs/dlq/{id}/requeueendpoints, and ajobs_dlq_depthPrometheus gauge. - Graceful shutdown: SIGTERM flips
/readyto 503, then Drogon drains after a configurable pre-stop delay. Worker version finishes the in-flight job before exiting.
Observability
- Prometheus metrics with cardinality-safe path normalisation.
- OpenTelemetry traces (OTLP HTTP exporter).
- W3C Trace Context: incoming
traceparentparsed (or generated) and echoed back asX-Request-Id+traceparenton every response. - Structured logs via spdlog with the trace-id in each access log line.
Testing
- 436 unit/integration tests against real Postgres + Redis, bucketed by
explicit suite filters (
make test-unitruns sidecar-free). - HTTP end-to-end suite: a real Drogon server + client exercising the
whole middleware chain on the wire — auth gate, cookie sessions, refresh
rotation/revocation, Idempotency-Key replay, permission bitmask, tracing
headers.
make test-e2e. - Frontend unit tests (vitest) for the session-refresh machinery and the
permission mirror;
package-lock.jsoncommitted for reproducible builds.
Ops
- Helm charts for the API, worker, and frontend, plus a
cpp-envumbrella that deploys all three (with in-cluster Postgres/Redis/etc.) as one environment.preStophook +terminationGracePeriodSeconds,ServiceMonitor, opt-inPrometheusRulewith baseline SLO alerts, opt-inExternalSecretskeleton for Vault / AWS / GCP secret stores. - GitHub Actions: build + unit/integration tests on
ubuntu-latest, gitleaks secret scan, clang-format / clang-tidy lints, ASan + UBSan and TSan sanitizer builds, a runtime-smoke image check, helm-render and OpenAPI-drift gates. Therelease.ymljob Trivy-scans and publishes the multi-arch (amd64 + arm64) image onv*tags — see the arch note under Kubernetes before deploying to an amd64 cluster. CODEOWNERS,SECURITY.md,CONTRIBUTING.md,CHANGELOG.md, PR templates.- Production config profile (
config/config.production.json) gated bymake prod-check— auth on, cookies secure, limiter fail-closed, docs UI off, non-weak secrets — before anything ships. - Prometheus alert rules out of the box (5xx rate, p99 latency, DLQ backlog, scrape-down, retry exhaustion).
- Renovate config: docker tags, GitHub-Action SHAs, npm lockfile and the vcpkg baseline stay current instead of fossilizing.
make warm-cachepulls the CI-built dependency image — first build in minutes instead of the ~30-minute cold vcpkg compile.
Account / Admin (flask-base parity)
- Register / login / logout / refresh / me — JWT in HttpOnly cookies, refresh-token rotation with Redis-backed revocation.
- Email confirmation, password reset, email change, password change via signed timed link tokens (libsodium-derived per-purpose keys).
- Admin user management (list / invite / detail / update / delete / roles) with self-protection (no self-delete, no self-role-change).
- Audit trail (
audit_log): every admin mutation on users and roles is recorded; read it viaGET /api/v1/admin/audit(paginated + filterable by action/actor/target/date), gated on a dedicatedkAuditReadpermission bit. - Email pipeline: SMTP via libcurl + inja templates; Mailpit dev
sidecar at http://localhost:8025. Account emails are delivered through
the jobs queue (
account_emailtype, handled bysrc/email/AccountEmailWorker.hpp) whenmail.via_jobsis on (the default) — so confirm/reset links get DLQ + retries — with an inline send fallback when Jobs is off or the enqueue fails. - Argon2id password hashing (libsodium).
Frontend (separate React SPA)
- Vite + React 18 + TypeScript + Tailwind + shadcn/ui primitives.
- TanStack Query + react-hook-form + zod + a hand-written fetch client
with openapi-typescript-generated types (codegen from
docs/openapi.yaml). - All flask-base pages ported: Login / Register / Confirm / Unconfirmed / Profile / Change Password / Change Email / Reset Password / Admin Dashboard / Users / User detail / Invite / Roles (CRUD), plus admin Jobs (paginated list + DLQ requeue) and Audit-log browsers beyond flask-base.
- Billing UI for the wallet/PayPal module: user wallet page (packages,
custom top-up, ledger history) + PayPal return/cancel pages, and admin
billing (packages CRUD, rate/bounds settings, payments list, manual
wallet adjustments). No charting dependency — the metrics dashboard is a
documented extension point over
GET /api/v1/admin/billing/metrics. - HttpOnly-cookie auth, no JWT in localStorage. Same-origin via Vite proxy in dev and nginx in prod — no CORS gymnastics.
- Lives in
frontend/(monorepo); builds to its own image, deploys independently behind nginx (frontend/Dockerfile).
Docs
docs/openapi.yaml— OpenAPI 3.1 for every registered route.docs/CONFIG.md— one table with every env var, JSON key, and default.docs/PATTERNS-FROM-FLASK-BASE.md— what we lift from flask-base and what we change.docs/adr/0005-spa-split.md— why the frontend lives in its own project.
Prerequisites: Docker + Docker Compose v2. On macOS the build runs in a
Linux VM (Docker Desktop or Colima) — give it ≥ 8 GiB of memory. The first
build compiles the C++ dependency set from source via vcpkg; under-provisioned
VMs (the Colima default is 2 GiB) OOM the builder and surface it as a cryptic
EOF / rpc error: Unavailable, which reads like a code bug but isn't. With
Colima: colima start --cpu 4 --memory 8. Run make doctor to check the
toolchain and the VM's memory, and make warm-cache to pull a prebuilt
dependency layer (falls back to the upstream template cache) so the first build
is minutes, not ~30.
git clone https://github.com/moveeeax/cpp-rapid-rest-template.git my-service
cd my-service
make doctor # verify Docker + VM memory before the first (cold) build
make warm-cache # optional: prime the vcpkg dependency layer (~30 min -> ~3)
# Or skip local toolchain setup entirely: "Reopen in Container" (VS Code) boots
# .devcontainer/ from the CI-published builder image — prebuilt vcpkg world
# included, `make test-local NAME='Foo*'` works out of the box (docs/TESTING.md)
# Rename template identity (project name, image registry, helm charts, etc.)
./scripts/init-project.sh my-service docker.io/myorg
# --no-demo also strip the flask-base reference material + README demo block
# --minimal --no-demo + remove the content module (posts/uploads/sitemap
# — the worked example); all sync gates stay green (REMOVING-THE-DEMO.md)
# --with-orgs install the multi-tenancy starter kit after the rename (docs/ORGS.md)
# Build + run Postgres + Redis + the API, wait for ready, hit /healthz
make quickstart
# Sanity check a few endpoints
make smoke
# Tail logs
make logsThe service listens on :8080 (HTTP) and :9090 (Prometheus /metrics).
Lean default — app + Postgres + Redis + Mailpit, Docker only, nothing built locally:
make up # minimal stack (AUTH_MODE=none — every route public, migrations auto-run)
make health # /healthz, /ready, head of /metrics
make frontend-up # optional: add the React SPA on :3001Or the whole experience in one command — auth (JWT) + SPA + worker + monitoring, zero config (heavy):
make up-everything # app, postgres, redis, mailpit, worker, replica, sentinel, kafka,
# frontend, monitoring. docker/.env.everything wires AUTH_MODE=jwt +
# cookies + mail automatically.
# create the first admin (migrations ran automatically on app startup)
docker compose -f docker/docker-compose.yml exec app \
./cpp_api_template --create-admin admin@local password
# open http://localhost:3001 — SPA, log in as admin@local / password
# open http://localhost:8025 — Mailpit (confirm / reset links land here)
# open http://localhost:3000 — Grafana (admin / admin)
# open http://localhost:16686 — JaegerNeed a middle ground? Start from make up and add only what you want — make up-monitoring,
make up-worker, make up-kafka, … see More stack variants.
To stop everything: make down. To wipe volumes too (DESTRUCTIVE, drops users): make down-v.
After init-project.sh you're on your own schema / business logic.
Fastest path — a whole CRUD resource in one shot:
./scripts/new-resource.sh Product (or make new-resource ENTITY=Product)
generates the migration, domain DTO, repository (on CrudBase), admin-gated
controller, route-registry row, OpenAPI block and an integration test —
following docs/CONVENTIONS.md. Then fill in your real
columns. The finer-grained steps below are for adding pieces individually.
Typical order:
- Add your first migration —
make new-migration SLUG=<topic>generates the nextmigrations/NNN_<topic>.sql(the template already ships 000–006). Seemigrations/README.mdfor naming anddocs/EXAMPLES.mdfor a workedusersexample. - Generate a controller —
./scripts/new-endpoint.sh FooController Get /api/v1/foocreates the stub, wires the include intosrc/api/Api.hpp, adds a row toApi::get_endpoints()insrc/api/Endpoints.hpp, and prints an OpenAPI stub. Pass--with-testto also emittests/api/test_<name>.cpp, and--patch-openapito insert the path block intodocs/openapi.yamlin place instead of just printing it. (Or usemake new-endpoint NAME=… METHOD=… PATH_=… WITH_TEST=1 PATCH_OPENAPI=1.) - Add a DTO + repository for anything touching SQL — follow the
pattern in
docs/EXAMPLES.md(domain struct withfrom_row+to_json, a repository that owns every SQL string and raises typed exceptions). make dev— rebuilds and restarts just the app container (no full stack restart, Postgres volume stays put).make test— rebuilds the test image with the docker layer cache (~2 min warm) and runs the full suite. For the fastest edit-compile-test loop use the nativemake test-local NAME='Foo*';make test-rerunre-runs the previous binaries without rebuilding (flake triage only — code edits do not land in it).- Verify migrations didn't drift —
docker compose exec app ./cpp_api_template --verify-migrationsexits non-zero if anything is pending. - Background work —
./scripts/new-job.sh reindex(ormake new-job TYPE=reindex) scaffolds a self-registering job handler undersrc/jobs/handlers/; it prints the one#includeto add tosrc/worker_main.cpp. Submit work withJobs::submit("reindex", payload).
| Command | When to use |
|---|---|
make test |
Cold run — rebuilds the test image, full suite, ~2 min |
make test-quick |
Alias for make test (rebuild with layer cache — honest, so edits actually run) |
make test-rerun |
Re-run binaries from the existing image — no rebuild, flake triage only |
make test-local NAME='Foo*' |
Fastest TDD loop — native incremental build + --gtest_filter |
make test-unit |
Only unit tests; skips anything needing Postgres/Redis |
make test-e2e |
HTTP end-to-end binary: real Drogon server + client, middleware on the wire |
| Command | Adds |
|---|---|
make up-replica |
Postgres read replica |
make up-sentinel |
Redis Sentinel (3 nodes) |
make up-kafka |
Kafka + Zookeeper |
make up-worker |
Background-job worker |
make up-monitoring |
Prometheus + Grafana + Jaeger |
make frontend-up |
React SPA (built + served by nginx on :3001) |
make up-everything |
All of the above + frontend (uses docker/.env.everything) |
All profiles share the same make down / make down-v.
Default is none — every endpoint is public. Flip one env var:
# Static bearer token (simplest)
docker compose run --rm --service-ports \
-e AUTH_MODE=bearer -e AUTH_BEARER_TOKEN=dev-secret-123 app
curl -H 'Authorization: Bearer dev-secret-123' http://localhost:8080/api/v1/jobsOr JWT HS256:
docker compose run --rm --service-ports \
-e AUTH_MODE=jwt \
-e JWT_SECRET=change-me \
-e JWT_ISSUER=my-issuer \
-e JWT_AUDIENCE=my-api app
# Mint a test token from the host; no Python or Node needed
TOKEN=$(make jwt SECRET=change-me ROLES=admin)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/jobsProtected endpoints that require a specific role:
void deleteAccount(const HttpRequestPtr& req, ...) {
if (auto err = Security::Auth::require_role(req, "admin")) {
callback(err); // 403 {error: "forbidden", required_role: "admin"}
return;
}
// ...
}See docs/openapi.yaml for which endpoints have BearerAuth
in their security block.
The template already ships a full auth / RBAC / admin / audit / API-key domain
(AuthController, AccountController, AdminController, AuditController,
ApiKeyController), a content module behind content.enabled
(PostsController, UploadController, ContentPagesController), and the
infrastructure controllers (HealthController, JobsController) —
you start from a working example, not a blank src/api/. For your resource, a
worked CRUD stack — typed DTO + repository + controller + migration + test —
lives in docs/EXAMPLES.md. Copy from there, don't try to
invent it from scratch.
- Add the route to a controller (existing or new) under
src/api/:METHOD_LIST_BEGIN ADD_METHOD_TO(MyController::listThings, "/api/v1/things", Get); METHOD_LIST_END
- Register the new route in
Api::get_endpoints()insrc/api/Endpoints.hpp(the route registry, surfaced at/). - Update
docs/openapi.yaml— manually, and CI holds you to it:scripts/check-openapi-drift.shfails the pipeline on any (method, path) mismatch between spec andApi::get_endpoints(). - If the endpoint mutates, validate inputs through
Api::Validation::*and return errors viaApi::Validation::response_400(errs)for bulk errors orErrorResponse::{bad_request,not_found,...}()for single-shot. - Add a test in
tests/api/(controller methods viaTestHelpers::make_request),tests/integration/(real Postgres/Redis), ortests/e2e/(the full Drogon server over real HTTP — middleware chain included).
Run the binary with any of these to bypass the server loop:
| Flag | Effect |
|---|---|
--print-routes |
Print the registered endpoint table and exit. No DB/Redis required. |
--dump-config |
Resolve config (JSON + env overrides) and print as JSON. No subsystems. |
--verify-migrations |
Connect to the DB and list migrations not yet applied. Exits 1 if any are pending — handy as a CI gate. |
--run-migrations |
Apply all pending migrations and exit. Same effect as RUN_MIGRATIONS_ONLY=1 env, surfaced as a flag for native dev / make migrate-local. |
--help / -h |
Show usage. |
The positional arg (if present, before flags) is the config path, same as the default boot mode.
See the full table in docs/CONFIG.md. Short version:
- Defaults live in
config/config.jsonwith${VAR}placeholders. - Env vars override everything.
- Per-deployment overlay: drop a
config/local.json, pointCONFIG_FILEat it.config/local.jsonis git-ignored.
With make up-monitoring:
- Metrics: http://localhost:9094 (Prometheus), http://localhost:3000 (Grafana, admin/admin)
- Traces: http://localhost:16686 (Jaeger UI)
- Every HTTP response carries
X-Request-Id— use that to correlate logs/traces.
The app emits OTLP_ENDPOINT-tuned OTLP HTTP to whatever you configure; default
http://jaeger:4318/v1/traces in the with-monitoring profile.
Four charts:
helm/cpp-api— the HTTP servicehelm/cpp-worker— the background-job workerhelm/cpp-frontend— the React SPA (rootless nginx)helm/cpp-env— umbrella that deploys all three as one environment (plus in-cluster Postgres / Redis / Mailpit / Jaeger / Kafka); seemake helm-validate
Render locally:
helm template api helm/cpp-api --set image.repository=my-registry/cpp-api
helm template worker helm/cpp-worker --set image.repository=my-registry/cpp-apiFirst prod deploy — start from the example overlays. Each chart ships a
tracked, secret-free values-prod.example.yaml. Copy it, fill in the TODOs
(hosts, image, datastore endpoints), and deploy:
cp helm/cpp-api/values-prod.example.yaml helm/cpp-api/values-prod.yaml # gitignored
helm upgrade --install api ./helm/cpp-api -n prod -f helm/cpp-api/values-prod.yaml \
--set externalDatabase.password="$DB_PASSWORD" \
--set externalRedis.password="$REDIS_PASSWORD" \
--set auth.jwtSecret="$JWT_SECRET"
# repeat for cpp-worker (its datastore/auth MUST match) and cpp-frontendImage architecture — match it to your nodes. CI builds the image only to run
tests (no publish); the release.yml tag job publishes a multi-arch (amd64 +
arm64) manifest on v* tags. If your cluster is amd64 (the example overlays'
nodeSelector assumes it) you must deploy an amd64 image — pull from a multi-arch
tag, or build for amd64 yourself:
docker buildx build --platform linux/amd64 --target runtime \
-t your-registry/your-project:$(git rev-parse --short HEAD)-amd64 --push .A mismatch shows up as exec format error / CrashLoopBackOff on first roll-out.
Per-cluster secrets and overrides go in untracked files — helm/**/values-prod.yaml,
helm/values.*.yaml, and helm/*.local.yaml are all gitignored. Either pass
real secrets via --set / a private values file, or wire externalSecrets
to Vault / AWS Secrets Manager / etc. Never put a real secret in a tracked
*.example.yaml.
Opt-ins you'll almost certainly want in prod:
autoscaling.enabled=true(HPA)pdb.enabled=true(PodDisruptionBudget)networkPolicy.enabled=true— tune the selectors for YOUR cluster namespaces firstserviceMonitor.enabled=true+monitoring.alertsEnabled=true(Prometheus-operator CRDs)externalSecrets.enabled=true— pull DB/Redis/JWT secrets from Vault or similar
The SECURITY.md hardening checklist walks through every one.
src/
api/ HTTP controllers + endpoint registry + middleware pipeline
Api.hpp controller includes + middleware wiring (register_controllers)
Endpoints.hpp get_endpoints() — the route registry (drift-checked vs openapi.yaml)
Middleware.hpp middleware bodies (request id → content type → auth → csrf → rate limit → idempotency → cors)
Guards.hpp handler guards (API_REQUIRE_ADMIN / _PRINCIPAL / _JOBS_READY macros, require_content_enabled, require_valid_uuid)
RequestUtils.hpp parse_int, clamp_int, parse_page_params, is_valid_uuid, normalize_path_for_metrics
Validation.hpp composable request-body validators
*Controller.hpp Built-in controllers: Auth, Account, Admin, Audit, ApiKey, Health, Jobs + the content module (Posts, Upload, ContentPages). Add your own with scripts/new-endpoint.sh; docs/EXAMPLES.md walks through a full Users CRUD.
cache/ Redis client (standalone or Sentinel HA)
core/ Application lifecycle (init, health, shutdown)
database/ Postgres pool + migrations
domain/ Domain structs (User, Role, AuditEntry, ApiKey, Post)
email/ SMTP mailer + account-email templates and the account_email job worker
jobs/ Redis-backed job queue + DLQ
messaging/ Kafka producer/consumer
observability/ Logger, Prometheus, OpenTelemetry tracer, W3C Trace Context
repositories/ SQL repositories (CrudBase, User/Role/Audit/ApiKey/Post, typed SQL errors)
security/ Auth (JWT/Bearer) + RateLimit + Idempotency
storage/ Object/file storage seam (local-disk backend, S3/GCS-swappable)
tasks/ Drogon-timer-based task scheduler
utils/ Config (JSON + env), Retry, Strings, ErrorResponse
webhooks/ Outbound webhooks (HMAC-signed, delivered via the job system)
tests/
unit/ Pure C++ tests (no external services)
integration/ Tests that need real Postgres / Redis
api/ Controller-method tests via TestHelpers::make_request
e2e/ Real Drogon server + HTTP client (separate binary)
docker/ Dockerfile + docker-compose.yml + env presets
helm/ Helm charts (cpp-api, cpp-worker, cpp-frontend + cpp-env umbrella), values documented
scripts/ make-jwt.sh, smoke.sh, init-project.sh, bench.sh,
new-resource.sh (full CRUD), new-endpoint.sh (single
controller, --with-test / --patch-openapi), new-job.sh
(job handler), new-module.sh (feature module),
new-react-page.sh, new-migration.sh,
check-openapi-drift.sh, lint-openapi.sh, env-check.sh
docs/ openapi.yaml, CONFIG.md, EXAMPLES.md, INDEX.md, adr/, Doxyfile
| Command | What it does |
|---|---|
make up / make up-dev |
Start base stack (and worker, with up-dev) |
make doctor |
Sanity-check the local toolchain (cmake, ninja, jq, vcpkg, …) |
make env-check |
Report ${VAR} placeholders in config/config.json that are unset and have no default |
make prod-check |
Semantic production gate: auth on, secure cookies, fail-closed limiter, strong secrets |
make warm-cache |
Pull the CI-built builder image — skip the cold vcpkg compile |
make build-local |
Native cmake build via the dev preset, no Docker |
make compile-commands |
Generate compile_commands.json for clangd |
make test / make test-rerun |
Full suite in Docker / rerun-only (no rebuild, flakes) |
make test-local NAME=Jobs* |
Native gtest run with a --gtest_filter |
make test-watch |
Re-run unit tests on src/ or tests/ change (watchexec or entr) |
make watch |
Rebuild + restart on src/ change (entr or watchexec) |
make coverage |
gcovr HTML report in coverage/index.html; fails under COVERAGE_MIN% line coverage (default 54, a regression floor) |
make ci-local |
Reproduce CI locally: format check + drift + spectral + tidy + tests |
make helm-lint |
helm lint + smoke helm template over all four charts |
make routes / make health |
Print endpoint table / hit health probes |
make psql / make redis-cli |
Open a shell against the running stack |
make migrate / make migrate-local / make migrate-status / make migrate-reset |
Run (Docker or native) / inspect / nuke-and-reapply migrations |
make init NAME=… [REGISTRY=…] |
Rebrand the template for your fork via scripts/init-project.sh |
make new-resource ENTITY=Product |
Scaffold a full CRUD resource via scripts/new-resource.sh |
make new-endpoint NAME=… METHOD=… PATH_=… [WITH_TEST=1] [PATCH_OPENAPI=1] |
Scaffold a controller via scripts/new-endpoint.sh |
make new-job TYPE=… [HANDLER=…] |
Scaffold a background-job handler via scripts/new-job.sh |
make new-migration SLUG=… |
Generate the next migrations/NNN_<slug>.sql |
make seed |
Apply optional migrations/seeds/*.sql fixtures |
make logs / make logs-pretty / make logs-worker |
Tail logs (json-pretty via jq, worker variant) |
make tail-trace TID=<id> |
Filter app logs by a specific trace id |
make fmt / make lint / make tidy / make lint-openapi |
clang-format / format-check / clang-tidy / spectral |
make jwt SECRET=... ROLES=admin |
Print a dev JWT to stdout |
make dev-token SECRET=... ROLES=admin |
Mint a JWT into .dev-token (gitignored, picked up by make smoke) |
make smoke |
curl through critical endpoints (uses .dev-token if present) |
make clean / make dist-clean |
Wipe build/, logs, Doxygen output (+vcpkg_installed for dist-clean) |
make down / make down-v |
Stop everything (and wipe volumes with down-v) |
make help prints this table at any time.
Docker path (recommended): make warm-cache pulls the dependency image
your CI already built, so make build / make test reuse those layers and
skip the ~30-minute cold vcpkg compile entirely.
Native path — vcpkg binary cache:
A cold local build pulls and compiles every dependency once. Wire up vcpkg's binary cache so subsequent builds (and CI clones) reuse the artefacts:
mkdir -p ~/.vcpkg-bincache
export VCPKG_BINARY_SOURCES='clear;files,~/.vcpkg-bincache,readwrite'
make build-local # first run populates the cache
make dist-clean && make build-local # second run is near-instantPersist this in your ~/.zshrc / ~/.bashrc.
Dev container path — zero setup: .devcontainer/ boots straight from the
CI-published builder image (ghcr.io/…/builder:cache) with the vcpkg world
prebuilt inside — no VCPKG_ROOT, no cold compile; make test-local NAME='Foo*' works from the first minute (see docs/TESTING.md).
See CONTRIBUTING.md. TL;DR: conventional commits, clang-format
clean, tests passing, don't break the error-response shape without updating
docs/openapi.yaml.
See SECURITY.md — private disclosure via security@tarassov.me,
plus a production-hardening checklist.
MIT. See LICENSE. Third-party dependencies and their licenses (plus the flask-base attribution for the ported account/admin surface) are listed in THIRD_PARTY_NOTICES.md.