Web app walkthrough
The web client (apps/web, served at /app/) is the whole product without a
terminal: identity, messages, contacts, files, sharing and inbox policy. This page
follows a new user through it and says which document or endpoint each screen writes,
so a behaviour you see here can be traced to the spec that defines it.
It is a React + Tailwind app built with Vite. The protocol lives in
@poweur/client, bundled into that build; the app owns
key custody (passkey PRF, or the mobile shell's native keystore) and the screens.
Five destinations
| Destination | What it is |
|---|---|
| Messages | conversations, plus Requests and — while the inbox policy accepts them — Anonymous as separate trays |
| Contacts | your contacts.json, with states, petnames and search |
| Files | your tree, sharing, and other people's shares |
| Apps | the launcher (forward-looking) |
| Settings | This identity (profile, keys, inbox policy — unlock required) and This device (add/switch identity, relay, lookup, About) |
Everything is laid out for a 375px screen first, because the Capacitor shell wraps this same UI.
First run
Creating an identity (Apps → Add new ID, or the welcome screen) registers it, wraps the keys under a passkey's PRF secret, and enrolls this browser in the relay keystore so clearing site data is not fatal — see key management. A browser or authenticator without PRF is refused (use Apple or Google passkeys, or the mobile app). Starting from nothing on a hosted relay, claiming an ID is the flow that gets you here.
Then a three-step setup runs. Every step is skippable, and skipping writes nothing:
- Who can message you — the inbox policy. It opens on
open("Anyone"), so a first message from a friend, or from thehello.poweur.netdemo, arrives straight away; the alternative offered iscontacts_and_requestsonce strangers start to bother you. The strictcontacts_onlymode is not offered to a new ID (it is in Settings, with a warning). The step can also turn on anonymous messages. See contacts and trust. - How people see you — display name, bio, avatar, one link →
.poweur/public/profile.json. - You're set.
The policy step exists because its default (open, anyone may message you) is the one
setting nobody knowingly chooses and nobody goes looking for until the first unwanted
message arrives.
Contacts and requests
- Add takes a Poweur ID, resolves it before anything is sent — a typo fails at the
input, not silently later — and sends
sys.contact.request. The key that resolves at that moment is pinned. - Messages → Requests is the handshake tray: the relay parks
sys.contact.requesthere in every inbox mode exceptcontacts_only. Accept pins their key and sendssys.contact.accept; Block writesblockedand sends nothing. A request still shows if you already list them as a contact — that is how a one-sided handshake (they asked; you already added them) gets answered. - When someone accepts a request you sent, reading your requests queue promotes them
to
acceptedon your side too — without that, your own policy would keep refusing their first message. - A message from someone you hold no entry for carries a one-tap Add.
Every write goes to .poweur/relay/contacts.json through the owner system-file API, so contacts sync to your
other devices and to the CLI.
When a key changes
Sending to a pinned contact whose key no longer matches raises a blocking dialog with
both keys and refuses to send until you press Trust new key — the web twin of the
CLI's --accept-new-key. A change covered by a signed rotation statement re-pins
silently and says so. See key pinning.
Inbox policy, anonymous and proof-of-work
Settings → Who can message you edits the mode and the anonymous block as one
document (.poweur/relay/inbox-policy.json). Anonymous messages are off until you
turn them on; the difficulty slider is labelled with what the challenge costs the
sender's browser, and warns past 20 bits where a phone stops feeling like it is
working. verified and payment appear disabled: designed policy slots relays answer
but do not enforce.
Accepted anonymous messages land in Messages → Anonymous and nowhere else, rendered without a sender and with no reply affordance — there is nobody to reply to. Compose can also send anonymously; the proof-of-work is solved in the page, with progress.
Files and sharing
Your tree, with the layout roots and their audiences (storage model). Uploads switch to the resumable chunked endpoint above 64 MB. The folder in view refreshes itself from the changes feed.
🔗 on a folder or file under /shared or /apps opens the share dialog: audience
(contacts or a typed identity), read or read-write, optional expiry. The grant is signed
in the browser and stored in your own tree; 🔗 at the root lists every grant with a
Revoke button. Sharing also sends the recipient an encrypted share offer, which
appears in their Requests tray; accepting it adds the folder to Files → Shared with
me, where it opens in the owner's tree with only what they granted. Revoking takes effect
at once. See sharing.
Keys, devices and recovery
Settings → Keys & devices lists every device using the identity — web, app and CLI — with its name, client, platform, when it was added and last used, and marks this one. Each row notes whether a passkey backup exists to restore it after a reinstall. You can remove one (ending its sessions). Recovery kit renders 24 words derived from the identity seed and checks them back. A device that holds no key can join an existing identity through the six-digit comparison ceremony — see key management.
What is not here yet
- Editing groups in the web app. The share dialog uses your groups, but you create and
change them with the CLI (
poweur share group set <name> --members=<id,id>) for now. - Rotating a pre-EPIC-011 identity onto a recovery seed, and nominating a recovery master, both owned by EPIC-011.
- Nothing here is blocked by the Host-routed well-known path. A profile card fetches
https://<identity>/.well-known/poweur/profile.json, which is that identity's own hostname, so the browser setsHostfor it and the relay serves the right tree (Access-Control-Allow-Origin: *). Only a local dev relay is different: many identities behind one IP that DNS does not know, so cards there fall back to the identity document.