Inspiration
In Y1T2 I missed the weekly meal form three weeks in a row and ate the wrong thing for half a term. The information was there — buried under fourteen other ALU threads. It was just unseen.
That's when I realised the things that hurt ALU students most aren't the deadlines we forget. They're the ones we never see: a recurring form quietly auto-assigning the wrong meals; a permit renewal you only realise was urgent the night before; one missed session in Web Infrastructure that quietly threads through your grade, your grant, your standing, and your visa — all at once. ALU is uniquely cross-domain (you're juggling academics, funding, progression and an annual permit at the same time) but every existing tool treats those as four separate planners. So Ray and I set out to build the co-pilot we wished we'd had on day one — calm, supportive, never alarmist — that reads the boring places for you and reasons across all four domains.
What it does
Qobi is the AI co-pilot for ALU students. It reads your ALU Gmail and Canvas (read-only), catches what's easy to miss, and reasons cross-domain about how one slip ripples through your degree, money, standing and visa. Concretely:
- An editorial morning brief — short, spoken, italic — instead of a wall of cards.
- Cross-domain cascade — one obligation expanded into the academic, financial, standing and immigration consequences it actually triggers, with structured detail rows + a supportive close.
- Permit tracker — appointment booker, persisted checklist, escalating reminders timeline.
- Standing & Money — your tier, buffer, what your grant covers and doesn't, and live Canvas submission-rate signals when connected.
- Cohort view — privacy-suppressed aggregates (no individual standing ever exposed; buckets under 3 students hidden server-side).
- Activity feed, done/snooze with optimistic + WebSocket reconcile, real-time plan updates the moment Gmail catches something new.
How we built it
Three deployments, one engine.
- Backend (
qobi-backend) — Express + TypeScript + Supabase (Postgres + RLS). The reasoning engine is Claude Opus 4.7 with zod-validated structured outputs, adaptive thinking, a cached system prefix, and per-user blocks (program, funding, tier, residency, intake). All Claude calls are server-side (single billing path); clients never see the model directly. - Web (
qobi-website) — Next.js 15 (App Router) + React 19 + TanStack Query + Tailwind v4, on the original Claude Design system (Instrument Serif headlines, Geist body, JetBrains Mono labels, brand red#e0202a). Real Google OAuth → backend → fragment-token handback; an "Authenticating…" checklist before routing into the dashboard or the onboarding wizard. - Mobile (
qobi-mobile) — Flutter, CLEAN + Riverpod + go_router, wired to the same backend with real OAuth, FCM push, and Supabase Realtime parity.
Ingestion: Gmail Pub/Sub push (with a 15-min poll as a safety net), Canvas planner API (with .ics as a fallback — both now persisted), all PII-scrubbed before storage, dedupe-keyed for idempotent upserts. Notifications: email via Resend (Welcome from Teni; briefs + reminders from Ray, brand-skinned) and FCM on mobile, with email backup when push fails. Realtime: a custom WS + Supabase Realtime so the plan refreshes itself the moment a new item lands.
What we learned
- The hard part isn't the reminders — it's the meaning. Anyone can ping you. Reading meaning across a hundred ALU senders and noticing the one slip that ripples through four domains is the wedge.
- Privacy as a feature, not a footnote. RLS on every per-user table; the anon key reads nothing directly; tokens encrypted at rest; cohort buckets under 3 students dropped server-side. Trust is the product.
- Additive contracts move teams in parallel. Every new backend field shipped as optional + nullable, so the frontend kept working while features lit up the moment data arrived. Same pattern for the cascade, the bootstrap timestamps, the risk signals.
- Tone is the product. Calm copy, an italic editorial briefing voice, "Qobi is cooking your first brief…" instead of "no data" — these tiny choices are what make an obligation-tracker feel humane.
Challenges we faced
- Cross-domain reasoning in one prompt. Getting Opus 4.7 to produce a validated, structured plan with per-item cascade nodes (
head/anchor/detail/cascade_supportive) without losing brand voice took several rounds of zod schema + prompt engineering. - Two real ingest sources, one set of dedupe keys. Canvas planner +
.icshad to share a dedupe key so the same assignment never doubled up; Gmail'susers.watch+ Pub/Sub had to coexist with a 15-min poll as a fallback. - The "Canvas can't give us attendance" reality. Personal Canvas tokens only expose submission status, not attendance % — so we honestly relabelled the Standing risk signals as submission rates rather than fabricating numbers.
- The onboarding-then-cooking gap. New users were landing on a dashboard that said "you're caught up" while sync + first plan were still running. We added a server-truthful cooking state (
created_at+first_brief_at) with an 8-minute fallback heuristic and an in-app countdown so new users never feel orphaned. - OAuth across three clients. Web (URL fragment), mobile (deep link), Swagger (JSON) — all from one callback, with an ALU-domain gate that fails gracefully to a polished
NotEligibleScreen. - Realtime without polling. A custom WS for app-level events (
plan.updated,briefing.ready,obligation.updated), Supabase Realtime as the resilient row-sync, and TanStack Query'ssetQueryDatafor optimistic done/snooze so taps feel instant. - Deploy + DNS hiccups at the worst possible moments — late-stage frontend host timeouts while the backend kept serving 200s. Reminders that "it works on my machine" ends at the edge.
In the end, Qobi became what it always wanted to be: nothing buried. Nothing missed. Nothing alarmist.
Built With
- claude
- express-js
- flutter
- google-cloud
- next-js
- redis
- resend-js
- supabase
Log in or sign up for Devpost to join the conversation.