About
How Mossnote works
A codebase overview for the curious and for contributors, covering the architecture, the main areas of functionality and how this site is deployed.
This page is an overview of how Mossnote is built, based on the source in the repository. It is not needed to use the app. For contributing, the repository's CONTRIBUTING.md is the authority.
The shape of it#
Mossnote is a single npm package (mossnote, Node 22.14 or newer) written in TypeScript. The same app and the same services run in two places:
- The computer version: a small Hono web server bound to the loopback address, a SQLite database per journal, and the React app served from the same process.
- The web version: the same server code compiled into a web worker in the browser, using SQLite compiled to WebAssembly, with the React app talking to it as if it were a server.
Only the parts that touch files differ, behind two small interfaces: Sqlite (the connection) and Storage (where journals and snapshots live).
| Folder | Contains |
|---|---|
src/shared | Schemas (zod), game dates, constants and limits, the search-query parser, templates. No Node or browser APIs. |
src/server | The Hono API, SQLite access (Drizzle ORM plus hand-written SQL where needed), migrations, snapshots, the journal manager, phone access. |
src/standalone | The server's app running in a browser worker: SQLite WebAssembly, the browser's storage and snapshots. |
src/web | The React app: routes, features, components, styles. |
Other folders: drizzle/ (SQL migrations), scripts/ (build, dev, packaging and these docs), tests/ (Playwright end-to-end tests), bin/ (the mossnote command).
Data model#
- Records: notes, people, plantings (farm entries), maps with markers, tags, links, settings. Identifiers are ULIDs created in the browser, which makes a retried save safe.
- Deletes are soft. A
deleted_attime moves a record into Recently deleted; a purge removes records after 30 days. - Links join a note to another note, person or planting. Map markers can target the same three.
- Dates are one integer,
year × 10000 + season × 100 + day, which sorts correctly. A day counter is a calendar of one 99-day "season". - Settings hold the calendar, layout (template, section order, names, hidden sections, quick actions), preferences and marker types, as JSON.
- Search uses SQLite's FTS5 full-text index (a hand-written migration), plus a vocabulary table for typo tolerance. The query language is parsed by one shared function so the server and the screen agree.
- Maps are stored as a versioned scene (layers and shapes) plus marker rows, with saved versions.
The API and its safeguards#
The web app talks to a JSON API under /api: health, journals, settings, notes, people, plantings, maps, tags, field-labels, search, pick (the typeahead lists), data (export, import, snapshots), trash and phone. Requests are validated with zod and errors share one shape.
- The server listens on the loopback address only, checks the
HostandOriginheaders, requires a custom header on every write, and sends strict security headers including a Content-Security-Policy with a fresh nonce per response. - Phone access adds a gate: a device without a paired session cookie is refused or sent to
/pair. See Phone access. - SQLite runs in WAL mode with foreign keys on. A lock file keeps two copies from opening one journal, an integrity check runs at start, and snapshots can be taken while the app runs.
- An automated check fails the build if the source or the built output contains a web address that could be fetched. The web version's tests also block every non-local request.
The React app#
- Routing: React Router with the routes listed in App addresses; Settings, Search and the map editor load on demand.
- Data: TanStack Query caches API results; saves are optimistic where it is safe.
- Features live in
src/web/features/<name>: calendar, capture (the note box and its pickers), farm, journal, journals, maps, notes, people, search, settings, tags, today. - The map editor is the largest feature: an SVG canvas with a pure-function engine (shapes, snapping, alignment, recognising rough strokes, measuring) and an editor around it (toolbar, panels, shortcuts, history).
- Interface: Radix primitives for dialogs, menus and popovers, Tailwind CSS with design tokens in
tokens.css, bundled Inter and Literata fonts, no icon library. Every template picks a tint. - Accessibility: lint rules for accessibility, and Playwright tests that run axe scans on the main screens in both themes.
Templates in code#
A template is an object in src/shared/templates: an id and name, a tint, a one-line description, names for each of five sections, the sections it uses in order, a calendar kind, quick actions and suggested tags. Two exist. Nothing in them may carry game facts. A journal stores the template id in its layout setting.
The web version#
src/standalone/worker.tsruns the server app and a SQLite WebAssembly build in a worker;src/web/standalonestarts it before showing the app and routes API calls to it.- Journals live in the browser's origin-private storage (or memory when that is unavailable); a Web Locks lock allows one tab at a time.
- A service worker keeps every file of the app for offline use and switches to a new copy only when the page asks.
- A manifest and icons make it installable. The build also writes
404.htmlas a copy of the app so an address typed into a static host still opens it.
Tests and checks#
npm run check runs lint, type checking and the unit, integration and component tests (Vitest). Further: npm run test:wasm runs the server's tests on SQLite WebAssembly; npm run test:e2e builds and runs Playwright end-to-end tests against the real server and the web version; npm run check:network looks for outbound URLs; npm run verify:pages builds the web version for a GitHub project site and tests it the way Pages serves it. CI runs these on Node 22, 24 and 26, installs and runs the finished npm package on Windows, macOS and Linux (npm run package:npm-smoke), and runs WebKit tests on macOS.
Deployment and these docs#
The Web version workflow (.github/workflows/pages.yml) runs on every push to main and can be started by hand from the Actions tab. It runs the tests, builds the web version with MOSS_BASE=/<repository>/, runs the outbound-URL check, then builds the documentation into the same folder and checks the finished site: every address stays under the base path, 404.html is the app, and the offline file list matches the build. A browser test then opens the site through a small server that answers like GitHub Pages and uploads dist/standalone only if all of that passed. So:
- the app is at
https://<owner>.github.io/<repository>/, and - these pages are at
https://<owner>.github.io/<repository>/docs/.
The documentation is plain HTML with no framework. Pages are fragments in docs/content; npm run docs:build (scripts/docs/build.ts) wraps them in one layout, writes relative addresses (so any base path works), builds the contents list and a small search index, and copies the images and recordings. The app's service worker deliberately lets page loads under docs/ go to the network, so the documentation is never replaced by the app's own page. The screenshots and recordings were captured from the running app with a separate script that is not part of the repository. See About these docs.