Surfaces
Express middleware v0.3.1
A zero-dependency, dev-only review-notes overlay for Node. Mount one middleware and a floating Red Pen button appears on your pages - leave typed notes, pin them to elements, and they persist to an append-only log in your repo that you are meant to commit.
Red Pen for Express is the web sibling of the WordPress plugin. It works in any Express app, and in any Connect-style app that uses the same middleware signature. It has no database, no build step, and no runtime dependencies - it ships as plain Node and serves its overlay client as a single script. Free, MIT. Dev credit: Lincoln Tracy.
NODE_ENV === 'production', and separately it answers only connections from this machine - localOnly is a real check on the connection's remote address, not a guess from an environment variable. It is a tool for the people building the app, never for its visitors, so it is safe to leave mounted.
How it works
The middleware does two things. First, it watches outgoing HTML responses and injects a small overlay script tag before the closing body tag, so every page you serve gets the Red Pen button without you editing any templates. Second, it mounts a tiny notes API under a URL prefix (default /__redpen) that the overlay talks to. Notes are appended to a log in your project - .redpen/notes.jsonl, one record per line - and a derived .redpen/notes.json snapshot is written beside it for tools that read notes off disk. There is no other moving part.
Install
Red Pen for Express is not published to npm. The source lives on GitHub at github.com/LTracy86/red-pen-express - clone it somewhere near your projects, then install it into an app as a local file: dependency:
# clone once, next to your apps
git clone https://github.com/LTracy86/red-pen-express.git
# from the app you want to annotate - path is relative to the app
npm install ../red-pen-express
That writes a "red-pen-express": "file:../red-pen-express" entry into the consuming project's package.json, which resolves as a live symlink. Because it is a symlink rather than a copied package, pulling a new Red Pen version flows into every app that links it the next time that app restarts - there is no per-app reinstall step. You can also use npm link if you prefer a globally linked package.
Mount it first
Add the middleware right after you create the app, before static file serving, compression, or your routes. Mounting it early ensures the overlay is injected into responses and the notes API is reachable. Both module systems are supported.
CommonJS (require)
const express = require('express');
const redPen = require('red-pen-express');
const app = express();
app.use(redPen({ file: __dirname + '/.redpen/notes.json' }));
// ...your routes...
ES Module (import)
import redPen from 'red-pen-express';
const app = express();
app.use(redPen({ file: here + '/.redpen/notes.json' }));
In an ES Module there is no __dirname by default - derive a directory variable (for example with fileURLToPath(import.meta.url)), or omit file entirely to default to the current working directory.
.redpen/ to your project's .gitignore. Do the opposite: commit .redpen/notes.jsonl. It is an append-only log, and Red Pen ships a .redpen/.gitattributes marking it merge=union so two people on two branches cannot corrupt each other's notes. A nested .redpen/.gitignore keeps the derived snapshot and the migration backup out of your commits for you. If the old rule is still in your project's .gitignore, the store asks git check-ignore on open and prints the offending file, line and pattern - it does not edit that file, because that file is yours.
Options
| Option | Default | Description |
|---|---|---|
mount | /__redpen | URL prefix for the notes API and the overlay script. |
file | ./.redpen/notes.json | The snapshot path. The log lives beside it as notes.jsonl and is the real store. |
enabled | NODE_ENV !== 'production' | Master on/off switch for the whole middleware. |
inject | true | Auto-inject the overlay into HTML responses. Set to false to add the script tag yourself. |
localOnly | true | Serve only connections from this machine. Set to false to let someone else on the network use the same overlay - three people around one staging box. |
author | from git | The name notes and replies are signed with. Seeded from git config user.name, falling back to the OS username; each browser can override it from the name chip in the panel header. |
If you turn off auto-injection, include the overlay manually where you want it:
<script src="/__redpen/widget.js" defer></script>
Using the overlay
Start the app as usual. A red Red Pen button appears in the bottom-right corner. From there you can:
- Leave a typed note - choose a type (note, idea, problem, or question) and a priority, then write the note.
- Pin to an element - click Pin, then click any element on the page. The note is anchored to that element by a CSS selector plus a relative position. Numbered red markers render over the pinned elements and follow the page on scroll and resize.
- Locate - jump from a note back to its pinned element; the page scrolls to it and the element flashes.
- Move it through the workflow - Start and Stop move a note between Open and In progress, and Resolve closes it. Three tabs, one per state.
- Sign your notes - the name chip in the panel header sets who this browser writes as. It starts from your git identity, so a shared repo already reads correctly.
- The repository - the panel header links to "All notes" at
{mount}/repo, a standalone page listing every note across every page with filters and bulk actions. - Dark mode and a resizable panel - a moon/sun toggle in the panel header, and a drag handle on the panel's left edge. Both preferences are saved to
localStorageand restored next time.
What is new in v0.3.x
v0.3.0 is the release that makes notes shareable, and v0.3.1 is the pass that made three of its headline claims actually true. The short version:
| Change | What it means for you |
|---|---|
| Append-only JSONL store | .redpen/notes.jsonl, one record per line, shipped with merge=union. A pretty-printed JSON array corrupts under a real two-branch merge; a log does not. Reading replays the log, newest wins per field. |
author on notes and replies | Seeded from git config user.name. No accounts, no user table - just a name on the note, which is what a second person needs. |
| Three-state lifecycle | open, in_progress, resolved, with Start and Stop in the panel and on the board, amber cards and pins for in-progress, and a tab per state. |
| A real localhost gate | localOnly checks the connection's remote address instead of trusting NODE_ENV alone. |
statusAt, resolvedAt, typeLabel | The cross-surface contract fields. The Hub force-reverts a surface that does not send statusAt; Express used to be one. |
| Capability declaration | GET {mount}/capabilities tells the Hub what this surface does not implement, so the board can hide a control instead of writing a value that would be ignored. |
| Git-ignore warning (v0.3.1) | If your .gitignore still hides .redpen/, the store says so on open, with the file, line and pattern to change. |
Carried over from v0.2.0
Version 0.2.0 was the "notes UX parity" milestone - it brought the overlay's day-to-day note handling in line with the WordPress plugin. Every item below is still in the current release.
| Feature | What it does |
|---|---|
| Reply thread + inline form | Each note renders its full reply thread plus a toggleable inline reply form, so a note can become a short conversation. |
| Edit a note after adding | Load a note back into the form to change its body, type, or priority. You can re-pin it to a different element or clear its anchor. The store stamps an editedAt timestamp on field edits. |
| Status tabs | The old single flat list is replaced by tabs with live counts. Open is the default view. v0.3.0 added the In progress tab between the two. |
| Error toasts on failed saves | A save that fails (network error or a non-2xx response) now surfaces a red toast instead of failing silently. Reads stay quiet. |
| Persistent add-form prefs | The add form remembers your last-used type and priority across sessions (stored in localStorage), so repeated notes of the same kind are faster to file. |
| Undo toast on resolve | Resolving a note shows an Undo toast; clicking it reopens the note. The toast only appears after the server confirms the resolve. |
Storage
All notes for an app live under .redpen/ at the project root, in two files that do different jobs:
| File | What it is | Commit it? |
|---|---|---|
notes.jsonl | The store. An append-only log, one JSON record per line - id, body, type, priority, status, url/page, anchor, replies, author, and timestamps. Nothing is edited in place; reading replays the log, newest wins per field. | Yes. That is what merge=union is for. |
notes.json | A derived snapshot of the log, written atomically for tools that read notes off disk - the Hub reads exactly this file. A status the Hub writes into it is folded back into the log rather than silently reverted. | No - it is gitignored for you. |
Both are plain text you can inspect, back up, or wipe by hand. An older array-format notes.json is migrated to the log on first open, with the original kept as a .pre-jsonl-*.json backup. See Core concepts for the full note model.
The notes API
The overlay talks to a small REST API under the mount prefix. You rarely call it directly, but it is documented so you can script against it if needed.
| Route | Purpose |
|---|---|
GET {mount}/notes?page=<path> | List notes, optionally scoped to one page. Also returns the project's default author and the capability block. |
GET {mount}/capabilities | What this surface implements, so the Hub can hide a control rather than write a value that would be ignored. |
POST {mount}/notes | Create a note: { body, type, priority, url, page, anchor }. |
PATCH {mount}/notes/:id | Update a note: { status | body | priority | reply }. |
DELETE {mount}/notes/:id | Delete a note. |
GET {mount}/widget.js | The overlay client script. |
Seeding it across many apps
Because the package installs as a live symlink, Red Pen is meant to ride along in every local app you build. The pattern is always the same three steps: install the local dependency, mount the middleware first, and make sure that app's .gitignore does not hide .redpen/ - the notes are meant to be committed. Once linked, every future Red Pen release upgrades all of those apps on their next restart, with no reinstall.
Try the demo
npm install
npm run example # http://localhost:4000
The demo serves a small page with the overlay injected, so you can exercise the full loop - add, pin, reply, edit, start, resolve, reopen, delete - against a real .redpen/notes.jsonl.