ADR 0022, decided 23 Oct: the hand-off to assistants outside the app and the Claude connector (ADR 0006) go in v0.19.0, app and server together; the field keeps the web search. ADR 0021 takes the developer's D4-D6: the operator's keys for several providers, granted per person; personal keys on the person's devices, only where the operator allows them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
8.2 KiB
ADR 0021: A private instance: approved sign-ups and an assistant the operator grants
- Status: proposed 23 Oct 2026. Decided by the developer on 23 Oct: D1–D6 below; the rest under Proposal is for review before building.
- Builds on: ADR 0016 (the register), ADR 0019 (private studysets), ADR 0020 (SimpleLogin), ADR 0022 (the hand-off and the connector gone).
- Changes: the rule of 10 Oct 2026 that Halkyon calls no language model with an API key, for an instance whose operator opts in.
Context
The developer may stop the public instance and run halkyon.li for themselves and their close family instead. They don't know in advance which addresses their family will sign in with, and with SimpleLogin (ADR 0020) the instance keeps no addresses anyway.
On 10 Oct the developer ruled out API keys for language models: on a public instance anyone could drive up the bill, and each person should decide what a model sees. Assistants came through people's own subscriptions instead: the connector (ADR 0006) and the hand-off to Claude or Lumo, both gone since 23 Oct (ADR 0022). A family instance whose operator approves every account takes away the first reason; the second holds if the app shows what it sends.
People in one family may prefer different providers (the developer: "I might use myself the Claude API while other might want to use the OpenAI API, then one other the Proton Lumo API"). A plan for Claude's or ChatGPT's apps is no API access: the APIs are billed apart, by their providers' consoles.
Two protocols reach nearly every provider: Anthropic's Messages API (Claude), and the OpenAI-compatible chat completions (OpenAI, Mistral, Google Gemini's compatibility endpoint, OpenRouter, local models through Ollama, llama.cpp or vLLM). Proton announced a Lumo API for autumn or winter 2026 without publishing its format; community projects (lumo-tamer, an unofficial tracker) show an OpenAI-compatible interface.
Decisions (23 Oct 2026, the developer's)
- D1 Sign-ups wait for the operator's approval (over invitation links, which were recommended, and a sign-up window): anyone can sign up; nothing works until the operator approves them.
- D2 The assistant runs on the instance's key, granted per person (recommended, over each person's own key and both): the operator pays and switches it on for whom they choose.
- D3 Individual limits and statistics (the developer: "so I can see after the fact who potentially drove up the bill"): each person's use is limited and counted, and the operator sees it per person.
- D4 Several providers, all on the operator's keys (over personal keys alone, and over both, which was recommended): the operator sets up the assistants the instance offers and grants each person one or more.
- D5 Personal keys stay on the person's devices (recommended, over keeping them on the instance): the app calls the provider itself, and the instance sees neither key nor text.
- D6 Personal keys only where the operator allows them (over every instance, which was recommended): an instance setting, off unless switched on.
Proposal
- Who signs up, per instance:
SIGN_UP:open(as today),approval, orclosed. On halkyon.liapproval. - The operator is a role on the register. The first one:
operatorSetupCodein the passwords; the sign-up that gives it becomes the operator, approved at once, and the code counts once. A rule by handle would let a stranger who signs up first take the operator's handle. The operator can name others. - Waiting accounts (D1): after signing up (SimpleLogin, handle, keys: ADR 0020), an account waits. Its sessions can only see that they wait, or delete the account; nothing syncs and nothing uploads. The operator sees who waits in the app (the handle and when they signed up, which is all the instance knows) and approves or refuses; a refusal deletes the account. Unapproved after 14 days, it is deleted. At most 10 waiting at once, and sign-ups per network limited as today's, so strangers can't flood the list. The operator's app shows a badge; there is no mail, as the instance keeps no addresses.
- The assistants, set by the operator (D4): one or more, each with an ID, the name
people see ("Claude Sonnet"), its protocol (
anthropic, oropenaifor every OpenAI-compatible service), its base URL (the provider's own unless set) and model indeploy/.env(ASSISTANTS=claude,gpt, thenASSISTANT_CLAUDE_NAME,_PROTOCOL,_BASE_URL,_MODEL), and its key in the passwords (ASSISTANT_CLAUDE_KEY). There rather than in the app: the keys stay out of the database and its hourly backups, where the operator keeps the instance's other secrets. None set: no assistant on the instance, as today. Adding Lumo once its API is out, or switching a model, is a change of these and a restart; no app changes. - Two adapters of our own behind one interface, over plain HTTP, no SDKs, in a pure Dart package of their own, which the server uses for the operator's assistants and the app for personal keys (10). Features use only what both protocols share: a system prompt and messages, streamed text, and tool calls where a feature needs them. Tested against recorded replies of each. Not chosen: LiteLLM as a sidecar (a Python proxy for about 100 providers: one more container to run and update, for providers never used) and langchain_dart (a large framework for a small job).
- Grants (D2, D4): nobody has an assistant until the operator grants it to them, one assistant at a time, each grant with its limits. The person agrees once to what goes where: the provider's name, that the instance sees the text on its way, and that the operator sees how much they used, never what.
- Asking: the field offers the assistants granted beside the web search (the one last
used first), the app shows what it will send, the person sends it to the instance
(
assistant.ask, the answer streamed), the instance checks the grant and what is left of the limit, forwards the request, and counts what the provider's reply reports (input and output tokens). Past the limit it refuses (limitReached), saying when it resets. A private studyset (ADR 0019) goes only as far as the person sends it, decrypted on the device for that request. - Limits and statistics (D3): per grant, per day and per month (a day's limit stops a runaway loop early). Counted per person, day and assistant: requests, input and output tokens, and the cost at the prices the operator enters per model (per million tokens; without prices, tokens only, and limits in tokens). Neither prompts nor answers are kept. The operator sees everyone's figures in the app, per person and month; each person sees their own.
- The privacy page names the provider, says what is sent and that the instance sees it in passing, that the operator sees each person's figures, and that no content is kept.
- Personal keys (D5, D6), where the operator switches them on (
ASSISTANT_OWN_KEYS, off unlesson;instance.info()says so, and the app offers them only then): a person adds a provider in Settings (protocol, base URL, model, their key), and may add several. The key goes into their settings, which are sealed with their personal key (ADR 0019), so it reaches their other devices and the instance can't read it. The app calls the provider directly with the same adapters (5); the instance sees neither key nor text and counts nothing, and the person's limits are their provider's. The instance can't stop a modified app from doing so, and needn't: nothing passes through it. Personal assistants are offered in the field beside those granted.
Consequences
- The operator pays the provider, and sees who used how much, never what for.
- The instance sees what is asked on its way to the provider; the provider's own terms then apply to it.
- A provider that is down stops the assistants on it; the others granted, personal keys and the web search go on.
- Strangers can sign up and wait, but can't use anything, and their accounts lapse.
- Public instances keep the rule of 10 Oct: their operators leave the assistants unset, and may allow personal keys.