Observify

User Tests · cinderdoc-monorepo
← all user tests
USER TEST PLAN

cinderdoc-monorepo

Derived from: commit a95853c (dev), 2026-08-07, against the live production deployment at

0/61 checked · 0 pass · 0 fail

Flows

check each step and final check
F1

Cold load and the sign-in gate

Proves the editor loads cold, shows the recipient-bound identity gate, and drafts stay local until a real signed-in Save.

Persona / precondition: any visitor, signed out, fresh private/incognito tab, no cached session, at https://cinderdoc.thewolf.tech.

0/5

Load the site. Expect: page title editor-web — Cinderdoc, a My documents panel, and the
empty-state line No live local documents yet. Create one to set its fuse and save an encrypted
.cdoc.
(verified live, 2026-08-08T01:45Z).

Look at the identity panel. Expect: heading Sign in to open recipient-bound Cinderdocs,
explanation Sign in to prove which verified recipient email is requesting the key., and two
buttons Continue with Google and Continue with GitHub (verified live, 2026-08-08T01:45Z).

Without signing in, click + Create Cinderdoc and type a title and body. Expect: you can
compose a draft locally — nothing is sent yet — but no Save can complete while signed out (to
confirm — POST /api/keyvault/documents requires Bearer + the v2 client header per
[sensitive content omitted]
{"error":"authentication_required"}`, never a silent save).

Reload the page. Expect: still signed out, identity panel still shown — nothing from step 3
persisted a session.

[sensitive content omitted]
sessionStorage before you ever sign in (tokens are documented to live in memory only —
[sensitive content omitted]

F2

API identity and version boundary (read-only spot-check)

Proves the deployed keyvault API itself enforces the auth/version gate, independent of the UI — every check below is an unauthenticated GET and changes nothing.

Persona / precondition: a terminal with curl. No account needed.

0/6

Run curl -s https://cinderdoc.thewolf.tech/api/keyvault/health. Expect:
{"ok":true,"service":"keyvault","apiVersion":"2.0.0","minClientVersion":"2.0.0"} (verified
live, 2026-08-08T01:50Z).

[sensitive content omitted]
Expect: body {"error":"authentication_required"} and status 401 (verified live,
2026-08-08T01:50Z).

Run curl -s -w '\n%{http_code}\n'
https://cinderdoc.thewolf.tech/api/keyvault/documents/00000000-0000-0000-0000-000000000000/status
.
Expect: body {"error":"not_found"} and status 404 (verified live, 2026-08-08T01:50Z).

Run curl -s -o /dev/null -w '%{http_code}\n' http://cinderdoc.thewolf.tech/. Expect: 302
redirecting to the https:// origin — plain HTTP is never served content directly (verified
live, 2026-08-08T01:50Z).

Run curl -s -m 5 http://cinderdoc.thewolf.tech:8787/health and the same against port 5404.
Expect: both hang and time out — keyvault has no public port of its own; it is reachable
only through Caddy's /api/keyvault/* proxy (verified live, 2026-08-08T01:50Z — both timed out
after 5s with no response).

Final check: none of the five calls above needed a credential or changed any resource —
confirming this whole flow is safe to re-run at any time, including in production.

F3

Sign in, create, set the fuse, and save a recipient-bound Cinderdoc

Proves the primary authoring path end to end: identity, the fuse (expiry) control, the honesty copy shown together, and a real key-custody save.

Persona / precondition: signed out, your test Google or GitHub account.

0/7

Click Continue with Google (or Continue with GitHub) and complete the provider's real
consent screen. Expect: you land back on https://cinderdoc.thewolf.tech showing Signed in
as {your email}
and a Sign out control (to confirm — requires a real OAuth round trip;
[sensitive content omitted]
[sensitive content omitted]

Click + Create Cinderdoc, then click into the title and body fields before typing anything.
Expect: placeholder text reads Counter-offer — 123 Elm St (title) and a body placeholder
describing a $1,240,000 purchase offer — both disappear once you type your own text.

Open the fuse control. Expect: heading When does this document burn out?, subtext The
load-bearing control. This is the whole promise.
, and exactly four duration presets: 1 hour,
24 hours, 7 days, 30 days — there is no sub-hour preset in this UI.

Pick 1 hour and confirm the fuse. Expect: both honesty strings render, visibly separate,
never merged into one claim: This document self-destructs. Once it expires — or you destroy it
early — its encryption key is deleted forever and it can never be decrypted again by anyone,
including us.
and Screenshot & recording protection is a deterrent, not a promise. It can't
stop an external camera, a rooted device, hardware capture, or someone retyping what they see.

Click Save. Expect: a real POST /api/keyvault/documents round trip succeeds and the
document appears under All live cinderdocs (to confirm — requires an authenticated write, out
of scope for this pass's read-only checks; units/editor-web/DOGFOOD.md's recorded transcript
of the same saveDocument code path returned {"ok":true,"docId":"...","grantId":"...",
"container":{...}}
with no plaintext anywhere in the container).

Click Export. Expect: a browser download named <docId>.cdoc; opened as JSON it has exactly
the fields v, docId, alg, nonce, ciphertext, policy — no title or body field
anywhere (units/editor-web/README.md's .cdoc container contract).

Final check: search the exported .cdoc file's raw text for your typed title or body.
Expect: no match anywhere in the file — it is ciphertext-only.

F4

Reopen: the author gets no bypass

Proves reopening runs the identical opening sequence for everyone, including the document's own author — there is no owner fast path.

Persona / precondition: F3 completed; the saved document still listed under All live cinderdocs.

0/4

Click the saved document to reopen it. Expect: a fresh grant → key → decrypt round trip runs
(not a cached copy) and your typed content reappears exactly as saved.

Sign out, sign back in with the SAME account, and reopen the same document again. Expect:
identical success — units/editor-web/src/lib/reopen-flow.ts has no owner/author parameter, and
describeReopenDenial is keyed only by error code, never by who is asking.

While signed in, call GET
https://cinderdoc.thewolf.tech/api/keyvault/documents/<your docId>/status
with no
Authorization header (curl or dev tools). Expect: 401
{"error":"authentication_required","accessMode":"recipient-bound"}
— status is not free to
[sensitive content omitted]
GET, safe to run yourself.

Final check: the reopened content is byte-identical to what you typed in F3 — no truncation,
no stray characters from the encrypt/decrypt round trip.

F5

Wrong recipient and signed-out reopen are denied

Proves the recipient list is the real access boundary — holding the exact .cdoc file is never enough on its own.

Persona / precondition: the .cdoc exported in F3; a second Google or GitHub account whose email was never added as a recipient.

0/4

Sign out of the F3 account. Expect: the dashboard returns to Sign in to open
recipient-bound Cinderdocs
, and the F3 document is no longer listed as openable in this browser
profile (the local index is device-local metadata only).

Sign in with the second account and attempt to open the F3 .cdoc. Expect: the denial This
verified email is not on this Cinderdoc's recipient list. No key was fetched.
(the exact
REOPEN_DENIAL_COPY.recipient_not_authorized string in units/editor-web/src/lib/copy.ts) —
never any document content on screen.

Sign out entirely and attempt the same .cdoc again. Expect: the denial Sign in with a
Google or GitHub account whose verified email is on the recipient list. No key was fetched.

(the authentication_required variant of the same copy table) — a different message from step
2 because the failure reason is genuinely different (no session at all, vs. a session with the
wrong email).

Final check: at no point in this flow does any plaintext title or body appear on screen for the
second account or the signed-out state.

F6

Send the Cinderdoc to a recipient

Proves the ciphertext-only email relay: a real recipient gets added to the document and receives a real message, and the server never keeps a copy of the attachment.

Persona / precondition: F3's document still live (unexpired), signed in as its owner, a second real email inbox you can check.

0/5

Open the live document and choose Send Cinderdoc. Expect: heading Send this encrypted
.cdoc
and the disclosure Cinderdoc relays the encrypted .cdoc to your recipients and does not
store the attachment on its server. Resend and recipient mail providers process the email in
transit and may retain it under their own policies.

Type the second email into the recipient field (placeholder reader@example.com,
another@example.com
). Expect: the reminder Recipient binding does not stop an authorized
recipient from sharing what they can access.
is visible before you send.

Click Send. Expect: Sent to 1 recipient. and the second inbox receives a real email from
Cinderdoc <documents@mail.cinderdoc.thewolf.tech>, subject {your email} sent you a Cinderdoc,
with the .cdoc attached and the line Sign in with this verified email: {the recipient's
email}
(to confirm — requires an authenticated multipart POST and a real inbox check;
[sensitive content omitted]

From the recipient's inbox, download the attachment, open a fresh signed-out browser, and sign
in as the recipient. Expect: the document opens for real — the recipient is now genuinely on
the server-side list, not just holding a copy of the file.

Final check: if you can see the Resend dashboard, confirm the message shows one to address
only, no CC/BCC — recipients never see each other's addresses.

F7

Explicit Destruct is irreversible

Proves the manual key-destruction path is immediate, final, and independent of the expiry clock.

Persona / precondition: signed in as the owner of a live document — use a fresh throwaway one, not F3's, since this step cannot be undone.

0/5

Open a live document you own and choose the destruct control. Expect: modal heading
Destruct "{title}"?, eyebrow Burn the fuse now, and the finality copy Destroying this
document deletes its encryption key forever. It can never be decrypted again by anyone,
including us. Every exported copy everywhere becomes permanently unreadable.

Click Keep document (cancel). Expect: nothing happens — the document is still live
afterward.

Reopen the destruct modal and confirm for real. Expect: the dashboard row immediately shows
the document burned out (no Open/Export actions) — matching keyvault's real POST
/documents/:id/destroy
{"destroyed":true} behavior recorded in
units/keyvault/DOGFOOD.md.

Try to reopen the destroyed document anyway. Expect: This document has expired or been
destroyed. Its key is gone; it can never be opened again.
(REOPEN_DENIAL_COPY.denied_expired)
— never a partial success.

Final check: if you have API access, call destroy on the same document a second time.
Expect: still {"destroyed":true} — destroy is documented idempotent
(units/keyvault/README.md).

F8

Expiry burns the document out automatically

Proves the fuse is a real, enforced deadline, not just a countdown UI, even if nobody manually destroys the document.

Persona / precondition: signed in; a throwaway document.

0/4

Create a document, set the fuse to 1 hour — the shortest this UI offers (the fuse slider
bottoms out at 1 hour in units/editor-web/src/lib/fuse-duration.ts's FUSE_SLIDER_HOURS;
there is no sub-hour option in the editor). Save it, then wait out the hour (or come back to
this step later) and check the dashboard. Expect: the document now renders burned out — no
Open/Export actions.

Faster alternative (mirrors this project's own drills): if you have API access, create a
document with expiresAt a few seconds out and request its key once before and once after that
moment passes. Expect: the pre-expiry request succeeds and the post-expiry one returns 410
{"error":"denied_expired"}
— the same behavior recorded in units/keyvault/DOGFOOD.md's
transcript and re-proven one layer up in
units/server-deploy/DEPLOYMENT.md#sd-t007--key-destruction-at-expiry-drill-deployed-instance-level.

Check GET .../documents/<docId>/status for the expired document (with a valid session).
Expect: {"expired":true,...}.

Final check: attempt to reopen the expired document. Expect: the identical denial as F7's
destroyed-document case — expiry and explicit destroy converge on one honest message, This
document has expired or been destroyed. Its key is gone; it can never be opened again.

F9

Legacy-document compatibility (pre-v2 documents)

Proves a document created before the Auth+Send v2 cutover still honestly opens under the old rule until it expires, and is visibly marked as weaker than a recipient-bound one.

Persona / precondition: a .cdoc created before the 2026-07-25 v2 cutover (see F12). None exists in this checkout — run this flow only if such a fixture is still available; otherwise record it as not reachable.

0/4

Open a legacy (pre-v2) .cdoc. Expect: badge Legacy access — not recipient-bound and
warning `This Cinderdoc predates recipient binding. Until it expires, anyone holding the file
[sensitive content omitted]
legacy fixture exists in this checkout to verify against directly).

Try Send Cinderdoc on it. Expect: refused with To send with recipient binding, re-save
this legacy Cinderdoc as a new document.
— HTTP 409 legacy_document_not_sendable at the API
[sensitive content omitted]

Confirm an old client with no X-Cinderdoc-Client header hitting create/destroy/grant/key/send
now gets refused outright. Expect: HTTP 426 with body {"error":"client_upgrade_required",
"message":"This Cinderdoc requires a newer app before it can be opened.",
"minClientVersion":"2.0.0","downloadUrl":"https://releases.cinderdoc.thewolf.tech"}
(to confirm
— this needs a real write against production with a deliberately old header, out of scope for
[sensitive content omitted]

Final check: the legacy document, once past its own expiresAt, denies exactly like every
other expired document — no special-cased legacy leniency after expiry.

F10

Download and verify the desktop tester viewer (macOS)

Proves the unsigned tester distribution channel is honest about its own trust status and lets a tester verify integrity before running anything.

Persona / precondition: a macOS machine, a browser, at https://releases.cinderdoc.thewolf.tech.

0/6

Load the releases page. Expect: title Cinderdoc Viewer — tester release, eyebrow
CINDERDOC VIEWER · TESTER CHANNEL, heading Open documents without leaving expiry behind.
(verified live, 2026-08-08T01:53Z).

Wait for the page's own checksum check to finish. Expect: the status line becomes A tester
build is available. Verify its checksum before installing.
, a Download Cinderdoc Viewer
(.dmg)
link appears, and the shown SHA-256 reads
a6b61cd02aa1d5c1c4f154e1b3b8ed0d5793313bc55174b2bb62cecc1d55322a (verified live,
2026-08-08T01:53Z, by fetching /artifacts/CinderdocViewer.dmg.sha256 directly).

Download the DMG yourself and run shasum -a 256 on it. Expect: your local hash matches the
value shown on the page exactly.

Read the installation note on the page. Expect: it states plainly macOS warns because this
tester build is not yet notarized by Apple. That is useful information about the build's current
distribution status, not a warning to dismiss.
— it never tells you to ignore Gatekeeper
(verified live, 2026-08-08T01:53Z).

Move the app to Applications, try to open it, then follow the page's own steps: System Settings
→ Privacy & Security
Open Anyway for Cinderdoc Viewer, authenticate, then Open.
Expect: the app launches only after that explicit second confirmation.

Final check: run curl -s https://releases.cinderdoc.thewolf.tech/artifacts/latest.json.
Expect: 404 — there is deliberately no auto-updater manifest yet (verified live,
2026-08-08T01:56Z; units/server-deploy/runbooks/RELEASES.md's stated boundary).

F11

Desktop viewer: opening sequence and honest denials

Proves the packaged viewer enforces the same fixed opening sequence and honesty copy as the web spec — capture protection is verified before any key request, and every denial states the true reason.

Persona / precondition: the viewer installed in F10; a .cdoc you can open on this Mac, and one whose policy requires block-supported-capture (which macOS cannot provide — only Windows can).

0/6

Launch Cinderdoc Viewer cold, no document open. Expect: heading Open a protected
document.
, a capability line for this Mac's detected tier, and a button Open .cdoc… — no
leftover watermark or content chrome from any earlier session
(units/protected-viewer-desktop/src/lib/empty-view.ts).

Click Open .cdoc… and pick a document whose policy accepts detect-and-shield (macOS's tier).
Expect: an opening-sequence panel titled Opening {filename} with the note The key is
fetched only after this device passes.
, then the real decrypted content renders with a visible
watermark.

Open a document whose policy requires block-supported-capture. Expect: a denial whose body
ends with The reason is this device's capability, not your access. — never a message implying
you personally lack permission.

With a document open, trigger macOS's screen-capture UI yourself (Cmd+Shift+5). Expect:
Covered — screen capture detected with footer SHIELD ACTIVE · screen capture detected ·
content masked
; content restores only after the capture UI is gone. This is the one step in
this whole plan a human should run for real — the project's own build deliberately never
triggered this real OS UI itself after an earlier attempt seized the operator's live screen
(see the incident note in units/protected-viewer-desktop/DOGFOOD.md), proving the mechanism
with an injected signal instead.

Open an expired or destroyed document (reuse one from F7 or F8). Expect: Burned out. The
key is gone.
framed as This is the guarantee, working / Self-destruct isn't an error state.
The document did exactly what it promised.

Final check: throughout this flow, confirm the honesty boundary is never upgraded to a stronger
claim: Screenshot & recording protection is a deterrent, not a promise. It can't stop an external
camera, a rooted device, hardware capture, or someone retyping what they see.

F12

Operator: confirm what's actually deployed

Proves the live deployment's own version signals are internally consistent, and surfaces a real gap this plan found between this checkout's git history and what's actually running — every step is an unauthenticated GET, safe to re-run anytime.

Persona / precondition: none.

0/5

Run curl -sI https://cinderdoc.thewolf.tech/. Expect: server: Caddy, HTTP/2 200, and a
last-modified header (verified live, 2026-08-08T01:50Z: last-modified: Sat, 25 Jul 2026
02:48:07 GMT
).

Compare that date against this checkout's own history: run git -C
~/Projects/cinderdoc-monorepo log -1 --format='%h %ci'
. Expect (currently fails — this is the
defect, not a testing mistake):
the checkout should be at least as new as what's live. Instead
the checkout's newest commit is a95853c, dated 2026-07-11 — two weeks OLDER than production's
2026-07-25 bundle. This checkout's own _handoff/cinderdoc-mono.md (written 2026-07-19) even
states production was still one epic behind at that time; production has since moved past both
this checkout and that handoff note without either being updated. Treat this checkout as a
historical snapshot, not a live mirror, until _build/sync-monorepo.sh is re-run from the real
workspace.

Run curl -s https://cinderdoc.thewolf.tech/api/keyvault/health. Expect: apiVersion and
minClientVersion both 2.0.0 (verified live, 2026-08-08T01:50Z), confirming the Auth+Send v2
[sensitive content omitted]
nothing in this checkout's git log or units/server-deploy/DEPLOYMENT.md narrates that cutover
happening.

Run curl -s -o /dev/null -w '%{http_code}\n'
https://releases.cinderdoc.thewolf.tech/artifacts/latest.json
. Expect: 404 — no
auto-updater manifest is served, by design (verified live, 2026-08-08T01:56Z).

Final check: none of the four checks above required a credential or changed anything —
confirming this flow is safe to run as a recurring health check.

Not covered and why

  • The GitHub sign-in path, walked separately from Google: structurally symmetric per

[sensitive content omitted]
here — worth a dedicated pass with a GitHub test account.

  • Real Windows on-hardware block-supported-capture validation: the dev host is macOS;

units/protected-viewer-desktop/README.md discloses this is compile/mock-tested only
(cargo check --target x86_64-pc-windows-gnu), never run on real Windows hardware.

  • Apple notarization / a signed updater: the tester DMG is deliberately unsigned and

/latest.json deliberately 404s (F10's final check); there is nothing to test here until that
ships.

  • Multi-recipient stress (the 20-recipient / 10 MiB raw .cdoc caps on Send): not exercised at

the edge in this pass.

  • Authenticated Coolify console operations (redeploy, DNS, env-var rotation, real ACME

certificate issuance): human-attended only, per every unit's own "Honest deferral list".

  • Postgres swap / horizontal scaling: named as future options in units/keyvault/README.md and

units/server-deploy/README.md; v1 is single-container/single-SQLite by design.

  • Backup/restore/rollback drills (units/server-deploy/DEPLOYMENT.md's SD-T005–T018): already

have real, recorded, dated transcripts in that file from this project's own operational history;
re-running them destructively against the live production stack is out of scope for a user-test
pass and was not repeated here.

  • KIT_FINDINGS.md / STATE.md / _pipeline/: intentionally scrubbed from this monorepo mirror by

_build/sync-monorepo.sh (by design, not a defect) — this checkout cannot surface the workspace's
own process-defect ledger; consult /Users/wolf/Projects/cinderdoc directly for that.

Results

FlowVerdictFailures → tickets
F1 sign-in gate
F2 API boundary
F3 create/save
F4 reopen, no bypass
F5 wrong recipient
F6 send
F7 destruct
F8 expiry
F9 legacy
F10 viewer download
F11 viewer opening sequence
F12 deployment consistency
updated just nownext 3m 00s