CritPages
The critique app for unpublished writing. Founder and sole engineer. Built with Claude Code, with me as the architect in the loop, in beta since mid 2026.
Why I built it
So that writers and literature buffs anywhere in the world can share their writing freely and give each other real critique and feedback. Beta reads, peer story sharing, non-fiction too.
What it is
Writers share pages with a critique group, a beta-reader round, or a single partner. Readers mark them up on their phones: a tag, a strike, a heart, or a comment attached to a word, a sentence, or a paragraph, each in about two taps. Each reader sees only their own marks. The writer sees all of them together. Groups can read live, everyone following along on their own device while one person reads aloud.
Native apps for iPhone and Android. The web handles invitations, accounts, finding beta readers, and scheduling. Manuscripts never touch the web.
Stack
- Expo and React Native, TypeScript, with the manuscript painted by Skia rather than rendered as text
- Supabase: Postgres with row-level security, realtime, and edge functions
- Next.js on Vercel Pro for the web, plain and portable by rule
- Resend for every email the software sends
- Route 53 DNS as Terraform, email records included
- Maestro end-to-end flows on real devices, and an RLS suite run against the live database
What kept it from becoming a Frankenstein app
Four hundred commits in fifteen weeks, most of the code written by Claude Code with me directing. The model is not what kept it coherent. A short list of ranked rules did, plus the habit of recording every decision, and its reasoning, in the commit that made it. These are the ones that mattered.
- Ranked goals, written down before the first screen
- The conventions file ranks four goals and says the higher one wins when they conflict: correctness and testability, then privacy and trust, then cheap change, then maintainability for a solo author who comes back after months away. A counterweight sits under them: ship the MVP. Every "helpful" addition the model proposed was tested against that list, and most lost.
- The security boundary lives in Postgres, and is tested against the live database
- The first migration was the schema and its row-level security policies together. The next day an integration suite ran those policies against the real database, and caught a real gap on its first run. That suite has grown to about 150 assertions covering document scoping, per-reviewer isolation, frozen-text immutability, and lifecycle. Unpublished manuscripts are the data; a scoping mistake is a privacy incident with the users least likely to forgive it.
- Supabase is called in exactly one layer
- Domain code is pure and knows nothing about React or the database. Services orchestrate it through repository interfaces. The repository layer is the only place the Supabase client appears, and the UI calls services, never repositories. Dependencies point inward. The same file forbids speculative abstraction: no interface until there is a second implementation or a test seam that needs it.
- Spikes go behind a flag, and the loser gets deleted
- The reader started as ordinary React Native text. A Skia canvas renderer was built as a spike behind a renderer boundary and a flag, retired every risk that could kill the switch, and ten days later became the only renderer. The legacy path, both flags, the swap plan, and thirteen hundred lines went with it. Drawing the page instead of rendering text is also why a manuscript cannot be copied out.
- A decision stands until its premise dies
- A spike opened all of Moby Dick as one document on the simulator and settled the question: no chapter splitting for MVP. The same day a Galaxy S24 Ultra killed the app at 316,000 words. The decision was reversed in a commit that names the dead premise, chooses chapter splitting at import because it is additive code rather than surgery on the most invariant-dense file in the project, and parks the windowed reader with its two known failure modes recorded.
- Decision gates that say no
- A document-to-workspace many-to-many was designed, then deferred at the gate: the survival benefit already shipped through soft delete, and the only unique payoff did not clearly beat a copy. What shipped instead was a re-architecture onto a link model, author-owned pages shared by links, with sessions that end themselves.
- Renames go full depth, and migrations are history
- When "group" became "workspace" and "facilitator" became "owner", the rename went through the schema, not just the UI. An applied migration is never edited, even when its header carries the product's old name. The newest migration that touches a table is the present; the rest are dated records of what was true that day.
- The code is canonical, and the cuts are deliberate
- The spec that started the project was demoted to rationale once the code outgrew it. What still binds from it is the list of things absent on purpose: no page cap, no save buttons, no AI in the product, no drag-to-select. The conventions file tells the model not to reintroduce them as helpful additions.
- The web is plain on purpose
- Full Next.js rather than a static export, because static to server-rendered is a migration and the reverse is a config change. No vendor-only APIs, so the host can move in an afternoon. Netlify was tried and rejected the same day, with the reasons written down. Marketing at the apex, the product on a subdomain, DNS in Terraform.
- Built for the readers it actually has
- Writing circles include retirees with declining vision. The app was walked on an iPhone SE at the largest accessibility text size, every broken control was measured and capped inside the shared components, and the painted manuscript now follows the phone's text setting until the reader picks their own.
Status
Pre-launch beta. Early access is open by email atcritpages.com. Product notes go out on the CritPages Substack.