LANGUAGE LEARNING · PWA + iOS · 2026

Chunks 语块

Learn English in chunks — from sentences you actually wanted to say.

TYPE
PWA + native iOS
YEAR
2026
STACK
JS · SwiftUI · Worker · D1
STATUS
PWA in daily use · iOS in progress
Chunks — sentence to flashcard pipeline
WHAT IT DOES

Your own sentences become your deck.

Vocabulary apps teach words you'll never use. Chunks starts from the sentence you just failed to say: type it in Chinese, get the natural English back, and the reusable phrases inside it become your flashcards — automatically.

01

Sentence in, chunks out

One Claude call per sentence returns a natural translation plus 1–4 reusable phrases, each saved as a card with its Chinese meaning and an example. If nothing is worth extracting, the whole sentence becomes the card.

02

Spaced repetition, twice

The PWA runs a deliberately simplified SM-2 with three grades. The iOS app runs full FSRS with a retrievability-sorted queue — and old SM-2 cards migrate into FSRS state instead of being reset. Swipes map to grades, so two gestures drive a four-grade algorithm.

03

Offline-first sync

Cards live on the device and sync through a Cloudflare Worker to D1: last-write-wins by timestamp, tombstones for deletes, 500 cards per batch — and the server enforces LWW again in its upsert, so a stale device can never clobber newer state.

04

One Worker, two AI providers

The Worker holds the API keys server-side and proxies both Claude and DeepSeek. The iOS app picks the provider by device region — mainland devices get DeepSeek — because the target is the China App Store.

THE ARCHITECTURE

Two clients, one thin backend.

A zero-build PWA and a SwiftUI iOS app share one Cloudflare Worker that does exactly three jobs: proxy the AI providers, guard the keys, and sync cards to D1. Tap a node to see why.

PWA VANILLA JS · NO BUILD iOS app SWIFTUI · IN PROGRESS CF Worker PROXY · KEYS · SYNC token-gated routes Claude · DeepSeek BY DEVICE REGION D1 Database CARDS · LWW UPSERT
TIP

Tap a node

Click on any block in the diagram to see what it does and why I picked it.

DECISIONS

Three calls that shaped it.

01

Last-write-wins over CRDT

One user, multiple devices — the odds of editing the same card on two devices at once are tiny. LWW is three lines of merge code on each client plus one WHERE clause on the server. I accepted losing one side in that rare race, and skipped vector clocks entirely.

02

Soft deletes, because the cursor demands it

Sync pulls rows where updated_at is newer than the cursor. A hard delete produces no row — the other device would never find out. So deletion is a tombstone flag plus a timestamp bump, and the UI filters instead of the store. The cost: every query must exclude tombstones, forever.

03

I shipped auto-tagging, then killed it

Cards used to get an automatic scene tag. In real use the labels bent to sentence emotion — a neutral phrase tagged as 'worry' because the sentence felt anxious — and I found myself searching, never browsing by scene. Cut it from the iOS rewrite, and wrote a do-not-rebuild rule into the project doc.

Back to all works