Offers and counter-offers
Turn-taking is a column, not a convention. waiting_on
decides whose move it is, so neither side can counter twice in a row or
accept a stale number.
Listings are easy. The hard part starts when someone makes an offer. Marketwright gives you the transaction layer: offers, counter-offers with turn-taking enforced in the database, acquisitions, a private deal room gated on deal state, and mutual completion.
Search the market and you will find a dozen excellent SaaS boilerplates. Auth, billing, a dashboard, a settings page. Not one of them has a negotiation.
So you ship a listings grid in a weekend, and then spend three months on the part nobody wrote for you: what happens when a buyer offers less than the asking price, the seller counters, and both of them need to see the same numbers, in the right order, without either one editing the deal from underneath the other.
That is the part this kit is. It suits any marketplace where a deal is negotiated rather than checked out: business and asset brokerage, equipment, domains, high-value resale.
Each of these is enforced in Postgres, not just in the UI, because a marketplace where the rules live only in React is a marketplace with a REST API that ignores them.
Turn-taking is a column, not a convention. waiting_on
decides whose move it is, so neither side can counter twice in a row or
accept a stale number.
An accepted offer creates a deal and moves the listing to
under_offer. Leaving that state while a deal is live is
refused, which is what stops two concurrent acquisitions on one listing.
Confidential documents, readable only through an active acquisition. An uninvolved buyer gets nothing, and the check is a storage policy, not a hidden button.
Both parties confirm the transfer happened. On the second confirmation ownership is recorded and the listing freezes, permanently.
Reports reach a queue with the listing and reporter named. Removal is one transaction, and the seller cannot quietly re-publish what a moderator took down.
A real account-deletion state machine with a cancellation window, and version-tracked consent. The parts nobody writes speculatively.
A live copy, seeded with a full marketplace: six listings, an open negotiation, an active deal with real documents in the data room, a completed sale, and a report waiting in the moderation queue.
Sign in as any of the accounts below. It resets every night, and the handful of actions that would break it for the next visitor are switched off — everything else works.
maya@example.com · password123 — a seller with listingspriya@example.com · password123 — a buyer mid-negotiation
npm run verify:flows runs a behavioural suite against a live
local stack. It signs in as ordinary users and calls the same RPCs the app
calls. It is not a unit-test suite, it exercises boundaries: an uninvolved
buyer cannot read a data room, an anonymous visitor cannot create an offer,
a sold listing cannot be re-published.
Every fix in this repository has an assertion that fails without it. Several of those assertions exist because the test was wrong first and passed for the wrong reason.
The security model has one sentence at its centre:
row-level security picks the row, GRANT picks the column.
Postgres RLS cannot restrict columns, so column-scoped grants are what stop
a user writing their own role or a seller re-publishing a sold
listing. They all live in 00012_privileges.sql, on purpose, and
npm run verify:schema proves the live schema still matches a
committed fingerprint.
The migration comments document real vulnerabilities that were found and closed, with the reasoning intact. You are buying the arguments as well as the code.
You are about to spend real money on code you have not read. Here is the part most landing pages leave out.
One payment, one developer, perpetual licence. Use it on unlimited projects of your own, commercial ones included.
One payment. No subscription, no seat count, no expiry. The price returns to $250 when launch week ends.
Get Marketwright — $169