# S017 — PWA feature parity + per-user data rights (2026-07-31) ## Context The web app could only read/move in one hardcoded space. Meanwhile the CLI had grown: multiple spaces, create space, member management, edit/delete of posts, thread export — plus account export/delete existed only as zds endpoints. The user asked for per-user data rights (own-data export + deletion) **before** demoing to the 同喜 class community; multi-space support is needed anyway, so ship both in one PWA pass. ## What changed **BFF (`app0/src/server.zig`, `core/spaces.zig`, `core/push.zig`)** - `GET /api/spaces` — caller's spaces; space accounts see `owned`; others see the BFF-configured community space if a member. - `POST /api/space` — create space `{name?}` → `{uri}`. - `GET /api/space/members?space=` — owner-gated roster. - `POST /api/space/add-member` / `/api/space/remove-member` — owner-gated. - `GET /api/account/export` — JSON of the caller's own records across every space they belong to (`{did, handle, exportedAt, spaces[]}` with their threads and posts). NOTE: mvp.dj (revised 2026-07-31) now says data export should live in a separate program and "the API deliberately does not own export" — this endpoint satisfies the user's direct request and matches the doc's export *shape* (own records, as JSON), but is interim until the standalone export program exists. - `POST /api/account/delete` — deletes the caller's own posts and threads from every space they belong to (their records only; other members' records — including replies to them — are untouched), prunes their push subscriptions, and ends the session. The account itself is preserved: the user can log straight back in to an empty slate. Returns `{posts, threads}` counts. - `deletePost` now routes through a generic `deleteRecord` (collection parsed from the record URI) — CLI `delete` and `move --delete` work for threads too, not just posts. `collectionFromUri` handles both the canonical and the space-qualified URI forms. **Web (`app0/web/`)** - Sidebar: auto-listed spaces (no paste-URI-to-enter), owner ★, create space (name input), join by URI, per-user account controls (export → `.car` download; delete → irreversible, full-page confirm). - Space page: thread list + thread composer for everyone; owner gets a manage-members panel (roster, add by handle, remove) and a 导出 (markdown thread export) action; posts show author/time; 编辑 / 移动 / 删除 per post (delete own; all actions when authority). - Route `#/s/` now takes any space URI; entering a space persists it to localStorage so it survives relogin. ## Verification - `zig build test`, `zig build scenario-space` (member-only space) — PASS. - `zig build scenario-serve` — extended checkpoints incl. sections 6d (spaces listing + create + members owner-gating), 6e (move via API, thread export), 6g (account-data export JSON; delete cascades: session 401, own records gone from spaces, others' records survive, account still able to log in and post afterwards) — PASS. - `bun run test` — 35 vitest (FakeBff parity for every new endpoint) — PASS. - Real-browser smoke against a live rig: login → sidebar spaces/★/create/ join/account links → thread view with 编辑/移动/删除 → owner members panel with roster/add/remove. ## Not in slice - Account (identity) deletion — only the user's linji records are deleted; the DID/handle and PDS account survive. mvp.dj's "messages, then the account" second step is deliberately not here: zds account deletion is dev-tools-only and DID-censoring; production needs a real flow. - Standalone export program (mvp.dj 2026-07-31) — see note above. - Webauthn/passkey, markdown rendering in posts.