This walkthrough starts the reference application, talks to the HTTP API over TLS,
authenticates, makes an authenticated call, renews the certificate without a restart
and points out what has to change for a production deployment. It complements the
Architecture page (why the
pieces look the way they do) and swagger.yaml (the wire contract); this page is the
"make it work" path.
Everything below uses the repository's bin/main, which wires
StartupBuilder with PropertyFeature, BaseFeature, HttpFeature and
CommandLineFeature and reads cfg/bootstrap.xml.
-
A built checkout (
bin/mainpresent). See Building the Project. -
The development certificate pair, created once:
./cfg/certs/generate_certs.sh
cfg/bootstrap.xmlsetsrequire_tls="true", so the server refuses to start without the pair. -
An admin password.
maincreates theADMIN_USERNAMEuser on first start whenADMIN_PASSWORDis set; both come from.env(copy.env.example). See Running the examples locally.
cp .env.example .env # set ADMIN_PASSWORD (and CONFIG_DIRECTORY)scripts/run-example.sh # loads .env, generates certs if needed, runs bin/mainmain also starts the interactive CLI and the worker processes declared in
cfg/bootstrap.xml, so run it from a terminal. The API listens on
https://localhost:8080. All examples below use curl -k, because the development
certificate is self-signed.
/health is a liveness probe: it answers 200 as long as the process is alive and
never touches a dependency, so a broken database cannot make a supervisor restart a
healthy process.
curl -fsSk https://localhost:8080/health
# {"status":"ok"}/ready is the readiness probe. It runs the checks registered at startup — a
SELECT 1 ping per configured database connection (database:<name>) and
shared_memory for the mapped segments — and answers 503 while one of them fails:
curl -fsSk https://localhost:8080/ready
# {"status":"ready","checks":{"database:DEFAULT_SQLITE":"ok","shared_memory":"ok"}}
curl -sSk -o /dev/null -w '%{http_code}\n' https://localhost:8080/ready # 503 while not readyUse /health for the liveness probe and /ready for the readiness probe of an
orchestrator; mixing them up is the classic way to build a restart loop.
POST /user/login authenticates with HTTP Basic and returns the user as a UserDto
whose id field is the bearer token:
TOKEN=$(curl -fsSk -X POST -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
https://localhost:8080/user/login | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
curl -fsSk -H "Authorization: Bearer $TOKEN" https://localhost:8080/user/state
# 200 with an empty body while the token is valid; 401 once it is notThe token is what Authorization: Bearer <token> expects on every protected
endpoint. A few things are worth knowing:
- Invalid credentials, a malformed Basic header, or a login while the account already
holds a valid token all answer
403(not401); see thePOST /user/loginresponses inswagger.yaml. - The token is bound to the address the client connected from. A request with the same
token from a different address is rejected — which is why
trusted_proxiesin the bootstrap configuration has to name exactly the proxies in front of the process (otherwise every client looks like the same address, or a client can pick its own address throughX-Forwarded-For). - Failed logins are rate-limited per address and locked out after too many failures; the details are in the Architecture page (Authentication and sessions).
Log out to release the session:
curl -fsSk -X DELETE -H "Authorization: Bearer $TOKEN" https://localhost:8080/user/logoutGET /user/users lists users; with limit (and after for the following pages) the
answer is a keyset-paginated envelope:
curl -fsSk -H "Authorization: Bearer $TOKEN" 'https://localhost:8080/user/users?limit=2'
# {"items":[...],"has_more":true,"next_after":"<user_name>"}The same pattern applies to the other groups of endpoints (swagger.yaml is the
complete list): /properties/... reads and writes configuration values,
/process/... inspects and controls the managed processes, /shm/... reads shared
memory and /history/... returns the history a worker produced.
(One entry in swagger.yaml is not served by bin/main: /hello belongs to
lintel::ExampleController, which an application registers itself — it is a minimal
Controller example, not part of HttpFeature.)
The certificate pair is read once at startup, so replacing the files on disk does nothing for a running process. Reload it in place:
# replace cfg/certs/server.crt and server.key first, then:
curl -fsSk -X POST -H "Authorization: Bearer $TOKEN" https://localhost:8080/admin/reload-tls
# {"status":"reloaded","results":{"tls":"ok"}}409 means the process serves clear text (nothing to reload), 500 with a reason
means the pair could not be read or OpenSSL rejected it — in that case the previous,
working pair stays in effect. The endpoint needs the admin group, like the other
/admin/... routes.
- Use
cfg/bootstrap_production.xmlas the starting point instead ofcfg/bootstrap.xml. It forces TLS, reads the PostgreSQL credentials and the log/shared-memory directories from the environment (${VAR}placeholders) and ships no fallback credentials. Never commit real credentials;scripts/check-secrets.shfails CI if a plaintext one appears in a configuration file or a PEM block. - Terminate TLS either in the process (as
maindoes) or in front of it. If a proxy terminates it, keeprequire_tls="true"only for direct TLS and settrusted_proxiesto the proxy addresses; a range that is too wide turns the per-address brute-force protection into no protection. - Wire
/healthto the liveness probe and/readyto the readiness probe, and make sure the dependencies behind/readyare the ones that actually have to be up. - Keep the request body cap in mind: the server accepts request bodies up to 8 MiB and rejects larger ones instead of buffering them.
| Symptom | Cause |
|---|---|
| Startup fails with a TLS/certificate error | cfg/certs/server.{crt,key} missing — run ./cfg/certs/generate_certs.sh |
403 from POST /user/login |
Wrong credentials, malformed Basic header, or the account already holds a valid token (log out first) |
401 on a protected endpoint |
Missing, malformed or expired bearer token, or the token is used from another address |
503 from /ready |
A registered dependency check failed; the body names the check and the reason |
409 from POST /admin/reload-tls |
The process runs without TLS; there is nothing to reload |
500 from POST /admin/reload-tls |
The new pair is unreadable or OpenSSL rejected it; the previous pair stays active |