ADR-0003: Anonymous Visitor build persists in cookie + claims to account on sign-in¶
- Status: Accepted
- Date: 2026-04-26
- Deciders: MASTER
- Related: issue #11 (Auth.js), issue #12 (UC-C save), issue #13 (UC-D share), Architecture → Use cases
Context¶
Use case UC-C (Save a build) was originally drafted with two parallel paths:
- Account-bound save — Builder signs in, build persists in their account, syncs across devices
- Anonymous save with URL only — Visitor saves, gets a public URL, can come back to it without an account
The Use Case Diagram (UML #1) flagged this as an open decision. The question is whether anonymous Visitors should be able to "save" at all, and if so, where the state lives.
The pull on the anonymous side: PC building is a lot of work to throw away. A Visitor who spent 20 minutes assembling a Build shouldn't lose it because they closed the tab or didn't sign in. Forcing sign-in to save is a friction wall right at the conversion point.
The pull on the account side: cross-device sync, naming, a library of builds, and the ability to attach price-drop alerts (UC-G) all require an identity.
Decision¶
Both, with claim-on-sign-in.
- Anonymous Visitor's build persists in a signed
build_sessionHTTP cookie, 1-year expiry, signed with a server secret to prevent client tampering. The cookie holds either the build state directly (if small enough) or a server-side session ID that points to the build row. - On sign-in, the cookied build is "claimed" into the new Builder's account in the same request that completes auth. The cookie is then cleared. Build now belongs to the user and survives across devices.
- URL-share (UC-D) is independent of save scope. Every saved build — anonymous or account-bound — gets a public unlisted slug. Shareability is a property of the build, not of who owns it.
Consequences¶
Positive¶
- No friction wall at save time. Visitor can build, walk away, come back tomorrow on the same browser, build is still there
- Sign-in is a value-add ("save across devices, get alerts"), not a gate
- Single mental model: every build can be shared via URL, regardless of ownership
- Account claim is a one-time delight moment — "your build came with you"
Negative¶
- Two storage paths to maintain in code: cookie path and account path. Mitigated by making both serialize/deserialize through the same
Buildshape - Cookie size limits — if a Build's serialized form ever exceeds ~4KB, must switch to server-side session ID (already planning for this from day one)
- Need to rotate the cookie signing key periodically; that becomes a runbook (
rotate-build-session-cookie-key.md) - Race conditions during claim — if a user has builds on two devices and signs into the same account on both, we need a merge policy (later concern; for v1, last-write-wins is fine)
Neutral¶
- Auth.js (issue #11) becomes a hard dependency for UC-C polish but not for the core save flow — the cookie path works without auth at all
Alternatives considered¶
Sign-in required to save¶
Rejected because: forcing identity at save time is the strongest possible friction wall, placed right at the moment a user has invested most in the product. The metric this would optimise (signups) trades against the metric we actually care about (conversions to deep-link click).
Anonymous save only, no account claim¶
Rejected because: cross-device sync is a real user need (started build at work, want to finish at home), and price-drop alerts (UC-G) require an email anyway. Half-stepping with anonymous-only forces users into a worse account-creation experience later.
LocalStorage instead of a cookie¶
Rejected because: localStorage is per-origin per-browser, doesn't survive a private window, and can't be claimed server-side without a separate sync step. Cookies are the right tool — they auto-attach to requests and we can claim atomically with the auth response.
Server-side anonymous account with auto-generated ID¶
Rejected because: indistinguishable from a real account except no email, but creates a maintenance burden of "ghost accounts" cluttering the DB. The cookie + 1y expiry is simpler.
References¶
- Architecture → Use cases — pending decision section, now resolved
- Auth.js docs on credential cookies (will be referenced from RFC for issue #11)
- Cookie signing:
iron-sessionor built into the Auth.js session strategy (decision pending in issue #11)