Files

18 KiB

PubScript — Master Plan

A self-hostable, multi-user writing studio for novelists — one codebase that runs as a web app and as a native desktop app, with a local-first AI co-writing engine built in.

This file is the single source of truth for the architecture, design decisions, and data model of PubScript.


1. Vision & Goals

PubScript is a technology-forward writing application for novelists. It gives an author one coherent workspace where the act of writing, the management of the story's moving parts (characters, world, plot, timeline), and the assistance of AI all live together — and it can be self-hosted as a multi-user web app or run as a desktop app on Linux, Windows, or macOS.

The product is defined by four commitments:

  1. Parity, not approximation. The web app and the desktop app are the same application. One codebase, one data model, one set of features. The desktop shell adds native capabilities (file access, print-to-PDF, local-AI plumbing) but never forks the feature set.
  2. The author's tools, integrated. Characters, world-building, whiteboard flow, and plot structure are first-class citizens — not bolt-on notes. AI feedback is anchored to specific passages and offered as adoptable/rejectable revisions, never a wall of free text.
  3. Self-hostable and private. One docker compose up runs the whole server (accounts, admin, projects, AI). Authors own their data. Local AI (Ollama, llama.cpp, LM Studio) is a first-class provider alongside OpenAI, Anthropic, Gemini, and OpenRouter.
  4. Fast enough to disappear. A 100k-word novel must open in seconds, typing must never lag, and a 200-node whiteboard must stay smooth. Performance is a feature, not an afterthought.

Non-goals (for v1)

  • Real-time collaborative editing (two cursors in the same document). Multi-user means accounts, roles, and sharing, not live co-editing.
  • Mobile-native apps. The responsive web app covers tablets/phones for reading and light edits.
  • A public SaaS / billing system. Self-hosted instances are private; there is no payment layer.

2. Locked Decisions

These were confirmed with the author and are fixed for the build:

Decision Value
Primary language TypeScript (strict) across app, packages, and server
UI framework React 19 + Vite 6 + Tailwind v4
Desktop shell Tauri 2 (Rust) wrapping the same built web bundle
Rich-text engine TipTap 3 (ProseMirror)
Flow / whiteboard React Flow 12 (@xyflow/react)
AI integration Vercel AI SDK v5
Server Fastify + SQLite (better-sqlite3), single-file, self-hosted
Export formats Markdown, HTML, EPUB 3, DOCX, ODT (OpenDocument), PDF
Multi-user Yes — accounts, an admin role, project sharing, managed from both web and desktop
Platform priority Linux first (Arch, Debian, Fedora), then Windows, then macOS
Self-hosting Docker Compose (one container serves the web app + API + data)

Why this stack

  • One TypeScript codebase for app and server means a single language, shared types, and no context-switching — the most efficient path for both a human and an agent.
  • Tauri 2 gives a ~10 MB native shell (vs ~150 MB for Electron) with real OS integration, and because it loads the identical web build, parity is structural, not aspirational.
  • SQLite keeps self-hosting trivial (one file to back up) while remaining fast enough for multi-user at the scale a writing team actually runs.
  • TipTap 3 + React Flow 12 + AI SDK v5 are the current, actively-maintained leaders in their categories and compose cleanly.

3. Architecture

3.1 The three runtime modes (one codebase)

                 ┌─────────────────────────────────────────────┐
                 │              PubScript App (React 19)         │
                 │  writing · characters · world · plot ·        │
                 │  whiteboard · AI roles · export · settings     │
                 └───────────────┬──────────────────────────────┘
                                 │  talks to a StorageAdapter
            ┌────────────────────┼───────────────────────┐
            ▼                    ▼                         ▼
   ┌────────────────┐   ┌──────────────────┐    ┌───────────────────┐
   │  Web browser   │   │  Tauri desktop   │    │  Local / offline  │
   │  (self-hosted) │   │  shell (Linux/   │    │  mode (desktop,   │
   │                │   │  Win/macOS)      │    │   no server)      │
   └───────┬────────┘   └────────┬─────────┘    └─────────┬─────────┘
           │  REST + WebSocket   │  same REST + WS         │  local adapter
           ▼                     ▼                          ▼
   ┌───────────────────────────────────┐        ┌──────────────────────┐
   │        PubScript Server           │        │  LocalStorageAdapter │
   │  Fastify + SQLite (multi-user,    │        │  (Dexie / Tauri FS) │
   │  accounts, admin, sharing, AI)    │        │                      │
   └───────────────────────────────────┘        └──────────────────────┘
  • Web mode: the browser talks to the self-hosted server over REST + WebSocket.
  • Desktop + server mode: the Tauri shell runs the same app and talks to the same server (over LAN or the internet). Admin and account management work identically here.
  • Desktop local mode: with no server configured, the app uses a local storage adapter (IndexedDB in-browser, Tauri filesystem on disk). Same UI, same features, data stays on the machine. This is the offline fallback.

The StorageAdapter contract (defined once in packages/core) is the seam that makes all three modes possible. Every feature codes against the contract, never against a specific backend.

3.2 Repository layout

PubScript/
├── PLAN.md                     ← architecture & design specification
├── README.md                   ← installation, quickstart & usage guide
├── package.json                ← pnpm workspace root
├── pnpm-workspace.yaml
├── apps/
│   └── web/                    ← the React 19 app (Vite)
├── desktop/                    ← Native Linux desktop shell & .deb packager
├── packages/
│   ├── core/                   ← domain types, IDs, time, token budget, StorageAdapter contract
│   ├── ui/                     ← design tokens, theme engine, app shell, component kit
│   ├── editor/                 ← TipTap schema, decorations, spellcheck integration
│   ├── exporters/              ← md / html / epub / docx / odt / pdf + shared flat model
│   └── ai/                     ← provider adapters, role engine, context builder, suggestion protocol
├── server/                     ← Fastify + SQLite: auth, multi-tenant, WS, admin, AI proxy
├── docker/                     ← Dockerfile, compose.yaml, entrypoint
├── .github/workflows/          ← CI (lint, typecheck, test, contract, build, docker)
└── docs/                       ← Data model reference & conventions

3.3 Data model (core domain)

All IDs are ULIDs (sortable, collision-free). All timestamps are UTC ISO-8601.

User        { id, email, displayName, role: 'admin' | 'author', passKey?, createdAt }
Project     { id, title, ownerId, sharedWith: {userId, perm: 'read'|'edit'}[],
              settings, cover, createdAt, updatedAt }
Book        { id, projectId, title, order, chapters: Chapter[] }
Chapter     { id, bookId, title, doc: ProseMirrorDoc, order, meta }
Scene       // v1: a scene is a `sceneBreak` node inside a document; optional Scene meta
Character   { id, projectId, name, role, bio, appearance, voice,
              relationships: {targetId, kind, note}[], arc, status, tags[] }
WorldItem   { id, projectId, kind: 'location'|'faction'|'item'|'glossary'|'system',
              name, fields: Record<string, string> }
PlotBeat    { id, projectId, template, title, description,
              status: 'draft'|'revised'|'locked', chapterId?, order, notes }
TimelineEvent { id, projectId, when: {era, day} | date, title, description,
                chapterId?, links[] }
Board       { id, projectId, nodes: FlowNode[], edges: FlowEdge[] }
Note        { id, projectId, body, pinned, tags[], linkedTo }
AISession   { id, projectId, role, model, settings, messages[], suggestions[] }
Suggestion  { id, sessionId, target: {docId, range|nodePath}, before, after,
              rationale, kind: 'replace'|'insert'|'delete'|'entity-update'|'draft',
              status: 'pending'|'accepted'|'rejected'|'pinned' }

3.4 StorageAdapter contract

interface StorageAdapter {
  // projects
  listProjects(): Promise<ProjectMeta[]>
  createProject(p: NewProject): Promise<Project>
  getProject(id: string): Promise<Project>
  // content (debounced autosave)
  saveDocument(doc: DocumentRef): Promise<void>
  saveEntity(e: Entity): Promise<void>
  saveBoard(b: Board): Promise<void>
  saveAISession(s: AISession): Promise<void>
  // live updates
  onEvent(cb: (e: StoreEvent) => void): () => void
}

Two implementations:

  • LocalAdapter — Dexie (IndexedDB) in the browser, Tauri FS on disk. Single-user, offline.
  • ServerAdapter — REST + WebSocket to the Fastify server. Multi-user, accounts, admin.

A contract test suite runs the same assertions against both, so parity is enforced by CI, not by hope.


4. Feature Specification

4.1 Writing surface (the heart of the app)

  • TipTap 3 editor with a clean, distraction-free canvas and a focus/typewriter mode.
  • Document tree: Book → Part → Chapter, drag-to-reorder, per-chapter word counts.
  • Native formatting: headings, emphasis, blockquote, scene breaks, footnotes, inline character mentions.
  • Fonts: a curated set of high-quality open fonts (e.g. EB Garamond, Source Serif 4, Inter, Space Grotesk, JetBrains Mono), selectable with size/leading/margins/measure controls.
  • Spell & grammar check (see 4.5).
  • Live word count, session stats, reading time.
  • Markdown import/export built in.

4.2 Characters & world-building

  • Character sheets: name, role, bio, voice, appearance, relationships (typed links to other characters), arc, status.
  • World items: locations, factions, items, glossary, systems — each a flexible field set.
  • Fuzzy search across all entities; backlinks; @mention autocomplete in the editor that links prose to entities.
  • A relationship/affinity view.

4.3 Plot & structure

  • Plot beat board with templates: Three-Act, Save-the-Cat (15 beats), Seven-Point, and custom. Beats carry status (draft/revised/locked), link to chapters, and show progress.
  • Timeline: a configurable calendar (era/day or real dates), events, causal links, filtered by character/location.
  • Whiteboard flow editor: free-form React Flow canvas — entity cards, notes, chapter nodes, images, groups; typed edges; minimap; auto-layout; 200+ nodes at 60 fps.

4.4 AI (see §6)

Four agentic roles — Editor, Reader, Publisher, Co-Author — with robust base prompts, per-role temperature/thinking/context settings, and feedback that is anchored to passages as adoptable/rejectable suggestions.

4.5 Spell & grammar

  • Spellcheck: hunspell compiled to WASM (runs fully in-browser, offline). Per-language dictionaries + a user custom dictionary.
  • Grammar/style: optional LanguageTool server (self-hostable, OpenAI-compatible-ish API) for grammar, style, and punctuation.
  • Inline squiggle decorations + a side panel to jump/apply/ignore.

4.6 Export

Markdown, single-file HTML, EPUB 3, DOCX, ODT, and PDF — with title/author/cover/TOC/styling options. (Details in Chunk 6.)

4.7 Theming

  • CSS-variable design tokens; light & dark; accent color; font/size/leading/density.
  • Shipped themes: Ink (default), Parchment, Nocturne, Terminal.
  • Settings persist per-user (server) or per-machine (local).

5. AI Design

5.1 Providers

Configured as { id, kind, baseUrl, apiKey, model, temperature, maxTokens, thinking, contextWindow }.

  • OpenAI-compatible (covers Ollama /v1, llama.cpp /v1 with --jinja, LM Studio, vLLM) — the workhorse for local models.
  • Anthropic (Claude), Google (Gemini), OpenRouter — via the AI SDK's native adapters.
  • Thinking / reasoning: o-series → reasoningEffort; Anthropic → thinking budget; local (e.g. Qwen3) → configurable extra body params / chat_template_kwargs.

5.2 Token budget

gpt-tokenizer (o200k_base) counts tokens. A budget engine allocates: system+role ≈ 10%, context pack ≈ 40%, history ≈ 30%, output reserve ≈ 20% (all adjustable). Context is assembled from the author's own data — characters, world, relevant chapters — so the AI always writes with the story's canon.

5.3 The four roles (base prompts live in Chunk 9)

  • Editor — a line editor: tighten prose, kill filter words, sharpen dialogue tags, fix pacing, flag weak openings/closings, check consistency against character/world data. Emits targeted, structured edits with rationale. Respects the author's voice.
  • Reader — an honest first-time reader: what was confusing, where attention dropped, what was telegraphed too early, what paid off. Reports as a reader, not in craft jargon.
  • Publisher — market & packaging: comps, hook, pacing vs. genre, title/cover/blurb ideas, series potential, format decisions.
  • Co-Author — a collaborator with full project knowledge: drafts scenes/chapters in the author's established voice, proposes plot turns, writes dialogue, expands world-building — always as suggestions the author accepts or rejects, never silently rewriting canon.

5.4 Tools (AI SDK tool-calls)

applySuggestion, updateEntity, addNote, draftChapter, queryProject (search). These let a role act on the workspace, not just talk about it.

5.5 Suggestion protocol (the signature feature)

AI output is structured JSON: { target: {docId, range}, before, after, rationale, kind }. The editor renders these as inline decorations over the exact passages. The author can Accept (applies the edit), Reject, or Pin (copies it into a pinned note for later). This is what makes AI feedback integrated into the tools rather than a chat sidebar.


6. Multi-User, Admin & Security

  • Accounts: email + password (argon2id), session tokens (httpOnly cookie on web, bearer on desktop). Optional passkey/WebAuthn later.
  • Roles: admin and author. Admin manages users (create, disable, change role) and sees activity — from both the web app and the desktop app.
  • Sharing: project-level, invite by email, read/edit permissions, owner transfer.
  • Security: rate limiting, audit log, multi-tenant isolation (a user only ever sees projects they own or are shared with), secrets never leave the instance.
  • Data: one SQLite file = the whole database; trivially backed up.

7. Platforms & Packaging

Target Artifact Notes
Web (self-hosted) Docker image + compose.yaml One container: serves the web app, runs the API, stores SQLite on a volume. docker compose up and you're in.
Linux (priority) AppImage, .deb, .rpm Tauri 2. First-class on Arch/Debian/Fedora.
Windows NSIS installer + portable Tauri.
macOS .dmg (arm64 + x86_64) Tauri; notarization is a later polish step.

Desktop adds: native file-system access (open/save anywhere), system print-to-PDF, and direct plumbing to local AI servers (Ollama/llama.cpp on the same or another machine).


8. Quality Bar & Conventions

  • TypeScript strict everywhere; no any; shared types in packages/core used by app and server.
  • ESLint + Prettier; conventional commits; pnpm workspaces; pinned, lockfile-committed versions.
  • Testing: Vitest (unit), a storage contract suite (same tests vs. Local & Server adapters), golden-file tests for exporters (epubcheck, unzip-validate), property tests for the token budget, and Playwright e2e smoke (write → autosave → reopen).
  • Performance budgets: 100k-word project opens < 2 s; typing latency < 16 ms; 200-node board at 60 fps; entity search < 50 ms.
  • CI (GitHub Actions): on push → install, lint, typecheck, unit tests, build web, build server, build Docker image. Desktop build matrix on demand.

9. Risks & Mitigations

Risk Mitigation
Large-document editor performance ProseMirror is built for this; decorations and spellcheck runs are debounced and block-scoped
AI suggestion rendering over live-editing text (range drift) Suggestions carry verbatim text anchors (originalText), re-resolved on inline edits
Local vs. server data divergence Contract test suite enforces identical behavior across LocalAdapter and ServerAdapter
EPUB/ODT spec edge cases Golden-file tests in CI; deterministic timestamps across ZIP/PDF formats
Scope creep in AI roles Roles are prompt + tools, unified under a single inline suggestion and session notes protocol