Everything we know about shipping orfc.
A working handbook for the people building, deploying, and reviewing on orfc — five sections, in-page navigation, no link jumping. Click an item in the sidebar to switch.
What's in here
Architecture. How the CLI, web app, sanitizer, and Postgres talk to each other — system diagram, stack, the four invariants we don't break.
API reference. Every endpoint orfc exposes — plans, comments, notify — with auth, status codes, and an end-to-end example.
Deployment. The pipeline from git push to orfc.dev, env vars, and the migration trip-wire that bit us once.
Changelog. Recent @orfc/cli releases.
"Documents that ship don't need to be markdown. orfc treats HTML as a first-class citizen — render it sanitized, preserve the author's theme, comment on it the same way you would on prose."
Architecture
A Next.js web app, a Postgres store, a TypeScript CLI, and a single sanitization choke-point that keeps user-authored HTML safe to render.
Stack
Frontend. Next.js 14, React 18, Tailwind. App Router with server components for the public read path, client components for the editor and comment UI.
Database. Postgres on Neon serverless, JSON-file fallback in local dev so the CLI stays demo-able offline.
CLI. Node 18+, Commander, TypeScript. Single binary at @orfc/cli. Reads ~/.orfc/config.json for credentials.
Auth + mail. NextAuth, Resend, Slack webhooks. Email-OTP login for both web and CLI.
Invariants
1. Every write goes through /api/plans. The CLI is a thin client.
2. Updates always snapshot the previous version into plan_versions — nothing is overwritten in place.
3. Sanitization happens at render, not at write. Stored content is the author's exact bytes; what the browser sees is filtered.
4. Comment anchors are DOM-based (block index + text snippet), so they survive the markdown ↔ HTML render-mode change.
API reference
Every endpoint orfc exposes. The CLI is built on these — there's nothing it can do that you can't reach with curl and a Bearer token.
Auth
Web requests use a NextAuth session cookie. CLI requests use a Bearer token issued by orfc login:
curl https://orfc.dev/api/plans -H "Authorization: Bearer orfc_live_…"
Plans
| Method | Path | Description |
|---|---|---|
| GET | /api/plans | List the caller's plans. Optional ?folder= and ?tag= filters. |
| POST | /api/plans | Create a plan. Body: title, content, contentType, accessRule, folderPath, tags. |
| GET | /api/plans/{id} | Fetch one plan. 401/403 if access rules don't match. |
| PUT | /api/plans/{id} | Update. Optimistic concurrency via expectedVersion. |
| DELETE | /api/plans/{id} | Author-only. Cascades. |
| GET | /api/plans/{id}/versions | List previous versions. |
| GET | /api/plans/{id}/diff | Line-LCS diff. 5,000-line cap. |
Comments & notifications
| Method | Path | Description |
|---|---|---|
| GET | /api/plans/{id}/comments | List comments. Polled every 15s. |
| POST | /api/plans/{id}/comments | Add a comment with anchorText + block index. |
| PATCH | /api/plans/{id}/comments/{cid} | Toggle resolved. Plan author or comment author only. |
| POST | /api/notify | Email + Slack notify. URL must match app domain. |
Example: publish HTML
curl https://orfc.dev/api/plans \
-H "Authorization: Bearer $ORFC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Q2 platform plan",
"content": "<h1>orfc Q2 …</h1>",
"contentType": "html",
"accessRule": "anyone"
}'
Deployment
From git push to orfc.dev. Vercel handles build + edge, Neon handles Postgres, schema migrations are still a manual step (until they're not).
Environment variables
| Key | Required | Used by |
|---|---|---|
DATABASE_URL | prod | Neon connection string. Empty in dev → JSON file fallback. |
NEXTAUTH_SECRET | all | Session signing. |
NEXTAUTH_URL | prod | Callback URL base. https://orfc.dev in prod. |
RESEND_API_KEY | prod | OTP email + review notifications. |
APP_URL | opt | URL embedded in API responses. |
Migration trip-wire
Schema-touching PRs need db push before merge, not after. Vercel auto-deploys main on merge, so a code-and-schema PR that skips the db step ends up deployed against an old schema and the API blows up everywhere. Until the migration is wired into the deploy pipeline, run it first.
Running a migration
# Option A — drizzle migrate (tracks applied migrations)
npm run db:migrate
# Option B — drizzle push (diffs schema.ts vs DB, no journal)
npm run db:push
# Option C — Vercel-managed env
vercel env pull .env.production.local
npx -y dotenv-cli -e .env.production.local -- npx drizzle-kit migrate
Changelog
Recent releases. Versions track @orfc/cli; the web app rolls forward continuously off main.
HTML content type, folder tree, theme toggle
CLI orfc push design.html detects content type from extension or sniff. HTML docs render through sanitize-html (after a brief detour through isomorphic-dompurify), inline SVG and styles preserved. Dashboard sidebar with folder tree + tag chips. --folder / --tag on push. Theme toggle in plan header for every viewer. Comment popover shows the snippet. Forced ::selection highlight.
Security pass + test infrastructure
OTP via crypto.randomInt. SSRF defense on Slack webhooks. Plan GET enforces access rules. CSP + baseline headers. Vitest across cli + web with 63 security-focused tests on day one. CLI push --update applies --access / --viewers.
Inline comments + version history
Highlight any text → leave a threaded comment, anchored to the snippet. Real-time poll for new comments (15s). Every push --update snapshots the previous version with diff view. Web editor with Cmd+S save, replacing the CLI-only edit path.
Initial public release
CLI: push, pull, list, delete, login. Markdown render via react-markdown + remark-gfm. Mermaid block support. Access rules: authenticated, anyone, viewer-list. Postgres on Neon. OTP login via Resend.