Inspiration

A third of all food produced on Earth is wasted — 1.3 billion tonnes a year, around 8–10% of global greenhouse emissions, roughly $1 trillion in economic cost. At the same time 828 million people go to bed hungry.

What struck us is that this isn't a production problem. The food exists and the need exists, often within a few kilometres of each other. A restaurant closes with trays of untouched prepared food; a shelter three streets away is short for tomorrow. What's missing is a fast, low-friction way for the two to find each other before the food spoils. So we didn't try to solve hunger or food production — we tried to solve the coordination gap.

What it does

RescueChain is a food redistribution network with three roles and one shared lifecycle.

Donors (restaurants, supermarkets, caterers) list surplus food in under a minute — category, quantity, expiry deadline, pickup address and instructions.

NGOs browse live listings sorted by what expires soonest, filter by food type, and claim what their community needs with an optional note.

Admins verify NGOs before they're allowed to claim anything.

The lifecycle: a donor lists food → an NGO claims it → the donor approves or rejects with a reason → either side confirms pickup → the donation is marked complete and impact metrics update. Every step generates a notification for the other party. A rejected claim returns the food to the available pool so another NGO can take it.

There's also an explainable matching page: for any open donation, the platform ranks verified NGOs by category experience, completion reliability, and current spare capacity — and shows the reasoning behind every score in plain language rather than presenting a number to trust blindly.

How we built it

Frontend — Vite, React 19, TypeScript in strict mode, Tailwind v4 with a design-token system, Radix primitives for accessible components, TanStack Query for server state, React Hook Form + Zod for validated forms. 22 routes across three role-scoped dashboards, with the dashboards code-split so a first-time visitor only downloads the landing page. 234 kB gzipped across three cacheable chunks.

Backend — Supabase: Postgres, auth, and row-level security. Five enums, four tables, and 15 RLS policies. A few design decisions we're glad we made:

  • A Postgres trigger creates the profile row on signup and refuses to mint an admin from user-supplied metadata — the role is downgraded to donor. Admins can only be promoted deliberately.
  • A partial unique index guarantees only one live claim per donation, so two NGOs can't both be promised the same food.
  • Public landing-page statistics come from a SECURITY DEFINER function returning aggregates only, so anonymous visitors see real counts with zero row-level read access.

Deployment — Netlify with SPA fallback, security headers, and immutable asset caching.

Challenges we ran into

RLS policies that recursed into themselves. Our profile policies needed to check "is this user an admin?" — but reading profiles to answer that triggered the same policy again. We solved it with SECURITY DEFINER helper functions (is_admin(), is_verified_ngo()) that read the table outside the policy chain.

A privacy leak we nearly shipped. Our first policy said "any verified profile is readable by any signed-in user." That's convenient — counterparties need contact details to arrange pickup — but it also meant every user's email was readable by every other user. We rewrote it to scope reads to actual counterparties: verified NGOs are discoverable, and donor details are visible only to verified NGOs while a listing is live or a claim links the two parties.

A duplicate foreign key that broke the schema. We'd declared each FK twice — once inline via references, once as a named constraint — not realising Postgres auto-names the inline one identically. The migration failed with a name collision until we removed the redundant declarations.

Auth that silently did nothing. Signup appeared to work but no profiles appeared in the database. Email confirmation was on, so accounts were created but never issued a session. We only caught it because we compared true row counts against what the app could see.

Marketing copy that outran the code. Covered below — it was the most uncomfortable problem we hit, and the most valuable.

Accomplishments that we're proud of

We verified our security instead of assuming it. We proved RLS empirically: a donor profile exists in the database, and an anonymous request for profiles returns an empty array. A row exists that the public cannot see. That's enforcement demonstrated, not claimed.

We made the matching explainable. It would have been easier to output a score and call it AI. Instead every point maps to a stated reason — "collected 3 produce donations before", "no open pickups, full capacity available" — so a donor can disagree with the ranking. We think that's the right default for software that mediates between organisations.

We audited our own claims and cut the ones that were false. Our landing page advertised live pickup tracking, route optimisation, and driver status. None of it existed. We removed all of it and replaced it with capabilities we could actually demonstrate.

The whole loop genuinely works — signup, verification gating, listing, claiming, approval, rejection with reason, pickup confirmation, notifications, and impact metrics, on real infrastructure with real auth.

What we learned

The gap between a demo and a product is mostly honesty. Late on, we read our own feature section — "Live Map Integration: live tracking of NGO pickups and optimized routing data" — and checked it against the code. There was no geocoding, no tracking, no routing. The map positions were a hash of the address string. "Pickup distance" in our matching algorithm was text similarity between two address strings, not distance.

Nothing was broken. It just wasn't true. We rewrote every claim to match what the code does and added a scope section to the README stating plainly what the platform does and doesn't do. The feature list got shorter and the project got stronger — the matching page is genuinely good, and it reads better next to honest neighbours than next to fictional ones.

Coordination software inherits responsibility it didn't ask for. We built a system that moves food between strangers and initially treated safety as someone else's problem. It records a donor-declared expiry and hides lapsed listings — but it captures no allergens, no temperature, no preparation time, and it vets recipients while auto-approving donors. We documented this honestly rather than papering over it, but it reframed how we think about the product.

Test the absence of data, not just its presence. Empty query results looked like working security. They were actually an empty database from a broken signup. Two very different problems that produce identical output.

What's next for RescueChain

Immediate — correctness and safety:

  • An expiry sweeper. Expired listings currently disappear from browse but stay available forever, and a known bug lets an NGO confirm pickup of food that lapsed after being claimed — recording it as meals saved.
  • Allergen declarations and a required preparation timestamp for prepared food and dairy. This is the most serious gap for real-world use.
  • A safety attestation on the donation form, and a disputed claim outcome so bad handoffs are recorded rather than silently completed.
  • Donor verification mirroring the NGO flow. Vetting recipients while trusting suppliers is backwards for a food platform.

Next — making the geography real:

  • Geocode pickup addresses so distance is actual distance. This upgrades matching from string similarity to real proximity and turns the schematic diagram into a working map.
  • Realtime subscriptions so claims appear without a refresh.

Later:

  • Donor reputation from completion history and dispute rate.
  • Recurring donations for predictable surplus, so NGOs can plan.
  • Multi-city support with regional admins.

Built With

Share this project:

Updates