- Python 62%
- TypeScript 33.5%
- CSS 4.2%
- Shell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| backend | ||
| deploy | ||
| docs/media | ||
| frontend | ||
| scripts | ||
| Specs | ||
| .env.example | ||
| .gitignore | ||
| HANDOVER.md | ||
| LICENSE | ||
| NOTICE | ||
| PLAN.md | ||
| README.md | ||
| SECURITY.md | ||
| TRACKING.md | ||
Writer
Long-form writing app (Scrivener-like binder, Type.ai-style editor, Story Bible, AI brushes). v0.4.0 — Alpha v0.4 (PLAN v0.83; tag v0.4.0-alpha).
Status / to-do: see TRACKING.md (in progress, open, backlog, known issues, done by release).
Current features (v0.4.0): Home + app shell + New project; Settings › Models (Open Notebook–style); Type.ai-parity editor (Must); Story Bible + propose-only auto-extract; autosave + 7 revisions; Settings › Backup (schedule / folder / rclone, optional password-encrypted archives); Pen names; Publish (EPUB 3 + Kindle DOCX + store preflight + EPUBCheck; zh-TW/zh-HK/zh-MO books with subsetted OFL heading fonts, optional body font — run scripts/fetch-fonts.sh once); themes (和紙 default, 7 built-in + custom theme editor) + focus dimming; UI i18n (en + 繁體中文(台灣)+ 繁體中文(香港)+ 廣東話(香港); every screen, server errors and preflight follow the UI language, check-i18n.py blocks hardcoded strings); AI chat tab (threads, streaming, scene/Bible context, scene recall — spoiler-safe earlier-chapter hits behind the 前情回顧 toggle with an 已引用 line); run sheet (shell badge → In progress / History / Diagnostics for every AI job: cancel/pause, recovery candidates to copy or discard, index rebuilds as jobs); Humanize (AI-tell scan + squiggles, Tells panel, zh-TW / zh-HK wording/punctuation pass + reviewable TW→HK conversion, Humanize brush with review, chapter runs, My Voice, Prose health report); 字詞 word tools (right-click: 同音 / 近義·反義 / 錯別字 / 粵語; offline dictionaries — run scripts/fetch-dicts.sh + scripts/build-dict-index.py once); selection brushes (Improve/Expand/Shorten/Simplify/Fix/Humanize/Custom) with Accept/Reject preview; comments on selections; command palette (Ctrl/Cmd+K); LAN/Tailscale (web 3002, API 5056 — works from phones); responsive phone layout. Built (v0.4.0): real multi-user accounts (W-158–W-160), Readability panel (W-146–W-150, W-155–W-157 optional LanguageTool), vertical 直排 EPUB (W-162), dictionary word tools (W-121–W-129), Drive sign-in prompt (W-161); Not yet: Styles/Uploads RAG (scene recall W-173 is in), Continuity/Fact Check, share links, PDF.
MIT licensed. Stack follows Open Notebook (FastAPI + Next.js; originally SurrealDB, SQLite since v0.48 / 2026-10-23, SurrealDB removed in v0.75 / W-193, spec 31). The AI provider / credential / model-discovery layer is adapted from Open Notebook (MIT, © 2024 Luis Novo), and each adapted file keeps its copyright header. See NOTICE. GPL apps (novelWriter, Manuskript, etc.) are UX references only. The editor uses only MIT TipTap packages (no @tiptap-pro).
Ports (chosen so they don't clash with Open Notebook)
| Service | Writer | Open Notebook |
|---|---|---|
| Database | SQLite data/writer.sqlite (no server) |
SurrealDB |
| API | 5056 (127.0.0.1 only) | 5055 |
| Frontend | 3002 | 3000 |
The database is SQLite (data/writer.sqlite; WAL, Alembic migrations run at startup). SurrealDB was removed in W-193 (v0.75, 2026-10-27): there is no WRITER_DB switch, no writer-db service and no port 8001. The last SurrealDB data stays as a cold archive in data/surreal/ and the final SurrealQL exports are in data/pre-sqlite/ (both gitignored).
Run
Prereqs: Python 3.12 + uv and Node 22+ (~/.local/node on AMD). The database is SQLite — no database server to install.
On AMD, Writer runs as systemd user services and starts at boot: systemctl --user status|restart writer-api writer-web (logs: journalctl --user -u writer-web -f). Unit files are in deploy/systemd/. writer-web serves a production build (next start, not next dev). After you change frontend code, run scripts/deploy.sh (builds, then restarts the API and web). The API service does not use uvicorn --reload. For development stop the services and run WRITER_DEV=1 scripts/start-api.sh and WRITER_DEV=1 scripts/start-web.sh (--reload and next dev).
cd /home/wing/Projects/Writer
cp -n .env.example .env # then set WRITER_ENCRYPTION_KEY (see below)
WRITER_DEV=1 ./scripts/start-api.sh # API with --reload (terminal 1)
WRITER_DEV=1 ./scripts/start-web.sh # next dev (terminal 2)
Production: scripts/start-api.sh and scripts/start-web.sh (after scripts/build-web.sh), or scripts/deploy.sh on AMD.
- App: http://127.0.0.1:3002. It always opens to Home.
- API docs: http://127.0.0.1:5056/docs
- Type-check / production build:
cd frontend && npm run build
Environment
| Variable | Purpose |
|---|---|
WRITER_ENCRYPTION_KEY |
Required to add AI credentials. Passphrase used to encrypt API keys at rest (PBKDF2 → Fernet). Without it, Settings › Models shows a banner, "Add configuration" is disabled, and the API returns 409. Keys are never stored as plaintext. Generate one with python3 -c "import secrets; print(secrets.token_urlsafe(32))" and put it in .env (untracked). Keep it stable: if it changes, stored keys can no longer be decrypted. The key is combined with a random per-install salt in data/.writer_salt (created automatically, mode 0600, untracked; SEC-07) — back it up with the key. |
WRITER_PASSWORD |
Optional shared password. Leave it unset to turn auth off (local dev only). |
API_*, CORS_ORIGINS |
See .env.example. |
NEXT_PUBLIC_API_URL |
Frontend → API URL. Leave unset: the browser uses its own origin and Next proxies /api to API_INTERNAL_URL (default http://127.0.0.1:5056). |
Feature tour (started at v0.2)
A. App shell + Home (spec 12)
- Collapsible sidebar. Global items: Home, Projects, New project, Settings. Per-project items: Overview, Write, Bible, and Plan/Revise/Sources (placeholders). Per-series items: Overview and Series Bible.
- Home has:
- Continue writing (last edited scene)
- Today's words
- Project and series cards (open, Bible, rename, archive, delete with typed confirmation)
- Recent documents
- Search across projects, scenes and Bible names/aliases
- New project wizard:
- Types: Novel, Series, Non-fiction, Academic, Short piece
- Structure templates: three-act, Hero's Journey, Save the Cat, 起承轉合, IMRaD and more
- Optional Bible seed
- Choosing Series creates the series, its shared Bible, and Book 1..N.
- Routes:
/,/projects,/projects/new,/p/<id>,/p/<id>/write/<scene>,/p/<id>/bible/<entry>,/p/<id>/report/humanize,/series/<id>,/series/<id>/bible/<entry>,/settings/{models,humanize,backup,pen-names,general,about}.
B. Settings › Models (spec 11, Open Notebook parity)
- Provider cards for every provider Open Notebook supports, each marked Configured / Not configured.
- Multiple credentials per provider. The Add/Edit dialog has: name, API key with show/hide, base URL, and provider-specific fields (Azure, Vertex, OpenAI-compatible…).
- Test connection, and a Discover/Sync models dialog (filter by type, search, multi-select, custom names).
- Model chips grouped by type, each with its own test button.
- Defaults card with Writer task slots:
- Required: chat, embedding
- Optional, falling back to chat: draft, rewrite, review, humanize_en, humanize_zh_tw, fact_check
- Advanced: outline, bible_extract, large_context, tools
- Auto-assign button.
- Encryption-key banner.
- Deleting a credential lets you migrate its models to another credential or delete them.
C. Editor, "Must" tier (spec 15)
- Toolbar: labeled, with tooltips and shortcuts.
- Bubble menu on selection: formatting, link, Comment, and AI ▾ brushes — Improve / Expand / Shorten / Simplify / Fix errors / Humanize / Custom — each showing an Original/Proposed preview with Accept/Reject (a revision is saved before Accept).
- Slash menu: searchable and grouped, with English and 中文 aliases.
- Blocks: paragraph, H1–H3, quote, bullet/numbered lists, divider, scene break (
***), images, footnotes.- Images can be uploaded, pasted or dragged in. They are stored in
data/uploads/and served by the API. - Footnotes are numbered and edited below the scene.
- Images can be uploaded, pasted or dragged in. They are stored in
- Marks: bold, italic, underline, strike, inline code, and links (Ctrl+K popover).
- Markdown input shortcuts.
- Paste:
- Pasting from Word, Google Docs or the web keeps structure and strips styling.
- Plain-text paste with Ctrl+Shift+V.
- Find & replace in the current scene, with match-case and whole-word options.
- Right panel (resizable and collapsible):
- Outline built from headings
- Version history: autosave snapshots throttled to 10 min, snapshot when you leave a scene, named versions (Ctrl+Alt+S), preview, and restore that saves the current text first
- Bible mentions (hover card, pop-up editor; per-entry voice feeds the AI brushes)
- Comments: anchored to quoted text, survive edits, Resolve/Reopen
- Assistant: brush mirror + Tells sub-tab (AI-tell list, Fix / Ignore / Disable / Humanize this sentence) with the zh-TW Taiwan wording pass
- Chat: talk with the AI about the book (threads, streaming, context chips)
- Live word counts for selection, scene, chapter and book. CJK text counts one word per character.
- Distraction-free mode (Esc to exit), with paragraph focus dimming, and full screen.
- View options: width, font (including Noto Serif/Sans TC), size, line height, theme, focus dimming, browser spellcheck toggle (
lang=zh-Hant-TWfor Chinese projects). - IME-safe: no autosave, menus or hover cards while an IME composition is in progress.
- Autosave: 1.5 s after you stop typing, at most every 10 s. Uses revision checks, so a conflict creates a "Conflict copy" version and reloads. If the API is unreachable, the text is backed up in localStorage. Status indicator, undo/redo.
- Storage: scenes are stored as TipTap JSON (canonical) plus derived plain text and word count. Legacy v0.1 Markdown bodies are migrated when opened, and the original is kept as a version.
D. Story Bible v1 (spec 13)
- Scope:
- One Bible per series, shared by its books. A standalone book has its own.
- Attaching a book to a series merges its entries into the series Bible; same-name entries are merged.
- Per-book overrides (summary, AI context) and "only in these books" scoping.
- Entry types: Characters, Places, Factions, Items, Lore, Events/Timeline, Terms, Themes, plus custom types.
- Entry fields: name, aliases (with kind and a detect toggle; Chinese names supported), summary, rich-text notes, type-specific fields, tags, and AI-context mode (always / when mentioned / never; default is when mentioned).
- Relationships are
relates_toedges with a label (SQLite edge tables since v0.48). - Bible pages: type rail, search/filter, entry editor, relationships, and an "appears in" list.
- Mention detection:
- Plain substring match on names and aliases, longest first. Latin names match on word boundaries; CJK names need at least 2 characters.
appears_inedges are recomputed when a scene is saved.- In the editor, mentions are highlighted with a hover card.
- Import/export as Markdown with YAML front matter, or as JSON.
- Auto-extract (propose-only): the AI suggests entries with verbatim evidence quotes from a scene; nothing is added until you Accept (Edit & accept / Reject available).
POST /api/ai/context/previewreturns the shape of the "included context" (always + mentioned entries) for AI work.
Not yet (planned)
Humanize chapter runs beyond the current targeted/full pass (deletion-scoped reviews stay limited), Plan/Revise/Sources views, suggestion/track-changes mode for typed edits (spec 15 "Should"; needs W-010), Styles + Uploads/RAG, Continuity, fact-check, share links, PDF/fixed-layout export, IndexedDB offline queue, real-time collaboration. (Pen names, Publish/EPUB, Backup incl. password encryption, Themes incl. custom editor, AI chat, Humanize, comments, command palette, and full i18n UI are shipped — see TRACKING.md for the release log.)
Autosave, revisions and backups
Autosave and "7 revisions of undo"
- Scenes save as you type (after a 1.5 s pause, at least every 10 s; delay adjustable in Settings › General). Unsaved text is kept in the browser if the server is unreachable; edits from another tab produce a "Conflict copy".
- Ctrl/Cmd+Z undoes individual edits (200 steps per session).
- Each scene keeps its newest 7 revisions (Settings › General › Revisions, default 7). A revision is saved when you pause typing for 2 minutes, when you leave the scene, at most every 10 minutes of continuous writing, and before a restore. Older ones are removed automatically.
- Editor › History (side panel): ◀ Previous revision / Next ▶ or Ctrl/Cmd+Alt+Z / Ctrl/Cmd+Alt+Shift+Z show older revisions as a read-only preview; Restore this brings one back (the text it replaces becomes a revision, so it can be undone). Save version… (Ctrl+Alt+S) or Keep (name it) makes a named version that never counts toward the 7 and is never removed.
Backups (Settings › Backup)
- Default: on, daily at 03:17 (AMD time), keep the newest 14 archives in
data/backups/, skip the scheduled run when nothing changed. Run backup now makes one immediately. - Each archive
writer-backup-YYYYMMDD-HHMMSS.zipcontainsmanifest.json(counts + SHA-256 of every file), the whole database (database/writer.sqlite, taken with the SQLite backup API with sessions stripped; pre-v0.46 archives have adatabase/writer.surqlSurrealDB export instead and can no longer be restored by Writer),uploads/, every book as folders of Markdown + TipTap JSON (with each scene's revisions), and each Story Bible as Markdown/JSON. Every backup is verified (integrity_check+ row counts) before it is accepted. - Not included:
.envandWRITER_ENCRYPTION_KEY. Saved AI keys are inside the archive but encrypted; to use them after restoring (especially on another machine) the server needs the sameWRITER_ENCRYPTION_KEYand the samedata/.writer_salt(also not in the archive; it is a random per-install salt, not a secret on its own, but without it v2-encrypted keys cannot be read — re-enter them in Settings › Models if lost). Keep a copy of both somewhere safe (password manager), separately from the backups. - Optional archive password (Settings › Backup › Archive password): new archives are AES-256-encrypted before upload (
*.zip.enc). The password is never stored in the archive — you type it to Restore or Verify (so a fresh machine needs only the password). On AMD it is kept encrypted withWRITER_ENCRYPTION_KEY. - Optional copy to a folder (USB disk, Syncthing/Dropbox folder) or a cloud drive via rclone (
~/.local/bin/rclone), each with its own keep-N. - On AMD (since 2026-10-09): backups are copied to Google Drive — remote
gdrive(scopedrive.file) → My Drive/Writer-Backups, newest 14 kept. Since 2026-10-12 the remote uses Wing's own Google OAuth client (Cloud projectwriter-app-backup, Desktop client, Testing mode with Wing as test user; W-072). Testing mode can expire the sign-in after ~7 days: if backups fail with an auth error, runrclone config reconnect gdrive:on AMD and sign in again. Because of thedrive.filescope, backups made with the old shared client live in a separate olderWriter-Backupsfolder that this client can't see or prune; delete it by hand once enough new copies exist.
Connect Google Drive (one time)
- At AMD's desktop, open a terminal and run:
A browser opens on AMD → sign in to Google → Allow → the page says Success!; rclone saves the remoterclone config create gdrive drive scope=drive.filegdriveto~/.config/rclone/rclone.conf(Writer never stores the token).scope=drive.filemeans rclone can only see files it creates, not the rest of your Drive.- Over SSH instead:
ssh -L 53682:127.0.0.1:53682 wing@AMD, run the same command, and open thehttp://127.0.0.1:53682/auth?...link it prints in your laptop's browser. - Or on any computer with a browser and rclone:
rclone authorize "drive" "scope=drive.file", then paste the printed JSON in Settings › Backup › Cloud drive › Not sitting at AMD? › Paste token.
- Over SSH instead:
- Settings › Backup › Copy backups to: Cloud drive → Refresh → remote
gdrive, folderWriter-Backups→ Test → Save settings. Each new backup is then uploaded to My Drive/Writer-Backups and only the newest N are kept there.
- OneDrive:
rclone config create onedrive onedrive; Dropbox:rclone config create dropbox dropbox.
Restore
- Settings › Backup › History › Restore (or Backups there… → Restore… to pull one from the folder/cloud first, or Upload a backup… for a zip on your computer). Type
RESTOREto confirm. Writer verifies the archive (checksums + scratch import), takes a safety backup of the current data, then replaces the database and uploads; if the import fails, the safety copy is put back. - Verify checks any archive without changing anything. Download saves the zip to the device you're using.
- Manual restore on a fresh install: stop the API, copy the archive's
database/writer.sqlitetodata/writer.sqlite(start the API once so Alembic verifies the schema), copyuploads/*todata/uploads/, and put the oldWRITER_ENCRYPTION_KEYin.env. Pre-v0.46.surqlarchives can no longer be restored (W-193); the final SurrealQL export of the live data isdata/pre-sqlite/writer-surreal-final-20261027.surql.
Pen names + Publish (EPUB export)
Specs: Specs/17-pen-names.md, Specs/18-publish-export.md (store requirements with sources).
Pen names (Settings › Pen names)
- A pen name holds a display name, a sort name ("file-as", e.g. Kirk, Ada; CJK names stay as written), language, bio, an optional photo, website, default publisher/imprint and copyright holder (which can be your legal name).
- Assign one per series on the Series overview. Each book inherits it and can override it on its Overview or Publish screen ("Inherit from series (…)").
- Exports use it for
dc:creator(+file-as, roleaut), the title page, the copyright line (© year holder), About the author (bio, photo, website) and Also by (your other books under the same pen name). - Home and Projects can be filtered by pen name. Pen names are included in backups (and in the readable export as
pen-names/*.md).
Publish (book › Publish, sidebar, or Publish in the binder header)
- Metadata: title, subtitle, series name and number, pen name, language (BCP 47:
zh-Hant,zh-Hans,en…), description, keywords, publisher, publication date, optional ISBN-13 (checksum checked), and rights. - Cover: JPEG/PNG upload; target 1600×2560 JPEG; per-store warnings; JPEG for store upload downloads a store-ready copy. A downscaled copy (≤ 4 MP) goes inside the EPUB.
- Front & back matter toggles: cover, title page, copyright (front, or back as a 版權頁, which is the default for Chinese), dedication, contents (optionally with scene titles under each chapter), Index (Story Bible names → linked chapter numbers, off by default), about the author, also by. Genre and audience (dropdowns on the project page and new-project wizard) go into the EPUB/DOCX metadata in the book language (spec 25). In the editor, select text and right-click › Add to Index… (or the book icon in the selection bubble, or Ctrl/Cmd+K) to mark an index term with an optional heading; right-click a marked term to remove it. Shift+right-click shows the browser's own menu. Under Front & back matter, Contents & Index — preview and edit shows both exactly as the export builds them; rename or hide Contents entries (the chapter keeps its title), rename/merge/hide Index headings (hiding a Bible name only removes it from the Index), add see-also, and pick 注音 sort for Chinese books. Save settings applies them to EPUB and Kindle DOCX.
- Layout: horizontal or vertical 直排 (
writing-mode: vertical-rl, right-to-left page turns); scene-break style (◇ ◇ ◇, * * *, ⁂, ❦, or blank); chapter headings (title, 第一章 + title, or number only); indented or spaced paragraphs. - Leave a scene, chapter or part out with the eye button on its binder row. Comments, highlights and Bible mentions are never exported, and empty chapters are skipped.
- Preflight: pass/warn/fail per store (Google Play Books, Apple Books, Kindle, Kobo) for metadata, cover, layout, content, size and the last EPUBCheck run.
- Export:
- one universal EPUB 3 (with an NCX for older readers, no JavaScript,
langon every file, embedded images, and footnotes as EPUB footnotes); a vertical EPUB as a second file; - Kindle DOCX, because KDP takes Traditional Chinese only as Word files, laid out horizontally.
- Every EPUB is validated with EPUBCheck, and the result is shown in the UI. Files are kept in
data/exports/<project>/(gitignored), with a downloadable history.
- one universal EPUB 3 (with an NCX for older readers, no JavaScript,
- EPUBCheck 5.4.0 is installed without sudo: a portable Temurin 21 JRE in
~/.local/opt/jre, EPUBCheck in~/.local/opt/epubcheck, and the wrapper~/.local/bin/epubcheck. Override withEPUBCHECK_BIN=…. If it is missing, Writer falls back to a basic structural check and says so. - Offline self-test of the builders (every TipTap node; zh-Hant horizontal and vertical, plus English):
cd backend && .venv/bin/python -m app.publish.selftest /tmp/writer-selftest
Per-store upload notes
| Store | Upload | Chinese / vertical | Cover | Notes |
|---|---|---|---|---|
| Google Play Books (Partner Center) | The universal EPUB | zh-Hant/zh-Hans are fine; vertical layout isn't documented, so check the Play Books preview | Must be inside the EPUB (it is); ≥ 640 px on the short side; also upload the JPEG | No JavaScript; < 2 GB; no ISBN needed (Google assigns a GGKEY) |
| Apple Books (Apple Books for Authors / Transporter) | The universal or the vertical EPUB | Language must be zh-Hant / zh-Hans; EPUB 3 is required for Chinese; vertical is supported |
≥ 1400 px on the short side; upload the JPEG separately | No prices, links to other stores or "available on Kindle" text anywhere in the book; ISBN optional |
| Amazon KDP | Kindle DOCX for Traditional Chinese (beta); the EPUB for English | Traditional Chinese only, horizontal only, from DOC/DOCX; no Simplified Chinese | Upload the JPEG separately (ideal 1600×2560, ≥ 625×1000, < 50 MB) | AI-generated content must be disclosed at upload; check the book in Kindle Previewer |
| Kobo Writing Life | The universal or the vertical EPUB | Supports writing-mode and right-to-left page turns |
Cover page + meta name="cover" are included; upload the JPEG too |
< 100 MB; Kobo may reject AI-generated or public-domain content; ISBN optional, but some partner stores need one |
Writer can't do these for you: store accounts, tax and payment details, pricing, territories, DRM choices, store categories (BISAC), the AI disclosure, buying an ISBN (in Taiwan, ISBNs are free from the 國家圖書館 ISBN Center), a print/PDF edition, and fixed-layout books.
Themes and focus (spec 19)
Nine color themes tuned for long writing sessions, plus a custom theme editor (Settings › General: pick a base theme, edit the core colors — highlights, tooltips and selection colors are derived; saved per device and applied without a flash). All built-in themes meet WCAG AA contrast for body text and avoid pure white and pure black:
| Theme | Page / ink | When to use it |
|---|---|---|
| 和紙 Japanese paper (default light) | #F6F1E3 書籍用紙 cream / #2B2722 soft sumi; the chrome is a cooler grey-cream so the page stands out |
Everyday writing |
| 褐 Sepia | #F1E4C9 / #55422E |
Warmer, lower contrast for long sessions |
| 白 Plain light | #FDFDFC / #1F2430 |
The previous neutral white look |
| 夜 Night | #16120E / #D8B686 amber |
Late night, with no blue tones |
System (the default setting) follows your OS: light uses Japanese paper and dark uses Night. (Soft dark and High contrast were removed in W-109; saved choices move to Night and 和紙.)
- Switch themes from the sidebar Theme button (a menu with swatches), from Settings › General › Appearance (live preview cards), or from the editor's View options. The choice is saved per device and applied before the first paint, so there is no white flash.
- Apply to editor page only keeps menus and panels on the System default and uses the chosen theme only on the writing page.
- Focus dimming: everything except the paragraph with the caret fades. The side panels fade too and come back when you point at them. You can set it to In focus mode (the default, Ctrl/Cmd+Shift+F), Always, or Off, and adjust the strength. Optional typewriter scrolling keeps the current line near the middle of the screen.
- Exported EPUB and DOCX files never use these colors. Their CSS is generated by the backend.
Layout
backend/app/ FastAPI: routers/, ai/ (Open Notebook-derived provider layer), bible.py, seed.py
frontend/src/ Next.js App Router: app/(app)/…, components/{editor,bible,settings,home}
scripts/ start-* / build-web / deploy / check-i18n.py / e2e_*.py
Specs/ product specs (each spec banner shows its own as-built status)
data/ writer.sqlite (live DB), surreal/ (retired cold archive, W-193), uploads, backups/, exports/, backup-settings.json, logs (gitignored)
Credits
- Open Notebook (MIT, © 2024 Luis Novo): the provider registry, connection tester, model discovery, the credential/model API shapes, and the settings UI are adapted from it. See NOTICE.
- Esperanto (MIT): AI provider abstraction.
- TipTap open-source packages (MIT): editor.
- lucide-react (ISC): icons.
Access from other computers
The web app listens on all interfaces at port 3002 (WEB_HOST, default 0.0.0.0); the API listens on 127.0.0.1:5056 only (API_HOST, default 127.0.0.1, W-027). The browser talks to one origin: it loads pages from 3002 and calls /api/* on that same host, and the Next server proxies those calls to the loopback API (so a phone at http://100.79.93.36:3002 works without ever reaching port 5056, and 127.0.0.1 on a phone is never used). The database never leaves the machine: SQLite is a local file.
- LAN:
http://192.168.200.206:3002 - Tailscale:
http://100.79.93.36:3002(orhttp://amd:3002with MagicDNS)
If the LAN address doesn't load from another machine, allow the ports in the firewall: sudo ufw allow from 192.168.200.0/24 to any port 3002 proto tcp (5056 needs no rule — it is loopback only).
Set WRITER_PASSWORD in .env before exposing Writer beyond this machine — anyone on the LAN/Tailnet who can reach port 3002 can otherwise use the app. To keep it local-only, start with WEB_HOST=127.0.0.1.
Clone the repo from another machine
Forgejo itself stays on 127.0.0.1:3001 (HTTP) and 127.0.0.1:2222 (built-in SSH). A user service, forgejo-tailnet, forwards those ports on the Tailscale address only (deploy/tailscale-forward.py, unit in deploy/systemd/). The LAN address does not answer. tailscale serve was not used because it needs root.
- Web:
http://amd.tail19c69c.ts.net:3001/(alsohttp://100.79.93.36:3001/) - On AMD,
http://127.0.0.1:3001/still works, and theforgejo-amdSSH alias (127.0.0.1:2222) is unchanged.
To clone or push from AMD255 or the NUC, add that machine's SSH public key in Forgejo (avatar → Settings → SSH / GPG Keys). Then:
git clone ssh://wing@amd.tail19c69c.ts.net:2222/wing/Writer.git
HTTPS also works if you create a token in Forgejo (Settings → Applications) and use http://amd.tail19c69c.ts.net:3001/wing/Writer.git. No keys were added for other machines.