Inspiration
Maria has two kids, no car, an eviction notice, and nowhere with a stove. She doesn't need a directory — she can already find twenty phone numbers. What she can't find out is which of those places will turn her away at the door, and which one to try first.
Getting help today fails in a specific way: you travel across the city to a food bank, and only then learn you needed a photo ID you lost when you left the apartment. Meanwhile a neighbour three blocks away wants to help and has no idea what's actually useful. And the organization in the middle has no reliable count of what it holds, because a volunteer saying "I donated this" and a recipient saying "I got it" are not the same as it arriving.
We wanted the moment of asking for help to produce a plan, not a list — and to do it without Maria having to hand her name and address to strangers.
What it does
A person describes what's happening in ordinary language. Bridge returns an ordered plan that already knows what each organization requires at the door: blocked options are labelled with what's blocking them, and the step that unblocks them is placed in front — "ask the shelter front desk for a signed letter," not "proof of address required."
At the same time it publishes an anonymized one-line need to the community commons. A volunteer sees "Needs groceries. Household of 3; no car, no stove access." and nothing else — no name, no address, never more than a three-digit ZIP area. Bridge will never ask where someone lives; that question is blocked deterministically, even when the model tries to ask it.
Organizations are the source of truth. A volunteer offer is not a receipt; a claim is not a distribution. Only a bound organization operator confirms what physically arrived, and matching only ever consumes verified, organization-held supply.
Matching then happens on its own. Nobody presses a button — whichever half of the match arrives second wakes the matcher. And it matches on usability, not category: dry rice and beans is refused for a household with no stove, while ready-to-eat delivered meals score 95/100.
How we built it
Entirely in Jac, as one persistent graph — 30 .jac files, ~9,700 lines.
The graph is the product: Person, Org, Requirement, Need, Contribution, OrgTally, PlanStep, CaseStrategy as nodes, with typed edges (HasNeed, BroadcastTo, RoutedThrough, Matched, KeepsTally) carrying the relationships. Sixteen walkers perform every state transition by traversing it.
Nine by llm() roles do the reasoning, deliberately specialized rather than one chat prompt: situation reading, an adversarial privacy audit, eligibility judgement per gate, match judgement, escalation strategy, next-step narration, volunteer-offer extraction, plus a ReAct caseworker in core/agent.jac with four graph-reading tools (survey_orgs, inspect_gate, check_shelf, commons_pressure).
The frontend is Jac too — cl client components with sv import calling server walkers directly, so there's no hand-written API layer between the UI and the graph.
Challenges we ran into
The volunteer side was silently blind. CommunityBoard returned zero needs while three sat open on the commons. We dumped the live SQLite anchor store to find it: grant(need, WritePerm) grants the node, but the root.shared --> Need edge is created by whoever runs the walker, so it belongs to the seeker's private root. A traversal from root.shared in anyone else's session never crosses it. The Need was perfectly readable and completely unreachable — and it looked exactly like "no volunteers yet." Granting the edge fixed it.
flow in a comprehension captures the loop variable by reference. Every eligibility gate on an organization got the last gate's verdict — silently turning blocked orgs into "you qualify." Fixed with launch_* wrapper functions that give each future its own binding.
Adding one line of production config broke everything. Any [scale.*] key in jac.toml flips jac start into microservice mode; because the client sv imports three core modules, scale split them into separate services with separate stores, and every call died on Shared root unavailable. Bridge must run single-process.
GPS isn't a ZIP. The browser hands back 37.7749, -122.4194; the matcher keys on a three-digit prefix. We resolve coordinates offline against the seeded service areas rather than shipping anyone's location to a geocoding service.
Accomplishments that we're proud of
The privacy guarantee is topological, not a policy. There is no edge type in the graph whose source is a Donor or Contribution and which reaches a Person. A volunteer's session literally cannot traverse to a human being — from a Need the only way out is BroadcastTo → Org, and from an Org the only way to a pledge is back down RoutedThrough. That's enforced by the shape of the data, not by a permission check someone could forget.
Nobody presses a match button. Two walkers in two different user sessions coordinate purely by reading and writing the same graph. Maria's plan rewrites itself because of something Sam did, and neither walker ever called the other.
We don't trust the model where it shouldn't be trusted. question_guard.jac deterministically drops any follow-up question that solicits an address, ZIP, or name — both example questions in its docstring were produced by a real model with the rule already in the prompt. Unit allocations get clamped. Leak claims get verified. The model judges; it never gets to be arithmetically wrong about its own output.
It fails loudly. No keyword fallback anywhere. If the model is unavailable, the app says so instead of quietly degrading into a directory.
What we learned
Object-spatial programming changes where the invariants live. "A donor can never see a recipient" stopped being a code review question and became a property of which edge types exist. That's a much stronger guarantee, and we only got it by modelling the domain as a graph first.
We also learned that graph permissions have two halves — nodes and the edges reaching them — and that getting only one right fails invisibly rather than throwing.
On byLLM: structured outputs plus sem annotations meant we never wrote a parser or a retry loop. But specialized roles can contradict each other, and the fix isn't a better prompt — it's a deterministic check on the output.
What's next for Bridge
Real organization onboarding, so operators confirm receipts from their own devices rather than a seeded roster. Expanding beyond food, shelter, legal, and jobs into clothing, hygiene, transport, and childcare, which the ledger model already supports.
Built With
- css
- jac
Log in or sign up for Devpost to join the conversation.