Observify

User Tests · cinderdoc
← all user tests
USER TEST PLAN

cinderdoc

Derived from: commit a9ee2e8f (dev and prod, both confirmed at this same commit via

0/61 checked · 0 pass · 0 fail

Flows

check each step and final check
F1

Cold load: the local documents dashboard

Proves the unauthenticated entry point: a stranger's first look, before any sign-in.

Persona / precondition: a browser with no active Cinderdoc session, first visit to https://cinderdoc.thewolf.tech.

0/6

Load https://cinderdoc.thewolf.tech/. Expect: heading block MY DOCUMENTS / My documents
/ Encrypted documents authored on this device; a window-chrome strip reading
app.cinderdoc.com/documents and THIS DEVICE; a toolbar reading Encrypted local artifacts
and a keyvault-backed chip.

Check the section nav on the left. Expect: three buttons — All live cinderdocs (selected
by default), Destructed cinderdocs, How to use.

Check the sign-in card. 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 / Continue with GitHub.

Check the action row and the empty list. Expect: + Create Cinderdoc enabled;
Export Cinderdoc disabled with the note Select an active document to export its encrypted
.cdoc. Exporting is still a deterrent, not a technical guarantee against photography or
transcription.
; the list area reads No live local documents yet. Create one to set its fuse
and save an encrypted .cdoc.
; footer reads Ready to create or open a document with a
Privacy link and local author.

Check the browser tab title. Expect (ideal): a clean product-facing title such as
Cinderdoc. Currently: it reads editor-web — Cinderdoc — the internal unit name
(editor-web) leaks into a surface every visitor sees, verified live and in the shipped
index.html on 2026-08-07. This currently fails the ideal and is logged as a defect below.

Final check: reload the page. Expect: identical state — still signed out, still no
documents, same copy as step 1.

F2

Sign in with Google, then sign out

Proves the real identity boundary: a live OAuth round trip against production, then a clean exit.

Persona / precondition: any Google or GitHub account; no active Cinderdoc session. (Exercised live in this pass with an already-authorized Google account, which completed without a visible consent screen — a fresh account will see Google's normal account chooser and consent screen naming Cinderdoc first.)

0/4

Click Continue with Google. Expect: the browser leaves cinderdoc.thewolf.tech for Google's
[sensitive content omitted]

Check the session panel after the redirect. Expect: it now reads Signed in as <your email>
with a Sign out button, replacing the sign-in card.
— verified live 2026-08-07 (Signed in as dominick.mckoy@gmail.com).

Click Sign out. Expect: the panel reverts to the Sign in to open recipient-bound
Cinderdocs
card.

Final check: reload the page. Expect: still signed out — no session persisted past the
sign-out.

F3

Create a Cinderdoc and read the honesty boundary

Proves the authoring surface shows its placeholder example and both honesty blocks before anything is typed.

Persona / precondition: signed in or out (Save is what requires sign-in, not opening the editor); no active draft.

0/6

Click + Create Cinderdoc. Expect: the Editor screen opens at app.cinderdoc.com/d/draft,
kicker DRAFT, toolbar Back to documents / Save / Export .cdoc ▾, and a fuse: 7 days
chip.

Check the fuse rail. Expect: FUSE · STEADY, a Fuse lifespan slider, timestamps
lit <today> · <time> and destructs <+7 days> · <time>, an Adjust fuse button, and a
Require capture protection toggle that is OFF by default.

Check the draft body without typing anything. Expect: kicker DRAFT · PRIVATE, greyed
placeholder title Counter-offer — 123 Elm St, and greyed placeholder body We propose a
purchase price of $1,240,000, contingent on inspection and financing, with a 30-day close. The
seller to credit $8,500 toward closing costs.

Check the two honesty blocks below the draft. Expect: a LIFESPAN block reading This
document self-destructs. At expiry, Keyvault permanently stops releasing its encryption key and
ledger-safe destruction then deletes the wrapped key forever. Destroying it early deletes the
key immediately. Either way, it can never be decrypted again by anyone, including us.
and a
separate CAPTURE block reading 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.
— the two claims never merge into one sentence.

Type a real title into the title field. Expect: the onboarding checklist's step 1 (Create
a document — type a title or body.
) turns into a .

Final check: with the typed title still in place, click Back to documents. Expect: a
confirmation modal, badge MEMORY ONLY, heading Discard this unsaved draft?, copy This draft
only lives in memory — leaving discards it. It has not been saved, exported, or sent to a server.
,
and buttons Keep editing / Discard draft and return — confirming nothing was silently
persisted.

F4

Set the fuse and the capture-protection toggle

Proves the expiry picker — the product's one hard promise — and the honestly-limited browser capability toggle.

Persona / precondition: in the editor with a fresh draft (F3).

0/7

Click Adjust fuse. Expect: a Set the fuse modal, badge MODAL, heading When does this
document burn out?
, sub-copy The load-bearing control. This is the whole promise., a live
lit · now / destructs · <timestamp> readout, and four presets 1 hour / 24 hours /
7 days / 30 days (7 days selected by default).

Click 24 hours. Expect: the destructs · timestamp and the Expires at field both jump
to ~24 hours from now.

Check the disabled preset. Expect: a greyed Destroy on first read button with the note
Unavailable: destroying a document on first read needs an approved server policy.

Check the confirm box. Expect: tag CONFIRM THE LIFESPANAt <timestamp> the key is
deleted forever — no recovery, no author bypass. You'll reopen this document through the same
sequence as any recipient.

Click Cancel. Expect: the modal closes and the fuse rail is unchanged from before step 1
(still 7 days) — cancelling never mutates the draft.

Toggle Require capture protection on. Expect: the switch turns on. ⚠ Not independently
verified past the switch itself: the shipped browser code (getCaptureCapability()) always
reports "unsupported", so saving with this toggle on from any browser is expected to be
refused server-side (see F7 step 2) — no browser can honestly claim OS-level capture blocking.

Final check: turn the toggle back off and confirm the fuse still reads 7 days before
continuing.

F5

Attempt to save without signing in

Proves Save fails closed for an anonymous author, with no network call and no silent document.

Persona / precondition: in the editor with a draft; signed OUT.

0/3

Click Save. Expect: no navigation and no hanging spinner — the status line updates
instantly to Sign in to create a recipient-bound Cinderdoc.
— verified live 2026-08-07.

Check the network log for the duration of step 1 (e.g. via browser devtools). Expect: zero
requests to /api/keyvault/* — the refusal happens before any network call is made. ⚠ verified
here by reading the shipped handleSave (it returns before calling saveDocument when no
session exists) and by an empty request log captured during the live click above; not
separately re-confirmed with devtools open in this pass.

Final check: sign in (F2) and click Save again on the same draft — it proceeds past this
refusal (continues at F7).

F6

Export .cdoc before saving

Proves the export control explains itself instead of silently doing nothing.

Persona / precondition: in the editor with a fresh, unsaved draft.

0/3

Click Export .cdoc ▾ in the toolbar. Expect: a panel titled Export .cdoc, badge
EXPORT, kicker EXPORT · DISABLED STATE (DETERRENCE FRAMING), and a disabled button reading
Export .cdoc — unavailable.

Read the two disclosures. Expect: WHY IT'S OFFSet a fuse and save first — a .cdoc only
ever carries encrypted content and key-custody metadata, never a decryptable copy.
and AND TO
BE CLEAR
Exporting doesn't add protection. Once a recipient opens it, capture protection is
still a deterrent, not a promise — it can't stop a camera, rooted device, hardware capture, or
retyping.

Final check: click Close — the panel dismisses and the draft is untouched.

F7

Save a real Cinderdoc while signed in

Proves the actual create → keyvault round trip every later flow depends on.

Persona / precondition: signed in (F2); a draft with a title and body typed. ⚠ Not executed in this pass — completing step 1 creates a real, persistent document against production keyvault, so this authoring session deliberately stopped short of it. Every Expect: below is quoted from the shipped source (editor-web/src/App.tsx's handleSave, src/lib/copy.ts), not independently observed live.

0/3

With Require capture protection OFF, click Save. Expect: status flashes Saving…, then
Saved.; you land on the dashboard with status Saved <your title>. Select it to open or export
its encrypted .cdoc.
and a new row under All live cinderdocs showing your title, a
Recipient-bound chip, remaining time (e.g. 7d 0h left), a fuse progress bar, and
capture unsupported.

Repeat with Require capture protection ON instead. Expect: Save is refused —
Saving requires true capture blocking, which this device cannot provide. This document cannot
be created here.
— and no row is added.

Final check: the new row's chip reads Recipient-bound, not the legacy badge — confirming the
document is bound to your signed-in identity from creation.

F8

Reopen your own Cinderdoc

Proves there is no author bypass: reopening runs the identical sequence a stranger would face.

Persona / precondition: F7's document exists; still signed in as its owner. ⚠ Not executed in this pass (depends on F7's real document). Expect: values are quoted from reopenDocument, OnboardingGuide, and describeReopenDenial in the shipped source.

0/4

Click the saved row, then Open. Expect: status Reopening…, then Reopened.; the title
and body you saved reappear exactly as typed.

Open the How to use tab. Expect: all three onboarding steps now show Create a
document
, Save it, Reopen it.

Sign out, then try to open the same document from the dashboard. Expect: refusal Sign in
with a listed verified email before opening this Cinderdoc.
— no key requested.

Final check: sign back in as the same account — the document opens again identically (the
earlier denial did not consume or damage anything).

F9

Send a Cinderdoc to a recipient

Proves the ciphertext-only send path and its custody disclosure.

Persona / precondition: F7's document; a second email address you control. ⚠ Not executed in this pass — sending dispatches a real email through Resend. Copy below is verbatim from SEND_COPY in the shipped source.

0/5

On the dashboard row, click Send Cinderdoc. Expect: a panel headed Send this encrypted
.cdoc
, showing Signed in sender: <your email> and Each address is added to this Cinderdoc's
recipient list. Recipients must sign in with that exact Google- or GitHub-verified email to
request the key.

Type one recipient email into Recipient emails. Expect: the preview line reads
1 recipient: <the address you typed> (or No recipients selected yet. while the field is
empty).

Read the two disclosures. Expect: Attachment custodyCinderdoc 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.
and
Recipient reminderRecipient binding does not stop an authorized recipient from sharing
what they can access.

Click Send Cinderdoc. Expect: the button reads Sending…, then Sent to 1 recipient. on
success (or the honest failure copy Some emails were sent and some failed. Review each address
before retrying.
/ No emails were sent. Your recipient list was updated; retry when email
delivery is available.
if delivery fails).

Final check: the recipient's inbox actually receives an email with the .cdoc attached (or the
honest failure copy above showed instead of a false success).

F10

A verified recipient opens; an unlisted account is denied

Proves recipient binding actually gates the key, in both directions.

Persona / precondition: F9 sent to a second account you control; ability to also sign in as a third, unlisted account. ⚠ Not executed in this pass — needs two additional real identities.

0/3

As the recipient, sign in and open the emailed .cdoc. Expect: the document opens; an
access notice reads Recipient-boundThis Cinderdoc opens only for a recipient whose Google-
or GitHub-verified email is on its recipient list.
plus the limit Recipient binding controls
who can fetch the key. It cannot stop an authorized recipient from photographing, recording,
retyping, forwarding, or otherwise sharing what they can access.

Sign in as a THIRD account not on the recipient list and try the same document. Expect:
refusal This Cinderdoc wasn't sent to <that account's email>. with body Sign in with a Google
or GitHub account whose verified email is on the recipient list. No key was fetched.
and a
Use a different account button — no key is ever requested.

Final check: the original owner (F7's account) can still open the document — recipient binding
adds readers, it does not remove the author's own access.

F11

Destruct a Cinderdoc early

Proves the irreversible manual key-destruction path and its confirmation gate.

Persona / precondition: a live document you own (F7); signed in as its owner. ⚠ Not executed in this pass — destruction is irreversible by design and would burn F7's evidence document.

0/4

On the dashboard row, click Destruct. Expect: a confirmation modal, badge final action,
eyebrow Burn the fuse now, heading Destruct "<your title>"? (curly quotes, as shipped), 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. Expect: the modal closes; the document is unchanged and still live.

Reopen Destruct and click Yes, destroy this document. Expect: status Destroyed.; the
row moves from All live cinderdocs to Destructed cinderdocs, reading burned out · key
deleted <timestamp> · unrecoverable
.

Final check: try to open the destroyed document. Expect: This document has expired or been
destroyed. Keyvault will never release its key again, so it can never be opened.
— permanently,
not just until a retry.

F12

Desktop viewer: download, verify, and the protected opening sequence

Proves the tester-channel distribution and the native capture-protection screens a browser can never show.

Persona / precondition: a Mac (the only platform with a real, human-validated build — Windows compiles but is explicitly ⚠ not validated on real Windows hardware per the shipped DOGFOOD record).

0/7

Load https://releases.cinderdoc.thewolf.tech. Expect: heading Open documents without
leaving expiry behind.
, a macOS tester build section, a Download Cinderdoc Viewer (.dmg)
link, and a link to the SHA-256 checksum file.
(GET only) 2026-08-07: page returns HTTP 200 and the checksum file returns
a6b61cd02aa1d5c1c4f154e1b3b8ed0d5793313bc55174b2bb62cecc1d55322a.

Download the .dmg (≈4.6 MB) and compare its SHA-256 against step 1's value before opening it.
Expect: they match exactly. ⚠ downloading and running this binary was intentionally not done
in this pass.

Install per the on-page instructions (System Settings → Privacy & Security → Open Anyway) and
launch with no document open. Expect: an empty state, kicker Protected viewing, heading
Open a protected document., a device-capability line, guidance The key is requested only
after this device passes the document's protection requirement.
, and an Open .cdoc… button.

Open a .cdoc that only requires detect-and-shield (the default policy — e.g. one saved
without Require capture protection). Expect: an Opening <filename> sequence screen
listing Read the .cdoc container & policy → (Verified recipient identity, if
recipient-bound) → Check this device's capture protectionFetch decryption key, each
ticking to in order, then the document renders with a rotating watermark and a footer
watermark active · watching for a supported capture signal.

Trigger a real screen recording (e.g. Cmd+Shift+5) while the document is open. Expect: the
content is replaced by a shield screen, heading Covered — screen capture detected, body A
supported screen capture signal was detected, so this document is masked. It will restore after
that signal clears. This shield is a deterrent — it can't stop a camera pointed at your screen
or hardware capture.
, status SHIELD ACTIVE · screen capture detected · content masked; it
un-shields automatically once recording stops.

Open a .cdoc that requires block-supported-capture (only reachable via a document a real
Windows Tauri build saved with that policy — the browser and macOS can never declare that
capability). Expect on macOS: This Mac can't meet the required protection.This
document requires capture blocking. macOS can only detect a recorder and shield — it can't block
capture outright — so the key was never fetched. Open it on a device that supports blocking.

⚠ not exercised in this pass (no such document exists); copy verified from shipped source only.

Final check: click Close on an open document. Expect: it returns to the empty state from
step 3 with no residual document content, watermark, or status chrome left behind.

F13

Public honesty surfaces and error paths

Proves the parts anyone can check without an account: the privacy notice, the version advertisement, and a few refusals.

Persona / precondition: none — signed out, no session, terminal with curl.

0/6

Load https://cinderdoc.thewolf.tech/privacy directly (not via a link). Expect: HTTP 200, and
the page renders as Cinderdoc privacy, Effective July 23, 2026 · Last updated July 23, 2026,
ending with a Back to Cinderdoc link — served from the same single-page bundle as the editor
(client-side routed), not a 404.

Run curl -s https://cinderdoc.thewolf.tech/api/keyvault/health. Expect exactly:
{"ok":true,"service":"keyvault","apiVersion":"2.0.0","minClientVersion":"2.0.0"}

Run curl -s https://cinderdoc.thewolf.tech/api/keyvault/documents/does-not-exist/status.
Expect: HTTP 404, body {"error":"not_found"} — a made-up document ID gets an honest
not-found, not a stack trace or a 500.

Run curl -s -X POST https://cinderdoc.thewolf.tech/api/keyvault/documents -H 'content-type:
application/json' -H 'X-Cinderdoc-Client: 1.0.0' -d '{"policy":{},"recipientEmails":[]}'
.
Expect: HTTP 426, body containing "error":"client_upgrade_required",
"message":"This Cinderdoc requires a newer app before it can be opened.",
"minClientVersion":"2.0.0", and "downloadUrl":"https://releases.cinderdoc.thewolf.tech" — an
old or missing client is refused before anything is created. ⚠ not executed in this pass (a
POST, deliberately avoided); expected body quoted verbatim from the shipped version-gate.ts,
and this exact 426 behavior was already independently confirmed once at the project's own
2026-07-24 production sign-off.

Load a URL that doesn't exist on the site, e.g.
https://cinderdoc.thewolf.tech/this-route-does-not-exist. Expect (ideal): either a real
404 or a distinct "page not found" view. Currently: HTTP 200 with the ordinary signed-out
dashboard — the SPA's server-side catch-all serves the same shell for every unmatched path, and
the client router only distinguishes / and /privacy, silently falling back to the normal
dashboard for anything else. This could mask a mistyped link for a tester or a customer.

Final check: the browser tab title reads editor-web — Cinderdoc on every page checked above
(dashboard, /privacy, and the unmatched-route fallback) rather than a clean Cinderdoc — the
same internal-name leak noted in F1 step 5.

Not covered and why

  • GitHub sign-in: structurally identical to Google per the shipped AUTH_COPY, not

independently exercised live in this pass to avoid a second real-account side effect.

  • Windows viewer: compiles clean (cargo check --target x86_64-pc-windows-gnu) but is

explicitly ⚠ not validated on real Windows hardware per the unit's own DOGFOOD record; no
Windows machine was available for this pass.

  • Legacy (pre-v2, non-recipient-bound) documents: the v2 cutover preserved old rows in

keyvault's database, but the dashboard reads a per-browser local index — no legacy document
currently appears in any browser profile available to this pass, so the Legacy access — not
recipient-bound
badge and warning can't be exercised without deliberately seeding one.

  • Backup/restore drill, rate limiting, scheduled destruction sweeps (KV-T011/KV-T012/SD-T016):

operational, not user-facing; already drilled once per the project's own STATE.md/CLAUDE.md
record, not re-run here.

  • "Destroy on first read": intentionally disabled in the UI itself (see F4 step 3) — not a real

feature yet.

  • Multi-recipient Send and the retry-after-partial-failure path: only a single-recipient send

was reasoned through against source; the 2+-address and "Send again" retry UI exist but weren't
walked.

  • Rooms/team identity sharing, offline grace, key rotation: named, explicitly deferred product

epics per STATE.md — not built yet.

Results

FlowVerdictFailures → tickets
F1 cold load
F2 sign in / sign out
F3 create + honesty copy
F4 fuse + capture toggle
F5 save while signed out
F6 export before save
F7 save while signed in
F8 reopen own document
F9 send
F10 recipient binding
F11 destruct
F12 desktop viewer
F13 public surfaces / errors
updated just nownext 3m 00s