Privatium: Choosing How to Build
Privatium is a personal-app framework. One binary provides append-only storage, multi-device sync with no server, discovery, pairing, encryption, snapshots, and plain-text backup. What you build on top is your choice.
Pick a tier
| If the app is… | Tier | Load next |
|---|---|---|
| Records, lists, forms, reports, trackers | 1 — Lua | privatium-tier1-lua |
| A game, canvas, drawing, animation, or has its own interaction design | 2 — Web | privatium-tier2-web, privatium-games |
| Hardware, scheduled jobs, or a non-HTTP protocol | 3 — Rust | privatium-tier3-rust |
Default to Tier 1. No build step, hot reload, no JavaScript required. Move to Tier 2 only when the interface genuinely cannot be server-rendered HTML.
Tiers mix freely on one node. A Tier 2 game can sit beside a Tier 1 tracker.
Pick a mode
| Mode | Meaning |
|---|---|
| host (default) | Many apps at /a/<slug>/, with a launcher |
| solo | One app at /. Indistinguishable from a purpose-built application. |
| embedded | Your own Rust binary; the core is a library |
Use url('/path') (Lua) or pv.url('/path') (JS) for every internal link. Never
hardcode /a/<slug>/ — it breaks in solo mode, and the linter flags it.
The bar every app shares
Every app wears the same chrome: a top bar with the Privatium mark on the left linking
to the launcher, the app’s title in the centre linking to its first page, and on the
right an Apps link and one Menu; a footer with a status line the framework writes when
the connection changes. A Tier 1 view renders inside the page frame that carries it. A
Tier 2 document, and a Tier 1 view that owns its document with layout(), receive the
bar and footer inserted at three anchors — </head>, the opening <body>, </body> —
unless app.toml says [ui] chrome = "none", which an app with its own full-window
interface (a canvas, a game) does. An app adds its own items to the menu through [ui]
in app.toml and the menu() template helper, writes its own task status through
pv.status(), and never draws a second way back or a second menu (PV408). A Tier 1 app
can also set [ui] navigation = "swap", so its pages change inside one document under a
bar that never moves; the swap never crosses the app’s mount. The details
are in privatium-tier1-lua, privatium-tier2-web, spec/lua-api.md §4.1 and
spec/app-contract.md §5.
Invariants — true in every tier
- JSONL is the only truth. The SQLite cache, snapshots, and CSV are caches; deleting all of them
must lose zero data. Never write an
UPDATE— the answer is always an append. - One writer per log file. A device appends only to its own log.
- Append-only. Corrections are new events. Deletions are tombstones.
DECIMALandBIGINTare strings in JSON. JSON numbers are doubles. Converting money to a float is a bug every time, in every language.- The client never stamps
seq,lam,ts,dev, orapp. The framework does. - No secret enters a log file. Not keys, not codes, not tokens.
- XDG paths only. Never write beside the binary.
- IDs are ULIDs. No sequences, no auto-increment. They are also what makes an offline
outbox idempotent — never add a dedupe table or transaction IDs; whether a queued write
landed is read from the log, never remembered. The one exception is a deliberate
singleton keyed by a constant, the way
apps/animalskeys itscursorrow'cursor'; anything arriving over the HTTP data API must still be a ULID. - No node is primary. Every node is a peer. An always-on node on a VPS is a peer that happens to be reachable, not a server.
- Devices pin the cluster key, not a node key. Pair a phone once and it trusts every node in the cluster. The cluster private key never leaves a node.
- Discovery runs concurrently, never chained — mDNS, UDP broadcast, pkarr on the mainline DHT, and DNS all at once. They fail in different environments.
- A relay holds nothing; a node holds everything. Never suggest putting a full node on rented hardware when a relay would do.
Client capability
Not every client can do everything, and it is a property of the runtime, not a setting:
| Node | Native desktop | Native mobile | Browser / PWA | |
|---|---|---|---|---|
| Full replica | ✔ | ✔ | optional | ✘ |
| mDNS discovery | ✔ | ✔ | ✔ | ✘ |
| Multi-endpoint failover | ✔ | ✔ | ✔ | ✘ — single origin |
A browser cannot find a node — the user supplies the address. Never write code that has a browser client try a LAN address and then a remote one; mixed content forbids it.
Both tiers run on mobile and both update dynamically — Tier 1 sends HTML, Tier 2 sends web assets. Neither needs a per-user build. The one difference is offline: a Tier 1 app can show cached pages and queue writes but cannot render a view it has not visited, because it renders on the node. If full offline on mobile is a requirement, choose Tier 2.
Never propose shipping a native Lua interpreter that downloads and executes an app’s source.
Verify
privatium new <slug> [--tier lua|web|rust] [--from hello] [--scaffold <table>]
privatium new --examples # hello, animals, sketch and pantry, written to <data-dir>/apps/
privatium dev --app <slug>
privatium lint apps/<slug>
privatium lint apps/<slug> --format json
privatium lint apps/<slug> --fix # the two mechanical corrections of spec/cli.md §5.3, nothing else
privatium skill export # these skills, matching the running version (spec/cli.md §6)
Generate, lint, fix, repeat. Do not present an app as finished until the linter is clean.
Every rule, with its severity and the section to read, is reference/lint-rules.md.
Also load
privatium-accessibility and privatium-security apply to every tier. Load them alongside,
not instead of.
Detail
spec/app-contract.md, spec/lua-api.md, spec/data-api.md, docs/frameworks.md,
docs/connectivity.md (what works over which network, per client kind)