[12]
BAZARTY.COM (PAYLOAD + MONGODB)
▓ TIER B · YEAR 2026 · STATUS: LIVE · LANGUAGES: TYPESCRIPT
Extending an inherited codebase: multi-tenant, Stripe Connect, escrow
[FIG. 1] MISSION
I can step into an inherited, mature codebase and extend it without breaking its conventions — and that happens at work far more often than greenfield. bazarty.com exists in two architectures — this is the extended variant on Payload CMS + MongoDB (repo: marketplace-maci); the Supabase variant is described separately. I started this one from someone else's mature base (Antonio's multi-tenant + Stripe Connect tutorial). First I reverse-engineered the base into .planning/codebase/* (architecture, conventions, integrations) and sealed its patterns into custom Claude skills that generate code matching the house rules (payload-collection for Payload collections, trpc-router for tRPC routers). Only then did I add real commerce: websocket buyer↔seller chat, a full order lifecycle (confirm / dispute / review), transactional emails, reviews, a purchase library and ~25 marketing/legal pages. I run the architecture as multi-tenant with tenant data isolation: every vendor gets a shop on its own subdomain, and split payments go through Stripe Connect, which takes the platform fee automatically. Stack: Next.js 15 + React 19, Payload CMS + MongoDB, type-safe tRPC 11. I wired AI code review into CI so quality doesn't depend on how disciplined I feel that day. The platform's heart is trust between strangers: my own escrow mechanism holds the seller's payout until delivery confirmation (auto-accept after 48 h, payout release 2 days later via cron), and a complaint refund is always covered. Scale: 19 Payload collections, 17 tRPC routers, ~41 thousand lines of TypeScript and over 900 commits — the vast majority my own extension of the fork.
[FIG. 2] ARCHITECTURE
hover a block to see its description
[FIG. 3] CHALLENGES
[+][CH-01]
Extending someone else's mature base is a different discipline from writing greenfield: any change of mine could silently break a convention I didn't know existed. Before adding a single line I reverse-engineered the base into .planning/codebase/* — ARCHITECTURE, CONVENTIONS, INTEGRATIONS — and sealed the patterns (a Payload collection, a tRPC router) into custom Claude skills that generate new code already in the house style. Conventions stopped being tribal knowledge — they became enforceable.
[+][CH-02]
For handmade goods, stock is often exactly 1 — and two buyers clicking 'buy' at the same moment can't both get it. The Payload local API does read-modify-write (find → check stock → update), a classic TOCTOU race: both sessions read stock: 1, both pass validation, both decrement. Instead of fighting the framework, in this one spot I deliberately dropped below the ORM to the raw Mongo driver: a conditional atomic decrement where the $gte condition and $inc execute as one indivisible operation — modifiedCount === 1 means 'reserved', zero overselling. The reservation hooks in before the Stripe session is created, and a rollback restores stock when the session expires. It's the only place in the whole app that bypasses Payload — and the code explains why.
[+][CH-03]
I built the escrow twice in one day. The first version used Stripe's delay_days — a payout hold counted from payment. It worked, but protected the wrong thing: the window runs from payment, not actual delivery, so with slow shipping the funds could reach the seller before the buyer ever saw the parcel. I threw it out after a few hours and rebuilt the model: seller accounts switch to payouts=manual, and my own sweeper releases the payout — exactly 2 days after the buyer confirms delivery, with an idempotency guard on stripePayoutId and the amount computed as a mirror of the checkout math so it never exceeds the balance by a cent. The readiness to discard working code when the risk model is wrong — and to record why — is this project's most valuable lesson.
[+][CH-04]
Integrating Furgonetka (a courier aggregator) taught me humility before under-documented APIs: package creation broke on the address format (street as one field vs split street + building_number) and the API version, and the user-facing symptom was '0 delivery options' at checkout. Two successive fixes the same day made things worse — both broke package creation — so the third commit was a revert to the known-working version from main, with the reasoning spelled out in the message. Only then came the real fix: migrating to the calculate-price endpoint, hard parcel-dimension limits and — most importantly — replacing the silent zero with a message saying exactly what's missing (e.g. a two-part recipient name, minimum dimensions). The 'revert to known-good instead of stacking fixes' discipline stayed with me for good.
[FIG. 4] AI LAYER
AI doesn't sit in the product but in the process of building it — systematically: custom Claude skills (payload-collection, trpc-router) generate code that follows the inherited base's conventions, a debugger agent helps with diagnosis, and every PR passes AI code review in CI (claude-code-review.yml). It's an answer to how you scale discipline when working with inherited code.
[FIG. 2A] THE CI REVIEWER
.github/workflows/claude-code-review.yml · on: pull_request
▸ commit a3f9e21feat(chat): typing indicator
[◎] WAITING FOR A PR
- ⚠ modules/chat/server: tRPC procedure without .input() — add a zod schema
- ⚠ use-chat-socket: missing useEffect cleanup — socket leak on unmount
AN AI-NATIVE PROCESS, NOT AN AI PRODUCT: EVERY PR IS REVIEWED BY CLAUDE IN CI
[FIG. 5] GALLERY

