Skip to content
All work

02 · Nonprofit marketplace, payments

StuffRescue: Nonprofit item sharing where no client input can move money

Client
Nonprofit organization
Industry
Nonprofit marketplace, payments
When
2026, ongoing
My part
Sole builder, end to end
Status
Live

Overview

StuffRescue is a nonprofit item-sharing platform. Members give away and claim free household items in their local area: nothing is sold, nothing is bid on, and the goal is to keep usable goods out of the waste stream. Items are free to claim within 3 miles. Beyond that radius a $0.99 rescue fee applies, paid from a prepaid Stripe-backed wallet.

That small fee shaped the whole design. The moment real money enters a product, the question is not whether the happy path works but whether any client input can move money. Here none can. The wallet load amount is defined on the server. The Stripe webhook is the only path that credits a wallet, and a database constraint makes redeliveries no-ops. Debits happen in one atomic SQL statement whose WHERE clause is the balance check. The home location that determines the fee is immutable once set.

I built all of it alone: front end, API, database, payments, email, SEO engine, admin console and deployment. The project began as a migration off Base44, a low-code platform, onto a fully self-owned stack that I designed and executed with a written migration spec. It started in August 2026 and is ongoing, about 100 commits in.

What I built

  • 01Wallet loads are $20 Stripe Checkout sessions with the amount defined on the server, so the client cannot choose it. The signature-verified webhook is the only credit path, a UNIQUE constraint inside the wallet_credit SQL function makes redeliveries idempotent no-ops, and the success redirect never credits anything.
  • 02Claims debit the wallet in one atomic SQL statement where the WHERE clause itself is the balance-sufficiency check. A lost claim race refunds automatically, and the fee quote is shown before claiming, never first on a confirmation screen.
  • 03Home location is immutable once set, enforced by a database trigger plus an API column allowlist, so the radius fee cannot be gamed by moving the pin.
  • 04Custom two-step login: password, then an emailed one-time code, with trusted-device tokens. The API verifies Supabase JWTs with jose against JWKS, with stale-while-error key caching added after I diagnosed a live outage where Supabase Auth was unreachable from the VPS.
  • 05A 26-page React 18 consumer app, installable as a PWA: Tailwind with a brand token system, 40+ shadcn/ui components on Radix, TanStack Query, React Hook Form with Zod, Leaflet maps and an in-app diagnostics page. People post items from a desktop but the photos are on their phone, so a QR-code hand-off moves the upload to the phone through a token-scoped session. Even the "How it works" explainer video is code, React components rendered with Remotion.
  • 06Gemini 3.5 Flash Lite for bulk item uploads with automatic description generation, price estimation and category selection, and for AI-based search filtering in the catalog.
  • 07Item-scoped real-time chat with an emoji picker, location-sharing bubbles, read-state sync and order-status transitions, streamed over Server-Sent Events with a Redis event bus.
  • 08A separate Next.js 15 admin console, allowlist-gated with passwordless email-OTP sign-in, and a static blog engine that turns Markdown into zero-JavaScript pages, sitemap and RSS. GPTBot, PerplexityBot, ClaudeBot and link-preview bots do not render JavaScript, so the posts are plain HTML they can read, while the app catalog stays non-indexable.

Architecture notes

Four cooperating applications share one Supabase project: the consumer SPA on Vercel, a Node.js and Express API on a VPS under PM2, the Next.js admin console, and a blog build step that ships static pages into the same dist as the SPA. The API runs Node 22 with native TypeScript stripping, so there is no build step to break.

Authorization is defense in depth. The consumer app runs against RLS-protected Postgres. The API holds the service key, which bypasses RLS, so every guarantee is re-implemented in route code: JWT-scoped queries, column allowlists, owner checks. That is written down as a hard rule for every new route. The admin console is allowlist-gated on the server rather than role-column-gated, deliberately, so no application bug can escalate a normal account to admin.

The database layer is 20 hand-written SQL migrations covering schema, RLS policies, triggers such as lock_home_location and protect_profile_role, storage policies and the atomic wallet functions. The browser bundle contains exactly two public values, the Supabase URL and the anon key, and the build fails loudly if either is missing.

What was hard

Problem

Stripe retries webhooks, and a duplicate delivery must never credit a wallet twice.

Approach

The webhook is the only credit path, signature verified, and a UNIQUE constraint inside the wallet_credit SQL function turns redeliveries into no-ops. The success redirect credits nothing, so refreshing that page cannot mint money either.

Problem

A claim both debits a wallet and takes an item, and two people can race for the same item.

Approach

The debit is one atomic SQL statement whose WHERE clause is the balance-sufficiency check, so there is no gap between checking and spending. Whoever loses the claim race is refunded automatically.

Problem

The rescue fee depends on distance from home, so a movable home pin is free money.

Approach

Home location is immutable once set, enforced twice: a lock_home_location trigger in Postgres and a column allowlist in the API, so an application bug cannot quietly rewrite it.

Problem

Chat needs authenticated streams, and EventSource cannot send an Authorization header.

Approach

A custom SSE client built on fetch and ReadableStream instead of EventSource, with per-reconnect token refresh, exponential backoff and abort-safe teardown so a dropped connection never leaves a ghost stream. Events fan out through a Redis event bus behind the API.

Outcome

StuffRescue is live at stuffrescue.org. The work is ongoing, with the remaining Deno edge functions being folded into the Express API.

The money path is tested end to end: the Stripe CLI driving webhooks locally, Vitest and Testing Library on the front end, Node's built-in test runner on the API and admin, and a live suite that asserts the RLS policies against the real Supabase project. The nonprofit went from renting a low-code platform to owning its stack at every layer.