Back to work
Writing Platform·2026

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
The Writer's Haven editor
01

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.

02

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.

03

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.

04

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.

05

Challenges

High

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.

High

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.

Medium

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.

06

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

src/routes/_authenticated/route.tsxtypescript
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 />,
});