-
2026-07-08 — wu-0a merged (
f17a70d): Phase 0 security hotfixes complete — Razorpay HMAC verification (both endpoints), idempotent finalization + unique index migration (supabase/migrations/20260708120000_...— apply before deploy),payment_pendingstate machine, strict cart ownership (uid-owned carts require the authenticated user), failure flow moved to ownership-checked POST,cookies.getAll()leak removed, hardenedclientIdcookie, anon/supabaseAdminprivilege split (smoke-test RLS-dependent flows before production deploy), shared server helpers + 8 unit tests (13/13 green). -
2026-07-08 — wu-1a merged (
f52aaad): Phase 1 api/ skeleton complete — Hono app factory (AppTypeexported), identity middleware (verified JWT → user; clientId cookie → guest, optional HMAC signing viaCLIENT_ID_SIGNING_SECRET), CSRF (origin required for cookie-authed mutations), structured JSON logging, request-id, rate-limit baseline,requireAuth(),/health+ masked db probe, typedhc<AppType>client wired into SvelteKit, cart contracts/status constants, drizzle + fallback schema (cart, products). Pending: realdrizzle-kit pullintrospection — blocked onPOSTGRES_URLusing the direct DB host (IPv6-only, ENOTFOUND locally); switch it to the Session pooler connection string, then runnpm run db:pull -w @scweb/api. -
2026-07-08 — wu-0c merged (
8d8a240): cart UI bugs fixed —isUpdatingCartdeadlock,checkOut()crash, negative-qty persistence (extractedresolveCartQuantityChangepure fn), errors surfaced via toast; +6 tests. -
2026-07-08 — wu-0d merged (
e5fc190): quality gates —npm run build,npm run lint(ESLint 9 flat config; deferred debt inLINT_DEBT.md), prettier sweep,.env.examplecomplete,$libalias fixed,@sveltejs/adapter-node(Dokploy). -
2026-07-08 — wu-0e merged (
6051483): svelte-check 0 errors (was 128); correctedChat/printrequeststypes against the live DB; declared behavior fixes: failure redirect guards, pay-button NaN gate, maker-orders username query. PHASE 0 COMPLETE. -
2026-07-08 — wu-2a merged (
514ed27): Phase 2 schema —carts,cart_items,orders,order_itemswith partial unique indexes, status CHECKs, UNIQUE razorpay ids; money convention: whole INR rupees at rest, paise only at the Razorpay boundary (rupeesToPaise); idempotent per-cart-resilient backfill script. Migrationapi/drizzle/0001_...anddb:backfill-cartsare not yet applied/run — user applies when ready. -
2026-07-08 — wu-2b merged (
67811a8): cart endpoints —GET /cart,PUT /cart/items/:productId(transactional, stock ceiling, lazy creation,FOR UPDATE),POST /cart/merge(clientId-match guard, clamp-all invariant); PGlite integration tests against the real store. -
2026-07-08 — wu-2c merged (
56d7c1d): checkout endpoints —POST /checkout/order(snapshot + idempotent re-entry with total refresh),POST /checkout/confirm(HMAC-first,FOR UPDATE+ atomic paid claim BEFORE stock decrement — concurrent double-decrement impossible),POST /checkout/fail(paid never downgraded). 71 api tests. Runtime prerequisites still on user: applyapi/drizzle/0001_..., rundb:backfill-carts, fixPOSTGRES_URLto the session pooler. -
2026-07-08 — wu-2d merged (
3b8161c): frontend swapped onto the typed API client via a hardened same-origin proxy (/api/[...path]— path allowlist, SSRF-safe URL build + regression tests, Origin forwarding for CSRF,safeGetSession-validated Bearer only, 10s timeout → 502). Legacysrc/lib/client/cart.tsDELETED. Guest-cart merge on sign-in. API cart items now carry author/guarantee. PHASE 2 CODE-COMPLETE. Runtime activation still gated on user: poolerPOSTGRES_URL, applyapi/drizzle/0001_...+supabase/migrations/20260708120000_..., rundb:backfill-carts, then live smoke test + the Playwright checkbox. Cleanup unit (dropget_cart_by_uid/update_cart_by_idRPCs + jsonb list column) runs only AFTER live verification. -
2026-07-08 — wu-3a merged (
96e5ced): print-file endpoints in the API — streaming-capped uploads (50MB, forged content-length safe), structural STL sniffing, CSPRNG storage keys, race-safeupload_quotatable (migrationapi/drizzle/0002_..., UTC day boundary — intentional change), row-authorized signed download URLs, compensating quota decrement + orphan cleanup on partial failure. -
2026-07-08 — wu-3b merged (
918a1c4): all four frontend callers swapped to the API via the proxy (streaming, 60s upload timeout,print-filesallowlisted); typedprintFilesApi; stuck-download-loading bug fixed; Supabase Edge Functions DELETED (upload/download-model-request). -
2026-07-08 — wu-3c merged (
16ff6ff): print-request payments on the API — amounts exclusively from maker-authored quotes, confirm bound to the order-created amount (quote-drift logged), HMAC-first, FOR UPDATE + stage claim, purchases parity, cross-order replay blocked; legacy 3dp payment+server.tsDELETED. -
2026-07-08 — wu-3d merged (
09549d7): portal mutations server-side —POST /print-requests/:id/actions(role/stage transition map, server-setby: 'maker'→ quote-forgery vector closed),POST /chats/messages(participant-checked, actor-derived sender), CreatorStats recalculated server-side on completion,paid_externallyshippable, statsUpdate 403s non-owners. PHASE 3 COMPLETE. Chat read-receipts remain client-side (RLS-dependent — Phase 4). -
2026-07-08 — wu-4a merged (
eaa000a) + RLS drafts (92fc231): Phase 4 code complete — validated sessions on every server gate, single canonical/auth/callback(deaduser/pincodedeleted — remove from Supabase/OAuth redirect allowlists, see DEPLOY_NOTES.md),sanitizePostLoginUrlat write+ read (length/control-char capped), profile pages on uid-filtered server loads. RLS lockdown drafted from a live pg_policies audit:api/drizzle/0003_enable_rls.sql(new tables deny-by-default) +supabase/migrations/20260708130000_rls_lockdown.sql(purchases open-INSERT, addresses/reviews uid-spoof, printrequests direct UPDATE, CreatorStats self-writes, Chat column-level status grant) — apply after code deploy. Remaining Phase 4: RLS integration tests + live smoke, gated on the DB actions in DEPLOY_NOTES.md. -
2026-07-08 — wu-5a merged (
bc0137b), wu-5b merged (1773c00): Phase 5 observability + tests (backups excluded per user) — structured JSON logging web+api with request-id propagation, Sentry-protocol error reporting (optional DSN; GlitchTip-compatible; cookie/auth scrubbing), transactionalaudit_logfor every money/order transition (migrationapi/drizzle/0004_...), Playwright smoke suite (9 green + 2 DB-gated), GitHub Actions CI (lint/check/unit/build/e2e), placeholder tests removed. PHASE 5 (obs+tests scope) COMPLETE. Test counts: 63 root unit + 155 api + 9 e2e. -
2026-07-08 — wu-6a (
b066fb0), wu-6b (8807330), wu-6c (32a4291) merged: Phase 6 complete — proxy dot-segment rejection, fee constant deduped, single-image Dockerfile for Dokploy (web:3000 + api:3001, two domains, runtime-only secrets — verified via no-.env build simulation; realdocker buildstill pending, daemon was down locally), ALL deferred lint rules re-enabled andLINT_DEBT.mddeleted, shared domain types (anyeliminated),supabase_ltrename, dead code/commented blocks purged, two pre-existing stored-XSS holes fixed (chat{@html}→ plain text; creator product HTML now DOMPurify-sanitized), a11y warnings 17→0 with keyboard nav preserved, alerts→toasts, Razorpay/model-viewer error states. ALL CODE-SCOPE PHASES (0–6) COMPLETE. Final gates: build/lint/ svelte-check 0/0, 65 root + 155 api unit tests, 9 e2e (+2 DB-gated). Remaining work is runtime/decision-gated: see DEPLOY_NOTES.md (user actions), then RLS integration tests + live smoke + Phase 2 legacy cleanup unit; open decisions: Razorpay webhooks, payouts model, backups (user-owned).
Consolidates TODO (2026-07-07), the cart persistence review (2026-07-08), and the
Hono + Drizzle migration into one sequenced plan. TODO stays as the raw backlog;
this file is the order of operations.
- Don't fix things twice. Anything that dies naturally with the Hono + Drizzle migration (RPC quirks, Edge Function CORS, browser-side mutations) gets a hotfix now only if it's exploitable or user-facing today. Everything else is fixed once, in its migrated home.
- Money paths first. Checkout and print-request payments are where correctness bugs cost real money. They migrate first and get the deepest tests.
- Correct by construction over correct by patch. Prefer schema constraints (unique indexes, CHECKs, state machines) and single-statement/transactional SQL over application-level guards. Bugs like duplicate carts, negative quantities, and oversell should become impossible, not just handled.
- The frontend becomes thin. SvelteKit renders and calls a typed API client.
No business mutation lives in
+server.ts,+page.server.tsside effects, or browser Supabase calls when the migration is done.
Live vulnerabilities and user-facing breakage. Small, surgical patches to existing code — all of this gets rebuilt properly in Phases 2–3, but none of it can wait.
- Verify Razorpay signatures before marking anything paid.
HMAC-SHA256 over
order_id|payment_idwith the key secret, constant-time compare. Files:src/routes/(cart)/checkout/createOrder/+server.ts(PATCH),src/routes/3dp-portal/(authenticated)/user/[id]/+server.ts. - Idempotency guard on payment finalization: reject PATCH if cart/request is
already
paid; add a DB unique constraint on the Razorpay payment id. (Full idempotent finalization lands in Phase 2; this stops duplicate stock decrements and purchase rows today.) - Cart ownership on order creation:
createOrderPOST must match the cart against the caller'sclientId/user id, not accept arbitraryorderId. - Cart failure page: page load mutates any cart to
failedvia admin client. Convert to a POST with ownership + provider-state checks, idempotent. File:src/routes/(cart)/summary/failure/[cart_id]/[order_id]/+page.server.ts. - Stop returning
cookies.getAll()through+layout.server.tspage data. - Set explicit
httpOnly,secure,sameSite,path,maxAgeon theclientIdcookie (src/hooks.server.ts). - Split Supabase clients by privilege: user-scoped SSR client (anon key +
cookies) vs
supabaseAdmin(service role, server-only, used only behind explicit ownership checks). Rename so the dangerous one looks dangerous.
-
isUpdatingCartdeadlock: earlyreturnafter setting the flag but beforetry/finallyinupdateCartQuantityandremoveItempermanently disables all cart buttons. File:src/routes/(cart)/cart/+page.svelte(~L95, ~L157). -
checkOut()crash: readsproductDetailsCache[id].stockwhich is undefined while details are loading/failed → TypeError instead of a toast. - Negative quantity persists: in
changeCart, onlynewQty === 0removes; a relative change landing below zero skips the stock check and writes a negative qty. ClampnewQty <= 0→ remove. File:src/lib/client/cart.ts. - Surface
changeCarterrors on the cart page (currentlyconsole.erroronly; the stock-limit message is never shown there).
-
npm run buildpasses (missingPUBLIC_RAZORPAY_IDenv export et al.). -
npm run checkpasses (34 errors / 82 warnings currently). -
npm run lintpasses (Prettier: 154 files) and becomes a CI gate. - Add
.env.examplewith all required variables (no real secrets). - Fix
svelte.config.js$libalias (./src/libs→./src/libor remove). - Production adapter (decided 2026-07-08): deploying on Dokploy →
@sveltejs/adapter-nodefor the SvelteKit app. Dockerfiles (web + api) are needed before the first Dokploy deploy — scheduled with the Phase 5 deployment checklist, not now.
Nothing user-visible moves yet; this phase makes migration slices cheap.
- Repo layout (decided 2026-07-08): Hono service lives in
api/inside this repo with its ownpackage.json/tsconfig; shared contracts (types, zod schemas, status constants) live inapi/src/contractsand are consumed by SvelteKit via the exported Hono client type (hc<AppType>) and a TS path alias — no separate packages workspace. - Drizzle setup in
api/pointed at the existing Postgres viaPOSTGRES_URL(already in.env; Supabase's PG is just a connection string — vendor independence lives in the schema and queries, not the host). Introspect current tables; from here ondrizzle-kitmigrations are the source of truth for schema changes. - Hono app skeleton with cross-cutting middleware, so every migrated route
inherits it for free: - identity: verify Supabase JWT → user id, else signed
clientIdcookie → anonymous actor (treatclientIdas a cart hint, not identity) - zod request validation (kills the manualFormData/anychecks) - structured logging (route, request id, user id when safe, order/payment ids) - error monitoring hook - rate limiting baseline (tightened per-endpoint in later phases) - CSRF strategy for state-changing routes (cookie identity ⇒ CSRF token or strict same-site + origin checks) - Typed API client in
packages/shared(Hono RPC client or generated types) — the only wayapps/webtalks to the API. - Centralize status strings as typed constants/enums in
packages/shared(cart, purchase, print-request states) — needed before state machines. - CI: build + check + lint + test gates on both apps.
The money path. Replaces the client-side Supabase-RPC cart entirely; the bug list from the cart review dies here by design, not by patching.
-
carts:id, user_id, client_id, status, created_at, updated_at. Partial unique indexes —(user_id) WHERE status='active'and(client_id) WHERE status='active' AND user_id IS NULL— make duplicate active carts impossible (today: every visitor gets a cart row on page load, and concurrent loads create duplicates thatselectActiveCartpapers over). -
cart_items:cart_id, product_id, qty CHECK (qty > 0), unique(cart_id, product_id). Normalizing out of the jsonblistblob kills the whole-list read-modify-write race (today: last write wins, concurrent changes silently dropped). - No prices stored in the cart. Price live at read/checkout; snapshot into
orders/order_itemsat order creation. Kills the staleness class (today: storeditem.pricedrives the subtotal while rows and the actual charge use live price — they can disagree). -
orders/order_items: price + name snapshot, address (validated, structured — not a raw JSON string), Razorpay ids. Constraints: one purchase per successful payment id; one paid order per cart; status CHECK enforcing the state machineactive → payment_pending → paid → fulfilled/cancelled/refunded(no paid → active/failed transitions). - Backfill: migrate active carts from
cart.listjsonb into the new tables.
-
GET /cart— cart + live-priced items joined with product name/image/stock + server-computed totals. Never creates rows. -
PUT /cart/items/:productId { qty, mode: 'set' | 'add' }— one endpoint for add/update/remove: resulting qty ≤ 0 deletes; otherwise a singleINSERT ... ON CONFLICT DO UPDATEwith the stock ceiling checked in the same transaction. Lazily creates the active cart here — the only place. -
POST /cart/merge— on login, fold guest cart into user cart (sum quantities, clamp to stock), delete guest cart. Fixes silent guest-cart loss (confirmed: the current RPC matches onlyuidwhen logged in). -
POST /checkout/order— transaction: re-validate stock, snapshot priced items + validated address intoorders, create Razorpay order with the server-computed amount. -
POST /checkout/confirm— verify HMAC first, then one transaction:UPDATE products SET stock = stock - qty WHERE id = ... AND stock >= qty(atomic oversell guard — also closes the long-standing "can add to cart with no stock" item), mark order paid, insert purchase. 409 on guard failure. Idempotent: safe to retry, duplicate confirms are no-ops. - Payment failure/abandon endpoint (replaces the failure-page side effect): idempotent, ownership-checked, defined behavior for abandoned modal / failed payment / duplicate callback. Consider Razorpay webhooks as the final source of truth; client callbacks only submit proof.
- Don't store payment signatures in user-visible records; keep only what audit needs, server-side.
- Replace
src/lib/client/cart.tswith a thincartApimodule + one cart store fed only by API responses; optimistic item-count update with rollback. - Cart page drops
productDetailsCache/ per-rowgetItemDetails— the API returns items pre-joined (removes the Phase 0 crash and error-surfacing patches for good). - Swap route by route: cart page → product page add-to-cart → checkout.
- Call
POST /cart/mergeafter successful login.
- Drop
get_cart_by_uid/update_cart_by_idRPCs, the jsonblistcolumn, and the browser-side cart insert path. - Checkout/payment unit tests: invalid cart id, cart not owned by caller, wrong signature, duplicate confirm, out-of-stock, price mismatch, empty cart, merge-on-login, concurrent quantity updates.
- Playwright smoke: cart add/remove, checkout guard.
Same treatment as checkout; retires the Edge Functions.
- Move upload/download from Supabase Edge Functions into Hono, then delete
supabase/functions/upload-model-request.tsanddownload-model-request.ts. In the move (not before — CORS*, extension-only validation etc. die with the functions): - download authorizes byprintrequest_id→ row →user_id/creator_idcheck → signed URL for that row's path (never a user-supplied path) - upload: random storage keys, size limit, basic STL structure validation, sanitized metadata,upsert: false, private bucket - quota enforced in a transaction / unique-constraint table (race-safe) - rate limits + logging of user id/IP/UA; malware/abuse controls - Print-request payment flow gets the Phase 2 payment pattern: server-owned
amounts (never trust client
amountor event history), signature verification, idempotency, state machinerequested → quoted → order_created → paid → shipped/completed/cancelled. - Maker stats update: require
user.id === maker_id(or admin role). - Move browser writes to
Chat/printrequests(maker + user portal pages) into authenticated Hono endpoints. - Print-request payment tests: pay someone else's request, wrong amount, duplicate payment, invalid signature, missing quote, invalid address.
- Edge-function replacement tests: upload/download authorization, quota, size/type rejection, signed URLs.
- Migrate profile / orders / addresses / reviews / maker-application mutations
to the Hono API; sensitive profile pages load via server with explicit
ownership (replaces
+page.tsbrowser reads). -
getUser()(validated) instead ofgetSession()for trusted checks everywhere; single auth callback route (remove theuser/pincodeduplicate);postLoginredirects restricted to same-origin relative paths. - Audit every remaining admin-client query for ownership checks
(search:
supabaseServer/supabaseAdmin). - RLS endgame: with mutations behind Hono, browser Supabase shrinks to auth + (maybe) reads. Document per-table policy expectations for whatever remains browser-accessible; test that one user cannot touch another's rows (carts, addresses, purchases, printrequests, chats, filaments, creator stats). Supabase Auth/Storage stay only as intentional choices, not the data layer.
Some of this lands incrementally from Phase 1 middleware; this phase completes it.
- Replace remaining
console.*scatter with the structured logger. - Error monitoring across web app, API, and payment/upload failures.
- Audit log for money/order state changes: who, old → new state, provider ids, timestamp.
- Refund/cancel/retry story wired to the state machines (define what support can do, and what happens on partial failures).
- Backups: DB backups, storage backup policy for models, restore drill for orders/purchases.
- Deployment checklist: env vars, migrations, RLS verification, private buckets, Razorpay webhook secret, CORS origins, CI gates.
- Playwright smoke suite complete: home, product detail, cart, checkout guard, auth redirect, maker portal guard, upload happy-path stub.
- Replace placeholder tests (
tests/test.ts,src/index.test.ts).
Do opportunistically inside earlier phases when touching a file; sweep the rest here.
- Shared domain types (
PrintRequest,PrintRequestEvent,PaymentIds,RazorpayCheckoutResponse,MakerApplication,Filament,CreatorStats); remove broadanyfrom domain data. - Import generated DB types properly in
src/app.d.ts(post-Drizzle these come from the Drizzle schema, not Supabase codegen). - Svelte 5 migration cleanup:
$state()typing, bindables, deprecated<slot>, non-reactive updates; Bits UI version/API alignment. - Naming normalization (
supabase_lt,payment_id_a/paymentIdA,PrintingCrafters, …) and removal of large commented-out blocks (cart, payment, model viewer, checkout, portals — git is the history). - Delete dead/generated files (timestamped vite configs, sample components,
src/index.test.js). - Rule enforced from Phase 2 onward, swept here: loads read, endpoints mutate —
no side effects in
load. - Accessibility pass: label/control association,
aria-labelon icon buttons, keyboard support on interactive elements; unused CSS selectors; replacealert()with toasts; loading/error states for Razorpay + model upload/download; mobile checks for cart/checkout/portal/viewer/upload.
Phases 2–4 moved every MUTATION behind the API; reads deliberately stayed on browser → Supabase PostgREST (anon key + RLS) for velocity. Phase 7 removes the remaining coupling in ascending order of difficulty. Goal state: Supabase is just "a Postgres host + (for now) an auth provider" — swappable.
- Inventory every remaining browser/server Supabase READ: products (home
sections, browse/crafts, product detail, related),
constants(BANNERS, FILTYPES), reviews, users/usernames, portal reads (printrequests lists, maker profiles, filaments, CreatorStats/Reviews), profile pages (already server loads — swap their internals last). - API GET endpoints on Drizzle for each read cluster (
/products,/products/:id,/constants/:key,/reviews,/makers, …), typed via contracts; add short-TTL in-memory caching on hot lists (products, constants) — invalidation not needed at this traffic level, TTL is fine. - Schema coverage:
npm run db:pull(needs pooler URL) to introspect the full live schema intoapi/src/db/schema/— replaces the remaining hand-written fallback tables. - Swap consumers cluster-by-cluster through the existing proxy + typed client (pattern proven in Phase 2/3); the api needs a GET-heavy rate limit tier.
- Endgame: browser Supabase client shrinks to auth + realtime chat only; anon key stops mattering for data; tighten RLS to deny-all on tables no longer browser-read.
- Replace Supabase Realtime subscription in
MessageBoardwith API-served WebSocket or SSE fed by the chats store; move read-receipts to a small authenticated endpoint at the same time (removes the last browser write + the Chat column-grant RLS dependency).
Supabase Auth stays as the auth provider. No Keycloak/Zitadel migration. The API already treats auth as "verified JWT → user id"; the browser keeps supabase-js for sign-in, OAuth, and session cookies. 7c is cancelled.
- Payouts model (decide before the
ordersschema is considered final): platform account + manual batches vs Razorpay Route/split settlements vs another provider. Drives accounting, refunds, GST, disputes, maker KYC, payout reconciliation — and possibly anorder_items.maker_id+ settlement tables in the Phase 2 schema. - Razorpay webhooks vs client-callback-plus-verification as the final payment truth (recommendation: webhooks; callbacks as fast-path only).
Username auto-generation for Google login · profile picture upload · maker verification email · Fabbly color/material picker fixes (animation, mobile, dragging, infill display) · strength selection description · Z-axis constraint + maker-adjustable area constraints + pre-quote model area check · available-makers material filter · model download error handling and STL failure fallback (ties into Phase 6 loading/error-state work).