work · stack
Stack
A local-first reading app with spaced review of highlights
Solo: product, design, Android app, sync, AI layer · 2026

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.
- user
- external
- service
- client
- data
- guard
- ai
- call
- logical link
- ← scroll →
- 01Reader saves an article, PDF or shared link (Share to Stack from any app)
- 02Reader mode cleans the page (Readability + DOMPurify); selecting text makes a highlight or note
- 03Every write goes to IndexedDB and is marked as changed, so it works offline
- 04When signed in, changes upload to the Supabase items table; rows from other devices download by seq
- 05Optional Stack AI calls the Edge Function: sign-in check → daily limit → engine fallback chain → streamed answer with page and note links
- 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.
- user
- external
- service
- client
- data
- guard
- ai
- call
- logical link
- ← scroll →
save from anywhere
externalArticle / news
reader mode: Readability + DOMPurify
clientPDF / EPUB
pdf.js reader · phone files · Telegram import
serviceShare to Stack
from Chrome, WhatsApp, YouTube, Files
- user
Select a line
Highlight · + Note · Share
- data
Note saved with its source
painted back with the CSS Custom Highlight API
- client
Daily review
3 due highlights each morning, also in the notification
- service
Export or search
one search across everything · Markdown export to Obsidian / Notion
- user
Reader taps Summarize, Ask your Stack or Ask this PDF
- guard
Consent prompt in the app
AI switched off in Settings → the buttons are hidden
- guard
Verify Supabase sign-in
not signed in → sign-in prompt
- guard
Daily limit (ai_take in Postgres)
over 50 today → 429, resets at midnight UTC
engines, tried in order · only engines with a key; on failure the next one takes over
aiSarvam AI
aiGoogle Gemini
first for PDFs
aiGroq
aiOpenAI
aiClaude
- client
Streamed answer
citations become page and note links
- user
Edit on the device
- data
Write to IndexedDB + mark changed
the app never waits on the network
- guard
Signed in?
no → stays local; backup file still works
- service
Upload changed items
one row per item; the server stamps the next seq
- service
Download rows with seq > last seen
pages of rows, cursor saved per account
- data
Merge
item edited here but not uploaded → local copy wins
- ● NEW
- DUE NEXT DAY
- GOT IT · 3 DAYS
- × 2.5 EACH TIME
- UP TO 365 DAYS
SHOW AGAIN SOON
back tomorrow with a 1-day interval
STOP
never shown in review again
Cloud database as the source of truth with a required account
Local-first IndexedDB, with sync as an optional extra
Reading happens on trains and in bad signal; the app has to open instantly and work with no network and no sign-up.
Timestamp-based last-write-wins
Server-assigned seq numbers and 'unsynced local edit 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.
Calling one provider straight from the app
AI only in a Supabase Edge Function with an engine fallback chain
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.
A separate React Native app
One Next.js codebase exported to Capacitor
One solo developer, one codebase for web and Android; native Java plugins only where the web can't reach (music, share sheet, files).
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)