- TypeScript 49.4%
- Rust 49.2%
- Dockerfile 0.5%
- SCSS 0.3%
- JavaScript 0.2%
- Other 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| .github/workflows | ||
| assets | ||
| backend | ||
| scripts | ||
| startos | ||
| web | ||
| .dockerignore | ||
| .gitignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| icon.svg | ||
| instructions.md | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| UPDATING.md | ||
Wish List on StartOS
Wish List has no upstream project. The application is developed in this repository and built for StartOS, so this document describes the whole service rather than differences from an upstream release.
Wish List lets someone make gift wish lists, share each one by link, and lets the people buying for them claim gifts, split a gift's cost, add gifts that aren't on the list, and discuss every item, while the list's owners never see any of it. A list can have several owners. Source: https://forge.thebarsonists.run/helix-test/wish-list-startos.
Table of Contents
- Image and Container Runtime
- Volume and Data Layout
- File Models
- Dependencies
- Network Access and Interfaces
- Installation and First-Run Flow
- Actions
- Tasks
- Health Checks
- Backups and Restore
- Limitations and Differences
- Quick Reference for AI Consumers
Image and Container Runtime
The image is built from the Dockerfile in this repository, for x86_64 and aarch64. It holds one
binary, wish-list, and the compiled web app in /app/web.
| Subcontainer | Runs | Purpose |
|---|---|---|
wish-list |
wish-list serve |
The daemon: web app and JSON-RPC API on port 80 |
wish-list-admin |
wish-list <command> |
Temporary, per action: writes links, reads and deletes users in the database |
wish-list serve takes WISH_LIST_PUBLIC_URL from the package on every start. Run without
serve, the same binary is the admin CLI (signup-link, reset-link --user-id <id>,
delete-user --user-id <id>, users),
which works on the database directly whether or not the service is running.
Volume and Data Layout
Two volumes. Everything users create lives in one SQLite database.
| Volume | Mount point | Contents |
|---|---|---|
main |
/data |
wish-list.db (SQLite, WAL mode): accounts, passkeys, sessions, lists, items, claims, comments, avatars, push subscriptions, the VAPID key |
startos |
not mounted | store.json |
The server applies database migrations itself at start. Each user's view of the data is derived from the database and held only in memory, rebuilt at every start.
File Models
One file model, and no configuration file inside the container.
store.jsonon thestartosvolume holdsprimaryUrl, the address chosen with Set Primary URL. Only that action writes it. Hand edits survive, but an address that is not one of the interface's addresses is ignored in favour of the preferred one.
The primary URL reaches the app as the WISH_LIST_PUBLIC_URL environment variable, re-read on
every start; changing it restarts the service.
Dependencies
None.
Network Access and Interfaces
One HTTP interface serves both the web app and the API the web app calls.
| Interface | Type | Port | Serves |
|---|---|---|---|
ui |
ui |
80 | The web app, POST /rpc/v1 (JSON-RPC), GET /avatars/<id> (signed-in users only), GET /ws/db/<guid> (WebSocket of live updates) |
Share, sign-up and reset links are built on the primary URL, so the people a list is shared with
need to reach that address. Sessions are an HttpOnly, SameSite=Strict cookie, Secure on
HTTPS origins, valid for a year.
A passkey belongs to the host name it was created on. It signs in on any port of that host name, never on another address. Passkeys need a domain name, so they are unavailable on an IP address.
Pages update live. A signed-in browser calls db.subscribe for a snapshot of its user's view and
a single-use WebSocket that streams patches to it. Views are computed on the server from what each
user may see, so nothing hidden from the owner ever reaches the owner's browser. A browser that
loses the WebSocket reconnects with a fresh snapshot.
Push notifications, sent to every owner of a list when someone asks a question and to the asker when
an owner answers, are the service's only outbound traffic. They are standard Web Push (RFC 8030), encrypted (RFC 8291, aes128gcm) and
signed with a VAPID key (RFC 8292) the server generates on first start and keeps in the database.
They go to whatever push service the subscribing browser names: Google's for Chrome, Mozilla's for
Firefox, Apple's for Safari, and the user's UnifiedPush distributor for browsers that use one
(Fennec, IronFox). A subscription belongs to one device's session and ends when that session
does; an endpoint the push service reports gone (404 or 410) is deleted. Delivery failures are
logged and never retried.
Push endpoints must be https and on the public internet. An endpoint whose host is, or resolves
to, a loopback, private (LAN), link-local, CGNAT or other non-public address is refused when the
browser subscribes and skipped at delivery. Connections go only to public addresses, without
proxies or redirects. A UnifiedPush distributor on the LAN therefore cannot receive notifications.
Installation and First-Run Flow
The app has no open registration and no setup wizard: an account can only be created through a link the package mints, so whoever reaches the address first cannot claim the instance.
- Install raises two tasks, one for Set Primary URL and one for Create Sign-up Link.
- Create Sign-up Link returns a single-use link; opening it creates the first account.
- From then on, people join by the share links that list owners send, or by further sign-up links.
The service runs before either task is done. Until a primary URL is chosen, links use the
preferred address (a public domain, else the .local address).
Actions
Four actions. All run whether the service is running or stopped.
- Create Sign-up Link: run whenever someone needs an account but has no share link. It adds a single-use invitation to the database and returns the link. The link expires after 7 days. Each run makes a new, independent link; earlier ones stay valid until used or expired. Takes a few seconds and does not interrupt the service.
- Create Password Reset Link: run when a user is locked out. Pick the user; it returns a single-use link, valid 7 days, that sets a new password. Using the link signs that user out of every other session. Their passkeys are kept. Repeatable; each run makes a new link.
- Delete User: pick the user. Deletes their account, sessions, passkeys, avatar, claims, comments and questions, the gifts they added themselves, and every list they are the only owner of. Lists they co-own stay with the other owners, and their answers there stay, credited to a former owner. Restarts a running service, whose live views are in memory and would miss the change.
- Set Primary URL: run to choose the address links point at, typically a public domain so
that people outside the network can open share links. Writes
store.jsonand restarts the service. Changing it does not invalidate existing share links, which keep their token on the new address, but links already sent still name the old address.
Tasks
Two tasks, both important; neither blocks the service.
- Set Primary URL is raised while no primary URL is stored, or while the stored URL's host name is not one of the interface's addresses. Choosing an address clears it. It comes back if that address later disappears (a removed domain, or an IP address whose network link is down).
- Create Sign-up Link is raised once, at install. Running the action clears it, and it does not return.
Health Checks
One check, Web Interface, passes once the server is listening on port 80. It goes green within a few seconds of start. A failure that persists means the server exited: read the logs. The usual cause is a database the server cannot open or migrate.
Backups and Restore
Both volumes are copied wholesale (ofVolumes('main', 'startos')). The SQLite database is
copied as files, including its write-ahead log.
A restored instance is complete: accounts, passkeys, sessions, lists, claims and comments, push subscriptions, and the chosen primary URL all come back. If the interface's port changed, links follow the new port automatically. Passkeys keep working because they are bound to the host name, not the port.
Limitations and Differences
- No email: sign-up, reset and share links are copied and sent by hand, and nobody is notified of claims or comments.
- Owners see questions asked about their items, and who asked unless they asked anonymously (the default), and no other comments. They never see gifts participants added themselves. A participant made an owner loses their claims on that list, and the gifts they added go with them.
- Names are unique per server, compared case-insensitively, and are what people sign in with.
- A passkey works only on the host name it was made on; a user who switches to a different address signs in with their password or adds a passkey there.
- Removing someone from a list, as a participant or an owner, does not invalidate the share link they used. The owner who removes them is prompted to revoke it, which issues a new link to everyone the owner sends it to afterwards.
- The web app is in English only.
- Push notifications need a browser with the Push API. On iPhone and iPad that means Wish List added to the Home Screen; on Android without Google services, a UnifiedPush-capable browser. Private and incognito windows cannot subscribe, and push services on a private network are refused.
Quick Reference for AI Consumers
package_id: wish-list
image: built from Dockerfile
architectures: [x86_64, aarch64]
subcontainers: [wish-list, wish-list-admin]
volumes:
main: /data
startos: unmounted
file_models:
- store.json
startos_managed_env_vars:
- WISH_LIST_PUBLIC_URL
dependencies: none
interfaces:
ui: { type: ui, port: 80 }
actions:
- create-signup-link
- reset-password-link
- delete-user
- set-primary-url
tasks:
- { action: set-primary-url, severity: important }
- { action: create-signup-link, severity: important }
health_checks:
- primary