P R O J E C T SC O N T A C T

[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

bazarty.com (Payload + MongoDB) — fig 5
[FIG. 5] 01/02
bazarty.com (Payload + MongoDB) — fig 6
[FIG. 6] 02/02

[FIG. 6] STACK & LINKS

NEXT.JS 15REACT 19PAYLOAD CMSMONGODBTRPC 11TANSTACK QUERYSTRIPE CONNECTSHADCN/RADIXTAILWIND V4VERCEL BLOB
CAPABILITIES IN THIS PROJECT: NEXT.JS · PAYLOAD CMS · STRIPE · TRPC · MONGODB