Skip to content

Repository files navigation

Backpack

One live board of everything the school year asks of your family — with the sentence from the school's own notice that says so.

🔗 resilient-mastiff-559.convex.site · try it without signing up · built for the Convex All Gas Hackathon


The problem

A permission slip comes home in a backpack. A newsletter says the trip money is due Friday. A teacher emails about a costume. The calendar page says there is no school on the 17th. None of it is hard — it is just scattered across a website nobody opens twice, three teachers' emails and a piece of paper in a bag, and the cost of missing one is a child who is the only one without a packed lunch.

Backpack collects it into one list. Point it at the school's website and forward it the school's mail, and it keeps a single shared board of what the family actually has to do — forms to sign, money to send, days off, things to bring — that both parents watch change at the same time.

How it works

school website  ──Firecrawl crawl──┐
                                   ├──► OpenAI reads it ──► the board (live)
forwarded mail  ──AgentMail inbox──┘         │
                                             └──► verbatim quote, checked

Three things make it trustworthy enough to put on a shared board.

Every line quotes its source, and the quote is verified. The model answers against a strict JSON schema and must carry a sentence copied from the source. That claim is then checked against the actual text — an item whose quote is not found is discarded, not flagged. The model cannot put something on a family's board that the source does not say. Pointed at a school district's institutional pages, it correctly returns nothing.

Nothing appears twice. Sources are keyed by the SHA-256 of their normalised text, so a page re-crawled unchanged never reaches the model. Obligations are keyed by a fingerprint built from meaning — kind, date, normalised title — so the same deadline announced on the site and again in a reminder email collapses onto one card.

Settled work stays settled. A finished or dismissed item is never reopened by a later sighting, and one parent's claim survives a revision.

What each sponsor does

Convex The whole backend. The board is one live query, the crawl progress another, and presence shows the second parent on the same board. Auth, crons, HTTP actions, the scheduler, pagination, static hosting.
Firecrawl A durable crawl of the school site. startCrawl returns immediately, pages stream into the component's tables, and the board subscribes to the progress rather than polling it. A weekly cron re-reads every site.
OpenAI Reads each page and message into structured obligations under the grounding rule above, and drafts the question to the school office.
AgentMail The household's own address. Parents forward school mail to it; a reply on a thread we opened closes the question that opened it, and anything else becomes a new source.

Architecture

convex/
  convex.config.ts   nine component mounts, typed environment, no raw process.env
  schema.ts          fourteen tables on one tenant boundary
  lib/               outward adapters — openai/ firecrawl agentmail limits pools
  model/             domain logic — households obligations sources crawls budget
  pipelines/         crawlIngest · mailIngest · extract · recrawl
  *.ts               public API: thin, validated args and returns, auth first
src/                 the board

A household is the unit of privacy. Every public function passes through requireMembership, so that rule is enforced in one place rather than re-implemented per endpoint.

Two components earn their place by bounding spend rather than adding features. A finished crawl can land a hundred pages in one callback and each page is one model call, so extraction runs through a workpool with a fixed number in flight. And because crawling spends Firecrawl credits and extraction spends OpenAI budget on the operator's own key, both are rate limited per household and metered against a hard ceiling — every call reports its token usage, and both entry points refuse before spending past OPENAI_BUDGET_CENTS. The running total is on screen, because a number you have to find in a dashboard is one you find too late.

A per-household limit bounds one family and nothing else, though, and /demo hands a household to anyone who asks — so a loop over that URL would collect a fresh allowance every time. Two further buckets are therefore unkeyed, and bound the deployment: one on handing out a trial board, one on starting a crawl at all.

Running it

npm install
npx convex dev          # provisions a deployment and pushes the backend
npm run dev             # the frontend, at localhost:5173

Then set the four service keys on the deployment:

npx convex env set FIRECRAWL_API_KEY fc-...
npx convex env set AGENTMAIL_API_KEY ...
npx convex env set OPENAI_API_KEY sk-...
npx convex env set AGENTMAIL_WEBHOOK_SECRET whsec-...   # for inbound mail

Convex Auth needs a signing key pair. Its own CLI cannot write them on Windows (it shells out without shell: true, so it cannot launch npx.cmd), so:

node scripts/generate-auth-keys.mjs      # prints the two commands to run

Deploying — the frontend is served by Convex static hosting, so there is one command and no second host:

npm run deploy

Use that rather than building and uploading by hand. npm run build bakes VITE_CONVEX_URL from .env.local, which is the development deployment, so a hand-built bundle uploaded to production serves a site whose client talks to dev. The script lets the static-hosting CLI run the build, which injects the right URL.

Register https://<deployment>.convex.site/api/agentmail/webhook with AgentMail for inbound mail. Firecrawl's callback is mounted by its component and needs no setup.

A worked example board — openly fictional, and labelled as such — can be seeded into a household with:

npx convex run seed:demo '{"householdId":"<id>"}'

Notes and limits

  • Households share one AgentMail inbox. Each gets a sub-address of it, <inbox>+<householdId>@, and inbound mail is routed on the address it was sent to. AgentMail delivers sub-addressed mail with the tag intact — verified against the live API, since it is not in their documentation — so the free tier's three-inbox limit caps inboxes, not tenants. It is the better shape regardless of the meter: a household is a tenant, not a mailbox, and the deployment needs exactly one mailbox forever.
  • @agentmail/convex@0.1.0 is not used, deliberately. It reads its API key from process.env inside its own component isolate — where deployment variables are not visible — and declares no typed environment to pass one in, so every call fails with "AGENTMAIL_API_KEY is not set" while the key is plainly set. This is a known defect: agentmail-to/convex#6 fixes it the same way, and was open and unmerged at the time of writing, so npm still ships the broken build. AgentMail is reached over its REST API instead, with Svix webhook verification implemented in Web Crypto.
  • The openai package is not used either: it sets url.username while normalising a request, which the Convex runtime does not implement. The Responses API is called directly.
  • Extraction defaults to gpt-5.6-luna and to fifteen pages a crawl. Each page is one request, and a new OpenAI account is capped at fifty requests a day long before it runs out of money — a fifteen-page crawl costs about one cent and leaves room to run it again. Raise crawlLimit per school, or OPENAI_MODEL, once the account's tier allows it.
  • /demo is not a tour. It signs the visitor in anonymously, gives them a real household with a real address and a board already worth looking at, and hands them the same app a signed-up family uses — every control live, private to them. A read-only demo would show the output while hiding the point, which is two people moving the same list, and it would be a second lesser version of the app to keep working.
  • A trial board can be kept. An anonymous session lives in one browser and has no credentials to return with, so signing up afterwards used to mint an empty account and strand the work. Convex Auth has no account linking, so the hand-off is a single-use ticket minted by the household's owner while the trial session can still prove it owns the household, and redeemed by whoever they sign up as. The household moves rather than being shared; it is refused when the account already has one of its own.

Licence

MIT — see LICENSE.

About

Everything a school asks of your family, as one shared board — with the quote from the notice that says so. Convex · OpenAI · Firecrawl · AgentMail.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages