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, and audit features. Outgoing mail
lands in a public Mailpit inbox and requests are
traced in Jaeger. It holds no real data and is
reset periodically. Stood up with scripts/deploy-demo.sh
(helm/cpp-env/values-demo.yaml).
- 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/jobs/dlqand/api/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
- 305 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, Trivy image scan, clang-format / clang-tidy lints, ASan + UBSan sanitizer build, helm-render and OpenAPI-drift gates. Therelease.ymljob 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/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.
- 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)
# Rename template identity (project name, image registry, helm charts, etc.)
./scripts/init-project.sh my-service docker.io/myorg
# 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 — drop
migrations/001_<topic>.sql. Seemigrations/README.mdfor naming anddocs/EXAMPLES.mdfor a workedusersexample. - Generate a controller —
./scripts/new-endpoint.sh FooController Get /api/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-quick— the fast TDD loop; runs against the cached test-runner image in ~5 s instead of the ~2 min cold rebuild ofmake test.- 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 |
Fast TDD loop — reuses cached image, ~5 s |
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/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/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 domain
(AuthController, AccountController, AdminController, AuditController)
next to 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/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 (auth → rate limit → idempotency → cors → trace)
Guards.hpp handler guard macros (API_REQUIRE_ADMIN / _PRINCIPAL / _JOBS_READY)
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, Health, Jobs (full auth/admin domain). 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
jobs/ Redis-backed job queue + DLQ
messaging/ Kafka producer/consumer
observability/ Logger, Prometheus, OpenTelemetry tracer, W3C Trace Context
security/ Auth (JWT/Bearer) + RateLimit + Idempotency
tasks/ Drogon-timer-based task scheduler
utils/ Config (JSON + env), Retry, Strings, ErrorResponse
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-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-quick |
Full / cached suite in Docker |
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 40, 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. The dev container already
mounts a vcpkg buildtrees + downloads volume, so no extra setup there.
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.