back

work · stack

Stack

A local-first reading app with spaced review of highlights

Solo: product, design, Android app, sync, AI layer · 2026

Next.js 16TypeScriptCapacitor 8IndexedDBSupabaseDeno Edge FunctionsSarvam · Gemini · Groq · OpenAI · Claudepdf.jsApache JMeter
Stack: A local-first reading app with spaced review of highlights

Problem

People read a lot of articles, PDFs and news every day and forget almost all of it a week later. Read-later apps save links but never bring them back. Stack turns any line you select into a note that remembers where it came from, then shows your old highlights again on a spaced schedule, so what you read stays with you.

In short

  • -Local-first: everything lives in IndexedDB and works offline with no account; sign-in only adds sync across devices.
  • -Sync with a server-numbered seq per write: upload local changes, pull rows newer than the last one seen; unsynced local edits win.
  • -Stack AI runs in one Supabase Edge Function: checks sign-in, enforces a per-person daily limit, then falls back across five engines (Sarvam → Gemini → Groq → OpenAI → Claude).
  • -Daily review: 3 old highlights a day, pushed out 3 days, then ~2.5× longer each time you remember them.
  • -One Next.js codebase ships the website and a native Android app (Capacitor), with Java plugins for music, sharing and PDF discovery.
  • -Load-tested with JMeter: 500/500 requests served with the top two engines down (p95 1.6 s), and 80 simultaneous requests from one person stopped at exactly the 50/day limit.

Architecture

A Next.js 16 app exported as a static bundle and wrapped in Capacitor for Android. The phone has no server of its own: data lives on the device, sync and AI go to Supabase, and no API key ever ships in the app.

System designNo server on the phone: data lives on the device; sync and AI go to Supabase; no key ships in the app.
ANDROID · CAPACITOR 8SUPABASEAI ENGINESusesnativeHTTPpluginsevery writesummarize· askfallbackupload ·pull > sequser_idReadersave · highlight · review ·focusNews + webBBC · The Hindu · HN · anyRSS feedNative pluginsMedia3 music · share card ·PDF discoveryStack appNext.js 16 static export ·reader mode · pdf.jsIndexedDBlocal-first · every writetracked for syncAI functionsign-in check · daily limit· streams NDJSONPostgresitems table · server seq ·RLS · PDF storageSupabase AuthGoogle · emailFallback chainSarvam → Gemini → Groq →OpenAI → Claude
  • user
  • external
  • service
  • client
  • data
  • guard
  • ai
  • call
  • logical link
  • ← scroll →
  1. 01Reader saves an article, PDF or shared link (Share to Stack from any app)
  2. 02Reader mode cleans the page (Readability + DOMPurify); selecting text makes a highlight or note
  3. 03Every write goes to IndexedDB and is marked as changed, so it works offline
  4. 04When signed in, changes upload to the Supabase items table; rows from other devices download by seq
  5. 05Optional Stack AI calls the Edge Function: sign-in check → daily limit → engine fallback chain → streamed answer with page and note links
  6. 06Each morning the review picks 3 due highlights and reschedules them by how well you remembered

Decisions

+ Local-first IndexedDB, with sync as an optional extra- Cloud database as the source of truth with a required account  Reading happens on trains and in bad signal; the app has to open instantly and work with no network and no sign-up.
+ Server-assigned seq numbers and 'unsynced local edit wins'- Timestamp-based last-write-wins  Phone clocks drift; a server counter gives every device a clean 'give me everything after N' cursor, and nothing you just typed is overwritten.
+ AI only in a Supabase Edge Function with an engine fallback chain- Calling one provider straight from the app  Keys never ship in the APK, limits are enforced server-side, and free tiers can be stacked so one provider being down or out of credit doesn't break the feature.
+ One Next.js codebase exported to Capacitor- A separate React Native app  One solo developer, one codebase for web and Android; native Java plugins only where the web can't reach (music, share sheet, files).

Failure handling

No network, or the user never signs in

Everything reads and writes IndexedDB; changes queue and upload on the next sync.

The same item edited on two devices

Local edits not yet uploaded win; everything else follows the server's seq order.

An AI engine is down, busy or out of credit

The function moves to the next engine; the app gets a reset event and drops the partial text.

A person hits the daily AI limit

The function refuses with a clear 429 message; the rest of the app keeps working.

A feed URL points at a private or local address

The website's feed proxy resolves the host and re-checks every redirect; private IPs are refused.

Numbers

5

AI engines in the fallback chain

3

Highlights reviewed per day

50

Daily AI requests per person

configurable

500/500

Served with top two engines down

JMeter · p95 1.6 s

Load tests

JMeter 5.6.3 against the real Deno edge function and the real ai_take() SQL; AI engines replaced by mock servers with realistic delays.

Healthy

100 users × 5 summaries

500/500 OK, p95 817 ms

Failover

Sarvam + Gemini return 503

500/500 served by Groq, p95 1.6 s

Total outage

All engines down, 100 requests

100/100 got a clean error message, no crash

Daily-limit race

80 simultaneous requests, one person, limit 50

Counter stopped at exactly 50; the rest got 429

Bad sign-in

50 forged tokens

50/50 rejected with 401

Next

  • ○ Play Store release
  • ○ Grow testers from the soft launch
  • ○ iOS build (Capacitor project already in place)