ADR 0039: the family's instance halkyon.li on balthasar, with Learn, Todo, Train, Meditate and Jellyfin, sign-up by approval through SimpleLogin alone, published through ramiel by WireGuard with TLS passed through by name (HAProxy), the CI runner moved there in a nightly window, deployed by hand. Supersedes ADR 0032, whose server end goes with sahaquiel. ADR 0040: one Instance row in every app's Settings opening a route with accounts, sign-up, assistants, regions and state; the instance's settings in a file outside the backups, the keys staying in assistants.env; regions from CoMaps' polygons, which the sidecars build themselves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
11 KiB
ADR 0040: The Instance console: accounts, sign-up, assistants, regions and state in the apps
- Status: decided 8 Nov 2026 by the developer: D1 to D4 and D17 to D18 are their own choices, D5 to D16 the agent's recommendations, which they confirmed. Nothing is built yet.
- Builds on: Halkyon Learn's ADR 0021 (operators, approval, the assistants and their
grants:
InstanceOperatorEndpoint,AssistantOperatorEndpoint, the operator's card, badge and screen inhalkyon_app), ADR 0016 (the register andINSTANCE_SECRET), ADR 0030, D44 and D50 (BRouter, the tiles and Photon for Train), ADR 0033, D32 (Todo's search on the same Photon), ADR 0039 (halkyon.li, whose regions come from here, D5, D19).
Context
An instance's operator already approves, refuses and suspends accounts, names operators and grants assistants with budgets, in every app (ADR 0021). Everything else about the instance is in files on its host, changed over SSH, each change followed by a restart:
- who may sign up (
SIGN_UPindeploy/.env); - which assistants and models are offered, with their keys (
deploy/assistants.env); - the regions of Train's map, routes and search:
MAP_REGIONandBROUTER_REGION(boxes in degrees) andPHOTON_REGION(a country of GraphHopper's dumps), Europe by default.
Nothing in the apps shows whether the sidecars run or when the last backup was made.
halkyon.li (ADR 0039) runs on a home machine with 7.2 GB of RAM and 150 GB of free disk,
where Europe's tiles (45 GB, twice while cut) and Europe's Photon (8 GB of memory) don't
fit. Its family needs a few areas, smaller than a country: Upper Bavaria, South, for
instance. CoMaps (Organic Maps' fork) cuts the world into 1,159 such areas for its own
downloads, each a polygon in data/borders/*.poly, named by its place in the hierarchy
(Germany_Free State of Bavaria_Upper Bavaria_South, 500 KB). The repository is under
Apache 2.0; the borders come from OpenStreetMap's boundaries, so they carry
"© OpenStreetMap contributors", as the tiles do.
How each sidecar can be cut:
- Tiles:
pmtiles extracttakes a GeoJSON polygon (--region) instead of a box. - BRouter: its segments are 5° by 5° and can't be cut. A region takes every segment its polygon touches.
- Photon: GraphHopper's dumps stop at countries. Anything smaller is an index of our
own: an extract of OpenStreetMap cut to the polygon (
osmium extract -p), imported into a Nominatim database, which Photon then imports from (ADR 0039, D19).
Decisions
The developer's (8 Nov 2026)
- D1 One row in Settings, a route of its own: operators alone see a single row,
Instance, in every app's Settings, with the count of accounts waiting as its subtitle
and badge (replacing today's This instance card). It opens the Instance route, with
Accounts, Sign-up, Assistants, Regions and State. On a phone each opens a
screen of its own; on a wide window the route shows the list and the chosen part side by
side. The badge on the Settings cog stays. Not in Settings itself (too much there), not a
web console, not Django: the module's endpoints and
halkyon_app's screens, as for everything an operator does today. - D2 The instance's settings are not backed up; what belongs to accounts is: sign-up, the assistants offered and the regions are kept in a file of their own (D5), outside the database and every backup. Approvals, operators, grants, budgets and figures stay in the database and so in the backups, since a restore without them would leave everyone waiting.
- D3 The assistants' keys stay in
assistants.env: the console shows whether each assistant's key is set, never the key. - D4 Regions are added and removed in the console, from CoMaps' polygons. halkyon.li starts with none, and Train's and Todo's map, routes and search with them.
Recommended by the agent, confirmed by the developer
-
D5 The settings file:
settings.jsonin a volume of its own,instance_settings, mounted into the register's server alone (Learn's, ADR 0016). It is written whole into a new file and moved over the old, so it is never read half written.backup.shleaves the volume out. It holds no secret. Each setting comes from the file once the console has set it, otherwise from.env, as today. The console shows which applies (Set here or From the server's configuration) and can go back to.env's. A lost file thus returns the instance to its.env, and the regions to none. -
D6 The members ask the register: Todo's, Train's and Meditate's servers read the settings that concern them (sign-up, which every server enforces at sign-in, and the assistants, which Learn's server alone forwards today) from the register over the instance's own network, with
INSTANCE_SECRET, as they ask it about accounts now. They cache an answer for a minute. A change thus takes at most a minute, with no restart. -
D7 Sign-up: Open, By approval or Closed, a choice on the screen, applied within a minute. Closing it leaves waiting accounts waiting; they are approved or refused under Accounts as before.
-
D8 Assistants: each assistant's name, provider (protocol and host), model, efforts allowed, order and the ceilings of a request are kept in the file; its key stays
ASSISTANT_<ID>_KEYinassistants.env. Adding an assistant in the console makes it Key missing until its key is in the file and the server restarted: the one restart left, as keys never pass through the app. Renaming an ID stays impossible (its grants hang on it); the console makes a new one instead. The grants and budgets move under Assistants as a second part, Granted. -
D9 Regions are CoMaps' polygons, named and fetched at a pinned commit: the register's image carries the list of the 1,159 names (some 60 KB) of one CoMaps commit (
55846720, 8 Nov 2026; moved by hand). The console shows them as a tree to search (Germany › Free State of Bavaria › Upper Bavaria › South), from the names' parts. An added region's polygon is downloaded from that commit on Codeberg once, converted to GeoJSON and kept besidesettings.json. Nothing of CoMaps' is in the repository but the list. -
D10 One list of regions for all three: a region added is covered by the tiles, the routes and the search together, as a family needs each where it goes; the console shows each service's state per region (confirmed by D17).
-
D11 The sidecars bring themselves to what the register wants: no server ever gets Docker's socket. Each sidecar's refresher (
map-tiles-refresh,brouter-segments, and Photon's builder, D12) asks the register every 10 minutes for the wanted regions, their union as one GeoJSON MultiPolygon and its hash, withINSTANCE_SECRET, over a network it shares with the register's server alone. When the hash differs from what it last built, or a month has passed, it builds anew beside what it serves and moves it over, as today. It reports its state back: building (with what), ready (with size and date), failed (with the reason in a line), and the free space of its volume. The.envvariables stay for an instance without the console's regions: with none set there, nothing is built.- tiles:
pmtiles extract --region=<the union>; - routes: every segment whose 5° square the union's polygons touch (
E10_N45.rd5for Upper Bavaria); - search: D12.
- tiles:
-
D12 Photon's builder: one container,
photon-build, replacesphoton-data. It carries osmium, a PostgreSQL with Nominatim (mediagis/nominatim, pinned by digest) and Photon's jar. When the regions change or a month has passed:- it picks the smallest of Geofabrik's extracts that covers each polygon, from
Geofabrik's index (
index-v1.json, each extract's own polygon); - it downloads them and cuts them to the union;
- it imports the cut into its Nominatim, then that into a new Photon database;
- it stops its PostgreSQL, deletes Nominatim's database and marks the new Photon
database ready for
serve.sh, asdata.shdoes now.
It is held to 3 GB of memory. While nothing is to be built, it holds nothing but its script. Photon's server then needs about 1 GB of heap for a few regions (
PHOTON_HEAP), rather than 6. - it picks the smallest of Geofabrik's extracts that covers each polygon, from
Geofabrik's index (
-
D13 Builds wait for the day: refreshers and the builder are among the sidecars ADR 0039, D6, stops during the CI window from 02:00 to 05:00. A build already running is interrupted there and starts again afterwards.
-
D14 Room before a region: the console shows each volume's free space as reported. It warns, without refusing, when it is less than twice what the services take now, since a new cut is built beside the old.
-
D15 State: State shows what the instance can know without Docker:
- each sidecar's last report (D11) and whether it is within the nightly window;
- the last backup, from a line
backup.shwrites (state/backup.jsonunderdeploy/, mounted read-only into the register's server: when, how large, whether it succeeded); - the version each app's server runs.
A sidecar that hasn't reported for two of its rounds shows as Silent.
-
D16 Access: every new endpoint of the console is the operator's alone, with tests first asserting that a person who isn't an operator, someone signed out and a member service are refused. The sidecars' two endpoints (the wanted regions, the reports) take
INSTANCE_SECRETalone, with tests asserting that a person's session, operator or not, and a wrong or missing secret are refused. Reports are kept in memory only (the register's server forgets them when it restarts, and the next round reports again), so nothing of them reaches the database.
Order of building
- Regions (D9 to D14), which halkyon.li waits for.
- Sign-up (D5 to D7), with the settings file and the members asking for it.
- Assistants (D8).
- State (D15).
- The row in Settings and the route (D1), from step 1 on: Accounts is today's operator's screen.
Decided after the proposal (8 Nov 2026)
- D17 One list of regions (the developer's choice), as D10 has it, over a choice per service, which would have let a region have routes without its search.
- D18 A region is built whenever it is added or removed (the developer's choice), at any hour outside the CI window (D13), with no hours of its own.
Consequences
- The settings' file joins
.envas where an instance's configuration lives; after a restore the operator sets again what was set in the console. Approvals, operators, grants and budgets come back with the database. - A region costs disk and memory on its sidecars, which the console shows but does not refuse.
- The register's server gains an instance network shared with the refreshers, which have
a way out to the internet (
downloads). Only its two secret-bound endpoints are of use to them, as with the members today. - Photon's builder brings Nominatim in, as an import step that leaves nothing running.
sidecars/README.mdand.env.exampledescribe the console as the way to set regions, sign-up and assistants,.envas their defaults.