Files
verdande/docs/editions.md
kristianwindandClaude Opus 5 ff988d26bb feat(urd): mærket bliver ð, og frontend-bygget bliver deterministisk
## Mærket

Urds oplagte mærke var ᚢ uruz — bogstavet navnet begynder med i den ældre
futhark, præcis som Verdandes er ᚹ wunjo. Det blev tegnet sådan først.

Men uruz er allerede et af husets egne produkter, og dets egen
`scripts/gen-icons.ts` siger i sin kommentar at den "draws the Uruz rune (ᚢ)
in rune-gold" med "a tall left vertical, a shoulder across the top, and a
shorter" — altså samme glyf, samme konstruktion, kun en anden farve. To af
samme persons apps side om side på en hjemmeskærm, hvilket er nøjagtig det
sted mærket skal kunne skelnes.

ᚦ thurisaz blev forkastet et niveau nede af samme grund: en stav med en
trekant til højre, og det er ᚹ også. Ved 16px er de én form.

Så bogstavet frem for runen: ð, ethet i Urðr. Centreret ved at tælle malede
pixels frem for ud fra path-tallene, fordi en centreret kasse ikke er en
centreret glyf:

  verdande ᚹ   blæk x  53–108  midte 80.5  ·  y  18–131  midte 74.5
  urd      ð   blæk x  39–110  midte 74.5  ·  y  15–134  midte 74.5

Verdandes +5.5 i x er dens egen bevidste forskydning — en tung stav ved
siden af et let flag skal sidde til højre for midten for at se centreret ud.
ð er symmetrisk nok til at undvære den, og de to er enige i y.

Læsbarhed, målt: ð læses fra 32px og op. Ved 16px er det en klat, og det er
ᚹ også — en favicon er ikke det, der skiller to installerede apps i en dock.

`productRune` hedder nu `productMark`, fordi ð ikke er en rune.

## Determinisme

SvelteKits `kit.version.name` er som standard `Date.now()`. Millisekundet
landede i version.json, i index.html og i service-workeren, ændrede
indholdshashen på hver chunk der refererer dem, og dermed deres filnavne. To
bygninger af samme commit havde forskellige filer i sig.

Dockerfilen har i et år sagt at `-trimpath` og en tom buildid "keep the
output reproducible". Flagene gjorde deres arbejde; frontenden i binæren bar
et tidsstempel. Sætningen var falsk, og ingen læste den.

Fastgjort til VERSION, som udgivelsen allerede sender ind, og `dev` når
intet sætter den. Målt ved at bygge to gange og diffe træet, før og efter.
Prisen er at et lokalt `npm run build` ikke længere ligner en ny version for
service-workeren — hvilket kun betyder noget på en udrullet instans, hvor
udgivelsen sender det rigtige mærkat.

Det gør også CI's nye påstand mulig: de to images deler alle lag, så urd
bærer samme binær som verdande frem for en genbygning der tilfældigvis
matcher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 10:10:30 +02:00

7.3 KiB

Two editions: verdande and urd

One program, built once, with two faces.

verdande urd
What it is Tasks, projects and notes Notes
VERDANDE_EDITION full (the default) notes
Routes served 200 137
Mark ᚹ ᚢ

Set VERDANDE_EDITION=notes and the instance is urd: the task routes are not mounted, the sidebar has no way to them, search answers with notes only, and the program calls itself urd in its tab, its wordmark and its web manifest.

Running the notes edition

There is one image. The edition is an environment variable on the container:

docker run -e VERDANDE_EDITION=notes -v urd-data:/data -p 8080:8080 \
    ghcr.io/kristianwind/verdande:latest

No second image and no rune of its own, deliberately.

The edition is for an operator who wants an instance that genuinely has no task routes — the routes are absent from the router, not hidden in the interface. It is not how urd is normally reached. That is being built as a second face of one instance, with its own entry point and its own web manifest, so that both can be installed as separate apps while sharing one database, one login and one set of links. Until that lands, this page describes the edition only.

Two details that surprise people who look inside:

  • The binary is called /verdande and the database file is verdande.db, in both editions. Not an oversight: it is the same binary, and renaming the file per edition would be a migration hazard in exchange for nothing a user sees.
  • The icons for both editions ship in the one image. The server decides which to serve at /icon.svg, because Safari looks for apple-touch-icon.png by name and never reads the manifest, so renaming paths would have left iOS with the wrong mark.

Moving between the two, measured

A /data directory moves between the editions in both directions, losslessly. Same binary, same schema, same migrations — so this is not a feature that was built, it is a property that was checked:

step result
verdande creates a notebook, a note in it, and a task 1 note, 1 task
the same directory, VERDANDE_EDITION=notes needs_setup: false — same account. The note keeps its title, its body and its project_id. /api/v1/tasks → 404
the same directory, back to full the task is still there; so is the note

The ownership stayed at the panel's service account through all three.

So the way to move notes is to change the variable. GET /export/notes.zip is for getting notes into other programs — it writes one .md per note and carries the body and nothing else: no notebook, no sharing, no attachments, no pinned flag. That is the right division of labour, because the database already holds those.

!!! note "The task rows stay" An instance run as notes still has its tasks in the database; they are simply not served. Harmless on a fresh instance, dead weight on a converted one — and worth knowing before handing a converted instance to somebody else.

Why one codebase and not a fork

The two share everything that is hard: accounts, sessions, passkeys, sharing, invitations, the realtime socket, search, the backup format, the migrations. Notes are about a twentieth of the Go in this repository and reference no task type; tasks reference no note type. A fork would have duplicated the other nineteen twentieths in order to separate the one — and then every security fix would have needed applying twice, by hand, forever.

So the split is a configuration value read in one place, and the thing that keeps it honest is a test rather than a convention: TestNotesEditionServesOnlyItsOwnRoutes walks both routers, compares them against a written-down list of the 63 withheld routes, and fails in both directions — a route that appears in the notes edition and a route that stops being withheld are both a failure.

An unknown value refuses to start

VERDANDE_EDITION=note, Notes and none are all rejected at startup, by name, rather than being treated as "not notes".

The silent direction is the dangerous one here. An operator who sets up an instance for notes and mistypes the value would otherwise get every task route served on it, with no error anywhere and no way to tell from the outside.

What urd does not have

The 63 withheld routes:

what
29 tasks, sub-tasks, labels, saved filters, reminders, the upcoming and delegated views
8 the CalDAV server — every route in it reads or writes a VTODO
1 /.well-known/caldav, which is a claim that a CalDAV server is here
1 the ICS calendar feed, plus 2 for the token it is reached with
4 the task exports: tasks.ics, projects.zip, and a project's .csv and .ics
2 the Todoist and CSV imports, which create tasks
4 project templates — a template is a project plus sections plus tasks
5 sections, which group tasks: section_id is a column on tasks and not on notes
6 the AI task features: split, tidy the inbox, and the day's plan
1 the inbox hook, which pushes a line of text in as a task

Plus five of the nine MCP tools — search_tasks, create_task, update_task, complete_task and add_comment. The endpoint is still there; the tool list is shorter. A tool a model can call is a surface, and one that answers "there are no tasks" is worse than one that does not exist: the model tries it, gets an empty answer, and tells the person there is nothing to do.

Everything else is the same program:

  • accounts, invitations, passkeys and two-factor
  • projects — which in urd are what a note belongs to
  • per-note sharing with other people, independent of projects
  • search, over notes
  • export, of notes
  • the mail, calendar and MCP surfaces, minus their task routes and task tools
  • AI settings, ask, and the note actions

!!! warning "Still being decided" Four connectors are reachable in urd today and have not been settled either way, because each has a plausible notes reading and withholding them would be deciding what this product is rather than closing a hole: the mail-in address, IMAP mailboxes, Gmail, and Google Calendar. They currently file what they receive as tasks, so in urd they connect and then have nowhere to put anything. The same applies to one handler rather than a route: POST /api/v1/ai/notes/{noteID}/actions/apply creates tasks from a note.

The name

Verðandi and Urðr are two of the three Norns in Völuspá, and the poem has them cutting marks into wood — the act of writing something down. Verdande is what is becoming; Urd is what has become and was kept.

An instance started without the task routes is a different product to the person using it, so it says a different name. That name is read from the configuration in one place (Config.ProductName), which is why there is no build-time flag and no second frontend: the frontend is byte-identical in both editions, and the server renames the two files a browser reads before any JavaScript runs — the HTML shell's title and the web manifest.

!!! note "The beacon counts them separately" Both editions report the same two values once a day — instance id and version — and nothing else; see The install count. The edition is not in the payload, so the count does not distinguish them.