[17]
BAZARTY.COM (SUPABASE)
▒ TIER C · YEAR 2026 · STATUS: LIVE · LANGUAGES: TYPESCRIPT
Escrow, Postgres full-text search and spec-first delivery
[FIG. 1] MISSION
I design escrow: a payment frozen until delivery confirmation, with intermediate states and disputes handled. bazarty.com exists in two architectures — this is the Supabase variant; the Payload + MongoDB variant is described separately. bazarty is a handmade-and-art marketplace connecting makers with buyers: a seller opens a portfolio shop, a buyer browses a Pinterest-style catalog (boards, likes, mosaic covers), and funds reach the seller only after delivery confirmation. The entire post-purchase (purchase → shipped → delivered → receipt → review → dispute) lives in the 1:1 chat thread as transaction cards, reusing one data primitive and simplifying the UX. I built search as full-text search inside Postgres itself, because for this catalog vectors would be overkill — and I can point to that boundary. I work spec-first: a planning package with a contract comes before implementation, and I run the backlog as an engineering artifact with acceptance criteria, not a wish list. I can also move a generative prototype into disciplined delivery and name what survives from the prototype and what gets rewritten. Stack: TanStack Start (SSR, 34 file routes) and the full Supabase stack: 35 SQL migrations with RLS and SECURITY DEFINER RPCs, 9 Deno edge functions (Stripe Connect, webhooks, Furgonetka pricing and labels, a mail queue, an auto-release cron with an 88-day backstop). Every architectural decision goes on paper — 20 ADRs with a 'why + alternatives' section — and launch phases pass code review and security review before anything gets checked off.
[FIG. 2] ARCHITECTURE
hover a block to see its description
[FIG. 3] CHALLENGES
[+][CH-01]
Handmade goods are one-offs — qty of 1 — so two parallel payments could 'sell' the same piece twice, and the Stripe webhook would move both orders to paid. The first reservation version (a 15-minute hold on a partial unique index WHERE status='active', error code 23505 as idempotency) looked fine — only a code review exposed the gaps: holds weren't released on errors (stranded holds), and the pending→paid transition itself had no collision guard. I closed it with an atomic state transition in a single RPC transaction, apply_paid_transition: a product lock plus a uniqueness guard, and a detected collision doesn't auto-refund — it flags the order needs_review with a double_sale_detected event; the money stays put, a human decides. Concurrency is settled by the database, not the client.
[+][CH-02]
Seller onboarding ended with enabled status in Stripe while our payout_status hung on pending — the seller couldn't withdraw. The cause sat in a Connect nuance: a platform webhook doesn't receive account.updated events from connected accounts, and the new Stripe Dashboard no longer has a connect toggle — an endpoint with connect=true can only be created via the API, and it carries its own separate signing secret. It ended with two webhooks under the same URL and signature verification against both secrets in a loop until one matches. Plus the whole escrow machine: release transfers via source_transaction, a failed transfer rolls releasing→paid back, idempotency via processed_stripe_events, a buyer dispute halts auto-release, and a cron backstop forces resolution after 88 days.
[+][CH-03]
After a period of fast 'clicked-together' development I discovered that the key escrow objects — the funds-releasing RPC, the Stripe events table, the Connect columns — existed only in the live production database, not in versioned migrations: the database wasn't reproducible from the repo, and an agent reading the code 'couldn't find the escrow'. Regaining reproducibility took several passes, because the first backfill covered the main objects and an audit surfaced more unversioned ones (payment_events, payout columns, RLS functions). The end state: idempotent migrations (applying on the live database is a no-op), a 'live database vs migrations' audit and a written decision that exactly one canonical database exists. An infrastructure-as-code lesson I won't forget: a schema that isn't in the repo simply doesn't exist.
[+][CH-04]
This project is also a story of changing method. It began as a generative prototype in Lovable — fast, but with regressions, reverts and a drifting database — and ended in spec-first discipline: a planning package with phases F0–F6, a journal of twenty ADRs, a /loop whose definition of done includes code review and security review. I went from generating and clicking to measuring and closing — and that safety net immediately started catching real bugs: code review found, among others, a race in the search debounce (the alive flag set inside the timeout callback instead of the effect cleanup — an older request could overwrite newer results) and a pricing vulnerability where the client dictated the shipping cost; today the server computes the amount, calling the courier pricing itself, with a safe fallback that is never zero.
[FIG. 4] AI LAYER
No model runs in the product — search is Postgres full-text, not vector. AI built the platform though, in two epochs: first a generative prototype in Lovable, then spec-first discipline in Claude Code — a planning package with a MASTER-PROMPT, a /loop over a phased backlog, context measured from the statusline file instead of guessed, and every phase's definition of done including /code-review and /security-review. The project memory holds 29 notes with hard working rules distilled from real mistakes.
[FIG. 2A] THE LIVE SAVECHIP
CLICK A TILE — TAG HEURISTICS SUGGEST A BOARD
47 SAVES
NEXT.JS (FERMENT) vs TANSTACK START (OBJET)
| NEXT.JS | TANSTACK START | |
|---|---|---|
| ROUTING | App Router (konwencje) | TanStack Router |
| RENDER | RSC na Vercelu | Vite 7 + Cloudflare |
| STAN | server actions + DB | zustand/persist |
| DANE | tRPC + Payload | TanStack Query + Supabase |
→ hover a row — the consequence of the architectural choice
[FIG. 5] GALLERY





