Glitz: the anatomy
A three-sided commerce marketplace. Brands list products, creators mint tracking links, buyers purchase through them. Commissions are attributed automatically, held thirty days, and paid out, with teams of creators splitting the commission by rule.
Live on iOS, Android and web. Built for a client; live payments and live Shopify confirmed May 2026.
Stack Flutter, Firebase, Cloud Functions, Next.js, Stripe Connect, Shopify
- 787
- commits across mobile, backend and web
- 83k
- lines of Dart, 94 screens
- 59
- Cloud Functions
- 89
- web pages against the same database
- 58
- mobile builds shipped
- 34
- Maestro flows, screenshot helpers included
The signal path reads top to bottom. Every stage is a decision that was made in the code, with the problem it answered and what it cost.
01 Catalogue: A brand connects a store. Products import and stay in sync.
The problem
A brand will not maintain a second catalogue by hand. If the store and the marketplace drift, the link a creator shared last week sells a product that no longer exists at that price.
The decision
Shopify OAuth, import, sync and reconcile, with HMAC-verified webhooks for changes and draft orders on the way back. The brand does nothing after connecting.
What it cost
A whole integration layer for one storefront platform: 3,493 lines in one directory, 11 of the 59 functions. It is a seam by folder, not by interface. The storefront's name is in the collection names and the security rules, so a second storefront would have to earn that interface first.
02 Link: A creator accepts a collaboration and mints a tracking link.
The problem
The collaboration document is writable by the client, because the app needs to update it. The fairness feature (a seven-day grace at the old rate when a brand cuts commission) and the exploit (a creator setting their own rate to 99%) live in the same field.
The decision
Cap the effective rate at the brand's product commission on the server, every time it is read for money. The client can write what it likes; the number that pays out is not the client's.
What it cost
The grace window is now bounded by the cap too, and the comment on the cap says exactly what that costs the fairness feature so nobody widens it without knowing.
03 Attribution: A buyer taps the link, often inside another app's browser.
The problem
Instagram's in-app browser, then the App Store, then a fresh install. The referrer does not survive that trip, and an unattributed sale is a creator who did the work and got nothing.
The decision
A 48-hour bridge: the tap is recorded server-side by IP with the user agent alongside, the app asks once on first launch whether anyone at its address tapped a link, and the newest unclaimed match wins. The recording endpoint is rate-limited and only accepts links that exist, so the pool cannot be flooded or poisoned.
What it cost
It is probabilistic. Two people behind one address installing within the window would be matched to the most recent tap, and the window and the rate limit are the whole defence. The note says so rather than pretending otherwise.
04 Charge: The buyer pays. A commission is minted against the link.
The problem
A colluding brand and creator could round-trip a purchase of product X and mint a commission against a link for product Y. And the first version checked for an existing commission and then wrote one, so two calls for the same sale a few milliseconds apart would both pass the check and mint twice.
The decision
Every commission must prove the payment bought this product. Commission documents get deterministic ids, keyed on the transaction rather than the payment intent because a multi-item cart shares one intent, so a duplicate create throws instead of paying.
What it cost
Exactly-once assembled from a document store's create-or-fail, with the mint called from the checkout rather than a webhook, so a failed mint never blocks a buyer and can leave a creator's entry missing. It is the same shape as the payments platform's webhook fix, solved the same way, which is the point of showing both.
05 Hold: Money sits on the platform for thirty days before transfer.
The problem
Splitting at charge time is simpler and wrong: a refund after the creator has been paid is a clawback nobody enjoys. Holding creates its own problems: float, stuck balances, transfers that fail.
The decision
Take the full charge, transfer after the hold. Around that: a retry ladder from one hour to sixteen over five attempts, then a platform alert; a balance monitor; and a reconciler for payouts that got stuck between states.
What it cost
A great deal more machinery than a split at charge. Every piece of it exists because a real case needed it.
06 Team: A creator's team splits the commission between leader and members.
The problem
Three ways to cheat it: mint a solo link to dodge the leader's cut, send a tampered split from the client, or carry a split over 100% so the leader's share goes negative.
The decision
A member's link is routed through their team automatically, earliest membership winning. The split is read from the team's own membership record, never the client. The split is clamped to 0..100.
What it cost
Creators who genuinely want a solo deal alongside a team cannot have one. That trade was made on purpose and is written down.
// Validate team member is on the team, and derive the split from the
// authoritative team_members doc — never trust the client's teamSplitRate.
if (teamMemberId && teamLeaderId) {
const memberCheck = await db
.collection("team_members")
.where("leaderId", "==", teamLeaderId)
.where("memberId", "==", teamMemberId)
.where("status", "==", "active")
.limit(1)
.get();
if (memberCheck.empty) {
throw new HttpsError("invalid-argument", …);
}
const rawSplit = memberCheck.docs[0].data().splitRate ?? 50;
resolvedTeamSplitRate = Math.max(0, Math.min(100, rawSplit));
}
07 Payout: Held money transfers to creators through connected accounts.
The problem
Payout minimums are per brand, not per creator. Grouping by creator alone leaves money stranded under the minimum forever, and the claim on it has already been taken.
The decision
Group by creator and brand, so each brand's minimum gates only its own money; groups under the minimum are skipped before anything is claimed and keep accumulating. Each entry is claimed in a transaction before its transfer, transfers carry deterministic idempotency keys, and marking paid is a separate step so a bookkeeping failure cannot be mistaken for a transfer failure.
What it cost
Found by auditing a money path that was already live, not by design up front. Two of the seven findings in that pass were open ways to lose money. Amounts under the processor's transfer floor still just sit, and the code says so.
08 Rules: Seven hundred lines of security rules, treated as code.
The problem
Parent rules do not cascade to subcollections. Without repeating the participant check, one collection-group query would return every private message on the platform.
The decision
Privilege-escalation defence on role fields, a guest tier excluded from every write surface, immutable financial records, and the participant check repeated where the platform makes it easy to forget. Rules tested like code.
What it cost
Repetition, on purpose, in the one file where a clever abstraction is more dangerous than a copy.