The cart is a signed cookie (src/cart.ts). That is exactly right for a guest and wrong for an account: filling a cart on a phone, signing in and finishing on a laptop loses it, and the direction's own sentence is that a visitor browses as a guest, keeps a cart, and buys with an account.
Done looks like
[ ] One table, e.g. cart_line (buyer_id, variant_id, quantity, updated_at) keyed on (buyer_id, variant_id), read and written as plain data by one small module.
[ ] ctx.cart is the account's rows when ctx.who.role !== "anon" and the cookie's lines when anon. Handlers keep the type they have today and do not learn which it was.
[ ] Signing in merges the browser's lines into the account's: a line in both takes the larger quantity, never the sum, so the merge is idempotent and a reload or a prefetch cannot double a cart. The cart cookie is cleared once the merge is done.
[ ] A signed-in shopper's cart writes rows and no cookie; signing out leaves the account's rows alone and leaves the browser empty.
[ ] The boundary holds: quantity is still clamped 1..99, a variant that has gone inactive still drops out of the quote the way it does today, and a buyer cannot hold more than a fixed number of lines (50 is plenty) — a table a stranger can grow is a table with a cap.
[ ] Rows untouched for 90 days are swept at boot, beside the session sweep already in src/server.ts.
[ ] Tests: add as a guest, sign in, the line is there; the same account in two browsers sees one cart; merging the same cookie twice does not double a quantity; a cart survives sign-out and sign-in; every existing anonymous cart test still passes untouched.
[ ] README says where a cart lives in each case, and for how long.
Files I expect to change:src/db.ts, src/cart.ts (or a new src/heldcart.ts if cart.ts stops being one concept), src/shop.ts, src/context.ts, src/server.ts (the sweep), src/server.test.ts, README.md.
Risks: this changes where ctx.cart comes from under every handler that reads it, so the shape of a CartLine must not move. It waits on #10 so that the id that owns a cart is the same forge id that owns an order — two different answers to "whose is this" would be worse than the cookie. The merge is the one moment a guest's data crosses into an account: it is idempotent by the max-quantity rule, so it is safe wherever it is called from, and it must never run for an identity the app could not verify.
The cart is a signed cookie (
src/cart.ts). That is exactly right for a guest and wrong for an account: filling a cart on a phone, signing in and finishing on a laptop loses it, and the direction's own sentence is that a visitor browses as a guest, keeps a cart, and buys with an account.Done looks like
cart_line (buyer_id, variant_id, quantity, updated_at)keyed on(buyer_id, variant_id), read and written as plain data by one small module.ctx.cartis the account's rows whenctx.who.role !== "anon"and the cookie's lines when anon. Handlers keep the type they have today and do not learn which it was.src/server.ts.Files I expect to change:
src/db.ts,src/cart.ts(or a newsrc/heldcart.tsifcart.tsstops being one concept),src/shop.ts,src/context.ts,src/server.ts(the sweep),src/server.test.ts,README.md.Risks: this changes where
ctx.cartcomes from under every handler that reads it, so the shape of aCartLinemust not move. It waits on #10 so that the id that owns a cart is the same forge id that owns an order — two different answers to "whose is this" would be worse than the cookie. The merge is the one moment a guest's data crosses into an account: it is idempotent by the max-quantity rule, so it is safe wherever it is called from, and it must never run for an identity the app could not verify.Suggested: ledger.