The Writer's Haven
A private writing sanctuary for one serious writer — a rich editorial studio, a screenplay editor, an AI assistant and a second brain, publishing selected work publicly.
- Role
- Client Project — Full-Stack Build
- Year
- 2026
- Category
- Writing Platform
- Platform
- Web App
- Stack
- TanStack StartReact 19TypeScriptSupabaseTipTapOpenAID3Vercel

Why I built this
A serious writer needed a private, distraction-free home for long-form work — essays, blogs, philosophical pieces, scripts and research notes — that also publishes selected work publicly to readers. The product philosophy, in the client's own words: 'Writer first. Chrome second. AI third.' No collaboration overhead, no social features — just the tools to write, organize, explore and publish.
How it works
Rich editorial studio
A TipTap-based editor with footnotes, callouts, resizable images (upload/paste/drag-drop to Supabase storage), tables, YouTube embeds, slash commands and wiki-style [[links]], with debounced autosave and version snapshots every ten minutes.
Screenplay script editor
A dedicated script mode with Fountain-lite line classification — scene headings, character cues, parentheticals, dialogue and transitions — plus Tab cycling between element types and word/page counts.
AI assistant with eight actions
Enhance Prose, Tighten, Rewrite, Summaries, Critique, Philosophy Mode, Research Assistant and Script Notes — served through a diff view with Accept / Insert Above / Insert Below / Copy / Reject.
Second brain
Books (with OpenLibrary ISBN lookup), an ideas pipeline (raw → developing → promoted), searchable notes, and a D3 force-directed knowledge graph connecting pieces, books, ideas and notes.
Publishing and dashboards
Public/private toggles with an SEO checklist, a reader view with progress bar, and a dashboard with a 90-day writing streak calendar, weekly word comparisons and AI usage/cost stats.
Key decisions
The editor and writing surface were built before any AI feature. The AI panel is a secondary assistant with explicit depth controls — never an autopilot.
Every table has row-level security using a has_role(auth.uid(), 'owner') pattern; app-level middleware is a backstop, not the primary barrier. A dedicated migration fixed a PostgREST gotcha where the enum-typed function needed a text parameter.
OpenAI calls run in server functions with the key read exclusively from process.env; tokens and cost are logged per call.
Backend architecture
12 tables across 5 migrations
pieces, piece_versions, book_notes, ideas, kb_notes, writing_sessions, cross_links and ai_usage_log — all RLS-enabled with owner-manage policies plus public-read policies for published content.
Single-owner auth model
No self-registration; the owner account is created via a service-role script, and a user_roles table stores the owner role. Protected routes sit under an _authenticated layout that redirects to /login.
Two-layer server-function protection
attachSupabaseAuth attaches the bearer token to every server-function RPC; requireSupabaseAuth verifies the JWT via auth.getClaims() and hands handlers a typed context.
Serverless AI and deployment
runAI routes models by task (gpt-5-mini for prose edits, gpt-5 for analysis) and logs to ai_usage_log. The app deploys as a Nitro serverless build on Vercel with the preset pinned in vite.config.ts.
Challenges
Ideas status schema/UI mismatch
The database CHECK constraint allowed raw/developing/promoted but the UI wrote seed/ready/archived — creating or updating ideas could fail. Fixed and verified aligned.
Writing-session analytics inflation
Debounced autosave inserted a writing_sessions row on every save, which would massively overcount sessions and corrupt the dashboard streak and word analytics.
Client-only route protection
Protected routes were guarded client-side only and self-registration wasn't disabled — multiple accounts could be created. Hardened with RLS, the two-layer server middleware, and documented Supabase auth settings.
What I learned
Documentation-driven iteration closes the vision gap: the audit → understanding → vision-gap → rebuild-plan chain produced a prioritized execution order.
In a single-owner app, RLS is the true security boundary — and subtle PostgREST casting gotchas can silently break it.
Verify credentials and deployment assumptions explicitly — a typo'd login email and an invalid vercel.json functions pattern each cost real debugging time.
Code snippets
export const Route = createFileRoute("/_authenticated")({
ssr: false,
beforeLoad: async () => {
const { data, error } = await supabase.auth.getUser();
if (error || !data.user) throw redirect({ to: "/login" });
return { user: data.user };
},
component: () => <Outlet />,
});