Project
Build a surface
Red Pen has five official surfaces, but the idea is bigger than five tools. If you review work somewhere - a game engine, an image editor, a video NLE, a code editor, a DAW we have not met - you can build a surface for it, keep your notes local, and roll them onto the same Hub board as everything else. This page is the map: the model to implement, the two ways to talk to the Hub, and the conventions that make a surface feel like family.
What a surface is
A surface is anything that lets someone leave a typed review note where the work happens and stores it locally. That is the entire definition. The five official surfaces are just five answers to the same question - "where do notes live in this tool?": a custom post type in WordPress, a JSON file next to an Express app, localStorage on a static page, a sidecar file beside a .blend, a per-Set store in Ableton.
Good candidates are tools with a reviewable artifact and a place to hang UI: game engines (Godot, Unity), image editors (GIMP, Krita), video editors, IDEs and code editors, 3D and CAD packages, other DAWs, note-taking canvases. If it produces work someone reviews, it qualifies.
Start from the shared model
The family is spec-driven, not shared code: each surface implements the same note shape in its own native idiom. The full model lives on the Core concepts page; the subset the Hub cares about is small:
| Field | Type | Meaning |
|---|---|---|
id | string | Opaque, stable, unique within your store. The Hub matches notes by it across pushes and pulls. |
body | string | The note text, plain. |
type | string | note | suggestion | bug | question, or a custom ct_* slug. Stored value is the slug; displayed value is the label (Note / Idea / Problem / Question). Send typeLabel (and optionally typeColor) with custom types so the board renders them properly. |
priority | string | low | normal | high. |
status | string | open | in_progress | resolved. Three states, no more - resolved notes leave the active list, in-progress renders amber, open renders red. The spec's middle value is in_progress; the Hub also accepts progress, which some of the official surfaces still store internally. Emit in_progress in new work. |
page / url | string | Where the note lives in your tool's terms - a page path, a file name, a Set name. The board shows it and groups by it. |
anchor | string | Optional short human-readable label for what the note is pinned to ("Cube", "Frame 120", "#signup-form"). Your surface can keep richer anchor data in extra fields - the Hub ignores what it does not know. |
createdAt | string | ISO 8601, in UTC - end it with Z. |
statusAt | string | Send this one. ISO 8601 UTC, stamped every time a note's status moves, in either direction. It is how the Hub decides who acted last when a status is changed on the board and on your surface. A surface that omits it gets its changes force-reverted by one stale board-side click, and a stamp the Hub cannot parse counts as absent. If a note's status has never moved, falling back to createdAt is fine. |
resolvedAt | string | ISO 8601 UTC, set on resolve and cleared on reopen. Fills the board's Resolved column, which is otherwise permanently blank for your surface. |
Extra fields are fine and encouraged (the official surfaces all carry platform-specific anchor data); unknown fields ride along untouched. Severity, assignee, and replies are also understood by the board if you have them - see the spec (RED-PEN-SPEC.md in the red-pen-express repo) for the full contract.
Two ways to connect to the Hub
1. The sidecar file (simplest - no code talks to the Hub at all)
Write your notes to a .redpen/notes.json sidecar next to the project the notes are about, in the shared envelope:
{
"version": 1,
"surface": "your-tool-slug",
"notes": [ { "id": "...", "body": "...", "type": "note", "priority": "normal",
"status": "open", "page": "scene-3.blend", "anchor": "Cube",
"createdAt": "2026-07-22T14:00:00Z", "statusAt": "2026-07-22T14:00:00Z" } ]
}
Note the Z. Stamp times in UTC - the Hub compares them as instants, and a stamp it cannot parse is treated as absent rather than trusted. The "surface" key is not decoration either: a scanned store is badged with the surface it declares, and a store that declares nothing falls back to a guess from the folder.
That is a complete Hub integration. The Hub's folder scan (board > Sources > scan) finds .redpen/notes.json files and adds them as file sources; the board reads them directly, and status changes made on the board (resolve / start / reopen) are written back into your file. Your surface just re-reads the sidecar to pick them up. No HTTP, no token handling, no network code.
2. HTTP push (for surfaces that cannot share a filesystem with the Hub)
POST your whole notes array to the Hub's ingest endpoint. This one endpoint authenticates with the connect token in the JSON body (so browser surfaces can push cross-origin); the user copies the token from the board's Connect a Site panel:
POST http://localhost:3900/api/ingest
Content-Type: application/json
{ "token": "<connect token>", "project": "My Project",
"surface": "your-tool-slug", "notes": [ ... ] }
The Hub self-registers a connected store for the project on first push - no setup step on the board. Pushing is full-state: send every note each time, and the Hub reconciles (board-side status changes it has recorded are re-applied, so a push never silently reverts a resolve made on the board).
For two-way sync, also pull: GET /api/notes with the token in an x-hub-token header (every endpoint except ingest authenticates via that header), match returned notes to yours by id, and apply the board's statuses locally. Pull before you push - otherwise a stale local state clobbers a resolve someone just made on the board.
A complete push client is small. The Blender add-on's is about sixty lines of stdlib Python (addon/hub.py in the red-pen-blender repo); in JavaScript it is one fetch:
await fetch(hubUrl + '/api/ingest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, project, surface: 'your-tool-slug', notes })
});
Best practices
These are the conventions the official surfaces follow. None are enforced; all of them are the difference between a surface that feels like family and one that fights it.
- Own your storage - never touch the user's files. Notes live in a sidecar, a database, or app storage. The Blender add-on writes nothing into the
.blend; nothing of yours should ship inside the artifact being reviewed. - Dev-only by default. Red Pen is for the people making the work, never its audience. Gate the surface the way your platform gates development: an environment check, a capability check, a developer-mode flag.
- Show only your own context. A surface lists the notes for the file, page, or project the user is looking at. Cross-project aggregation is the Hub's job - resist building a mini-board into the surface.
- Anchor by name, not by reference. Pin notes to stable names ("the object called Cube", "the element matching this selector"), not to live object handles - undo, reload, and rename survive names, not pointers. Detect and flag a lost anchor rather than crashing or silently dropping the pin.
- Degrade gracefully offline. The Hub is optional. Local notes must work with no Hub configured, and an unreachable Hub means a short timeout and an honest status line - never a blocked UI, never a lost note.
- No telemetry, no phone-home. The only network call a surface ever makes is to the user's own Hub, at the user's request. This is a family-defining commitment.
- Keep the vocabulary. Four built-in types with the family labels (Note / Idea / Problem / Question), three statuses, three priorities. Custom types carry
typeLabelandtypeColor. Brand red is#D32F2F, in-progress is amber, resolved is gray. - Stay light. The official surfaces are zero-dependency or close to it, and single-file where the platform allows. A review layer should never be the heaviest thing in the toolchain.
Naming and the nib
If you build one, call it Red Pen for <your tool> - that is what the name is for. The nib mark is in every official repo as SVG (and this page's favicon); use it so the surface is recognizable as part of the family. The one ask: keep it free and open, like the rest of the ecosystem. No paid Red Pen surfaces.
Reference implementations
- red-pen-static - a complete surface in one dependency-free file: overlay, storage, Hub push, pull-based two-way sync. The best single file to read first.
- red-pen-blender - the sidecar pattern plus a minimal stdlib push/pull client (
addon/store.py,addon/hub.py). The cleanest non-web example. - red-pen-express - the JSON envelope in its simplest server-side form (
src/store.js), plusRED-PEN-SPEC.md, the written family contract. - red-pen-hub - the board itself (
src/server.jsis the authoritative ingest/normalize contract).
The checklist
- Notes store locally in the shared envelope, in a sidecar or app storage - never inside the user's files.
- Types, statuses, and priorities use the family slugs and labels.
- The surface works fully with no Hub configured.
- Connected via sidecar (preferred where a filesystem is shared) or push, with pull-before-push if two-way.
- Dev-only gating on by default; no telemetry anywhere.
- Anchors are name-based and loss is detected, not fatal.