Database Schema

Writan uses Supabase (PostgreSQL) with three core tables, mirrored locally in IndexedDB via Dexie for offline-first support.

projects

id          uuid primary key default gen_random_uuid()
user_id     uuid references auth.users not null
title       text not null
created_at  timestamptz default now()
updated_at  timestamptz default now()

nodes

The central table — every piece of content is a node.

id              uuid primary key default gen_random_uuid()
project_id      uuid references projects not null
parent_id       uuid references nodes null    -- null = root node (Book or Series)
type            text not null                 -- 'series' | 'book' | 'act' | 'chapter' | 'scene' | 'beat'
title           text
position        integer not null default 0    -- ordering among siblings
 
-- Output layer
prose           text                          -- the readable text output
 
-- Data layer
data            jsonb default '{}'            -- typed + freeform data blocks
 
-- Generation layer
generation      jsonb default '{}'            -- beat notes, AI prompt log, decision record
 
-- Metadata
created_at      timestamptz default now()
updated_at      timestamptz default now()

Indexes

create index nodes_project_id_idx on nodes(project_id);
create index nodes_parent_id_idx on nodes(parent_id);
create index nodes_position_idx on nodes(parent_id, position);

pips

Feedback indicators attached to nodes.

id          uuid primary key default gen_random_uuid()
node_id     uuid references nodes not null
type        text not null           -- 'tutor_red' | 'tutor_amber' | 'tutor_blue' | 'craft' | 'generation'
status      text not null default 'open'  -- 'open' | 'answered' | 'dismissed' | 'snoozed'
question    text not null           -- the question or feedback text
detail      text                    -- expanded detail shown in popup
dismissal   text                    -- reason: 'answered' | 'deliberate' | 'not_applicable' | 'revisit'
note        text                    -- writer's own note when dismissing
source      text                    -- rule ID or 'tutor' for origin tracking
created_at  timestamptz default now()
updated_at  timestamptz default now()

JSONB Shapes

See Data, Generation & Output Layers for the full TypeScript shapes of the data and generation JSONB fields.

Local Mirror

All three tables are mirrored in IndexedDB (lib/db/localDb.ts) via Dexie, plus a syncQueue table that buffers operations for batch sync to Supabase. See Store Architecture for the sync pattern.