Inspiration
Approximately 47.9 million Americans, which is 1 in 7 households, experience food insecurity. This is a serious problem. 30-40% of the food supply in America gets tossed every year, which could be going to someone in need. This shows that the reason for food insecurity isn't supply: it's coordination.
What it does
Scraps is a live food-rescue board for Baltimore. Restaurants and grocers post surplus food they are about to toss; users see it on a map and claim pickups. Every other volunteer's screen updates instantly — no refresh, no polling.
When two volunteers tap Claim on the same pickup in the same instant, exactly one wins. The other is told who beat them.
Restaurants can set up a shopfront once — name, a line about who they are, an address — and every pickup they post starts from it. They can photograph the food, and a volunteer sees the picture before deciding whether to drive. A volunteer who doesn't know what they want can just ask the built-in chatbot something like: "what's near me that's still warm?" and the database answers by reading the live board.
Unclaimed listings whose pickup window passes delete themselves, on a timer inside the database, with no client involved.
How we built it
SpacetimeDB 2.10.1, which isn't a database you put behind a server — it's a database you put your server inside. Our application logic compiles to WebAssembly and runs in the database; clients subscribe to queries and the database pushes changes.
- Backend: one Rust module. Seven tables, fifteen reducers, three procedures, one server-side view.
- Frontend: React + TypeScript, Leaflet + OpenStreetMap.
- Data layer:
useTable, and nothing else. There is no fetching code anywhere in the client — not onefetch, not one poll.
We used five SpacetimeDB features that most projects won't touch:
| Feature | What we do with it |
|---|---|
| Serialized reducers | The contested claim resolves with no locking code |
| Scheduled tables | Unclaimed listings expire on a 30-second timer |
| Event tables | Claims broadcast to every client without being stored |
| Server-side views | "My pickups" is computed in the database, not in React |
| Procedures | The database itself makes outbound HTTP calls |
Plus a claim ceiling(three open pickups per user)counted inside the transaction, so it can't be raced any more than the claim itself can.
The procedures are the part we'd point at first. A reducer is deliberately deterministic: no clock, no filesystem, no network. Procedures are the sanctioned way out of that, and we used them for three things: 1) a Grok assistant that reads the live board and answers questions like "where's the nearest bagel pickup?", 2) a drafter that turns a donor's three words into a listing description, and 3) an address geocoder.
Challenges we ran into
The transaction boundary ate a feature. We added an event table to broadcast
every claim attempt, won and lost, so contention would be visible to everyone
rather than only to the loser. We wrote a comment predicting the losing write
might not survive, then tested it against the live database: exactly one row
arrived, won: true.
Our own scheduled reducer ate the demo board overnight. We came back to zero listings. Every unclaimed one had aged past its pickup window and
been deleted exactly as designed. This was the correct behaviour, but it was genuinely alarming since we hadn't accounted for this. We added a reset_board reducer and button to repopulate the listings for the demo.
Accomplishments we're proud of
We wrote no locking code. No SELECT … FOR UPDATE, no compare-and-swap, no
retry loop, no version column. The contested claim is a four-line check-then-set
that is correct because reducers are serialized transactions.
We proved it rather than asserting it. Fifty concurrent claims fired at a single listing from one browser:
50 fired · 1 succeeded · 49 rejected · 85ms
One write survived. No locks, no retries, no double-claim. It's a button in the app, so a judge can press it themselves rather than take our word for it.
What we learned
Neither of us had written Rust before this project. It turned out not to matter much: a SpacetimeDB
module is table structs and small mutation functions — no async, no lifetimes,
no Arc<Mutex<>>. About 400 lines total, and the hard parts of Rust never
appeared.
The bigger lesson was about where correctness lives. In a normal stack, "two people can't claim the same thing" is something you remember to implement. Here it's a property of the execution model, and the work is understanding the model rather than defending against it.
Built With
- grok
- leaflet.js
- openstreetmap
- react
- rust
- spacetimedb
- typescript
- vite
- xai
Log in or sign up for Devpost to join the conversation.