Skip to content

prisma/mrkdwn

Repository files navigation

mrkdwn

A collaborative markdown workspace where humans and AI agents are first-class co-authors.

→ Take the guided tour — how Automerge, Prisma Compute, Prisma Postgres, and object storage fit together, with the conflict-resolution story explained from first principles (served at /tour, next to the live app).

mrkdwn rendering its welcome document: Notion-style live markdown with colors, task lists, and the agent invite in the top bar

Notion-style live editing, realtime cursors, comments — and a one-click way to invite Claude Code, Codex, or any agent into the document, where it edits alongside you with its own avatar, cursor, and comment threads.

mrkdwn is an open-source demonstration app for Prisma Compute: a realtime, stateful, agent-friendly product built in ~3,000 lines of TypeScript as a single Bun process — with its data layer on Prisma Postgres via Prisma Next — that deploys to Prisma Compute as-is. No Kubernetes, no serverless gymnastics, no separate websocket tier.

What it does

  • Notion-style editing — markdown syntax is always concealed; structure is edited directly. Backspace at a block's start demotes/unwraps it, # promotes headings, [] starts a to-do, / opens the block-type menu, Tab indents, and every block has hover drag-handles for reordering (with a proper drag ghost). A </> toggle shows raw markdown for surgical edits.
  • Full GFM and then some — editable tables with Notion-style + row/column strips, task lists with layout-stable checkboxes, strikethrough, autolinks, :emoji: shortcodes with a typeahead dropdown, and Streamlit-style text colors (:red[text], :blue-background[text]) applied from the selection toolbar.
  • A real workspace — pages live at /{workspace}/{id}-{slug} (stable id, title-derived slug), navigated by a collapsible sidebar with filtering, ⌘K palette, @page-slug links between pages, and /page to create-and-link inline.
  • Canvas boards — a second document kind: JSON Canvas 1.0 boards with a FigJam feel (pan/zoom, sticky notes with markdown, drag-to-connect edges, the six spec colors), stored in Automerge as id-keyed maps so concurrent edits merge per node. File nodes (slug.md) render another workspace page live inside the board; agents read and write the whole board as spec JSON (GET/PUT /api/doc), merged per node with in-flight human drags.
  • Paste images anywhere — into a canvas or a markdown page. Bytes go to S3, the record to Postgres, and a Bun endpoint serves them with Bun.Image resizing (/api/images/{id}?w=640) and immutable cache headers.
  • Agents as collaborators — agents join over a tiny REST API using the oldText → newText editing contract they already know from their file tools. Invites are scoped to the page you're on; agents pick their own handle and display name, appear as Claude-spark avatars next to human circles (participants of this page only; offline collaborators fade), and receive @mentions from the doc or comments as a long-polled task queue — mentioning is easy, because @ completion puts the agents live in the current doc first.
  • Who wrote what — click any avatar and that person's contributions tint in their color, derived from Automerge history, while the comments panel filters to threads by or mentioning them.
  • Conflict-free by construction — every keystroke and agent edit syncs live over Automerge CRDTs. Concurrent edits merge; nobody's work is clobbered, ever.
  • Durable to S3, visibly — a background worker continually mirrors every page's full Automerge file (content + complete history) to S3-compatible storage, coalescing bursts of edits into at most one write per doc every 2 seconds. The dot in the topbar tells the truth: green = durable, amber = changes not yet persisted, red = connection lost (editing locks until sync resumes).

Quickstart

bun install
bun run dev          # → http://localhost:4545

That's the whole setup. In dev, the server auto-starts a local Prisma Postgres (via @prisma/dev, a stateful instance named mrkdwn) and applies the schema — no database to install, nothing to configure. Set DATABASE_URL to use a real Prisma Postgres instead (see .env.example).

Then open the welcome page, hit Invite your agent (top right), and paste the invite into Claude Code. The agent introduces itself, reads the doc, and starts collaborating — cursor and all. Mention @claude anywhere to hand it a task.

bun test             # 1,000+ tests: sync, REST, mentions, attribution, editor logic
bun run build        # production frontend → dist/web
bun start            # build + serve with NODE_ENV=production

The Prisma stack

The workspace/page registry (workspaces, page ids, titles, slugs) lives in Prisma Postgres, managed with Prisma Next. The entire data layer is:

  1. A contract — src/prisma/contract.prisma, two models, ~20 lines. Edit it, run bun prisma-next contract emit, and the typed client regenerates.
  2. Typed queries — db.orm.public.Document.where(...), autocompleted end to end (see src/server/store.ts, the one file that talks to the DB).
  3. Zero-setup dev — @prisma/dev boots a real local Postgres in-process; the schema applies idempotently on start.

Page content stays in Automerge (one doc per page holds title, markdown, and comments — one sync channel, one merge story); the registry is the queryable, relational source of truth for what exists. Page titles typed into the editor sync back into Postgres, re-deriving slugs.

For long-term durability, src/server/persist.ts mirrors each page to S3 as {workspaceId}/{pageId}.automerge (set S3_* in .env to enable — any S3-compatible store works; we use a Tigris global bucket). Writes are dirty-tracked and coalesced (≥2s apart per doc), and the registry's persistedAt column is updated after each successful S3 write — the edit hot path never touches Postgres or S3. Restoring a page is one GET + Automerge.load().

This repo also ships the Prisma Next agent skills (.agents/skills/, pinned by skills-lock.json), so a coding agent working on this codebase knows the contract → emit → migrate workflow out of the box.

Deploying to Prisma Compute

mrkdwn is exactly the shape of app Prisma Compute is for: a single stateful process that owns websockets, long-polls, background scanning, and disk state — things that fight serverless platforms — plus a Postgres it queries.

The deploy is one command. prisma.compute.ts describes the app (Bun framework, entrypoint, build), build.compute.ts assembles a self-contained artifact, and the CLI builds locally, uploads, and promotes:

bunx @prisma/cli app deploy --project <your-project> \
  --env NODE_ENV=production \
  --env DATABASE_URL=<your Prisma Postgres connection string> \
  --env MRKDWN_BASE_URL=<your public URL> \
  --env MRKDWN_AGENT_TOKEN=<stable agent bearer token> \
  --env S3_ACCESS_KEY_ID=... --env S3_SECRET_ACCESS_KEY=... --env S3_BUCKET=...

Websockets terminate on the same port as HTTP (/sync), so a single exposed port is all it needs. Compute disks are ephemeral per deployment — which is exactly why the S3 mirror exists: on boot, any page the registry knows but the local disk doesn't is restored from object storage, full history included (repo.ts). MRKDWN_BASE_URL makes agent invites point at the public URL, and MRKDWN_AGENT_TOKEN keeps them valid across redeploys.

How agents join

The Invite your agent modal (or GET /api/agent-setup?page=<id>) produces a self-contained invite scoped to the page you're on: base URL, bearer token, the page's ?page= target, a quick reference, and a pointer to the full skill at /skill.md — a valid Claude Code skill an agent can fetch or install (~/.claude/skills/mrkdwn-collab/SKILL.md).

Agents name themselves: the invite tells them to pick their own @handle and display name, announce presence, and read the page for work already waiting (unchecked to-dos, open questions, asks aimed at "the agent") — not just @mentions. An agent's first authenticated request registers its handle and delivers any mentions written before it joined.

The API lives under /api. Every request carries Authorization: Bearer <token>, X-Agent: <handle>, and X-Agent-Name: <display name> (how humans see the agent on avatars, cursors, and comments):

Endpoint What it does
GET /api/workspace Workspace + pages [{id, title, slug, path}] (unauthenticated)
POST /api/pages Create a page {title}
GET /api/doc Read (?format=markdown for raw text, ?page=<id> for a specific page)
POST /api/doc/edits Exact-match replacements {edits: [{oldText, newText, replaceAll?}]} — atomic, 409s with hints on miss/ambiguity
POST /api/doc/append Add a section at the end
PUT /api/doc Full replace (server diffs into splices so concurrent human edits survive)
GET/POST /api/comments, POST /api/comments/:id/replies, .../resolve Threads anchored to quoted text
GET /api/notifications?wait=25 Long-poll for @handle mentions (doc + comments); ack with POST /api/notifications/ack
POST /api/presence Heartbeat — shows the agent's avatar/cursor to humans
GET /api/status Unauthenticated UI badge data (agent online/pending)

Agent edits apply as Automerge splices on the server, so they merge with in-flight human keystrokes instead of clobbering them — and they're typed in at human speed: the server streams each edit into the doc in small jittered chunks with the agent's live cursor riding along, so watching an agent work feels like watching a colleague type, not text teleporting in. (Set MRKDWN_AGENT_TYPING=off for instant applies.) Agents are told to stay reachable: keep long-polling for 30 minutes, restarting the window whenever new instructions arrive.

Architecture

src/
  prisma/   Prisma Next contract (Workspace, Document) + generated client types
  server/   one Bun.serve: HTTP + websocket sync + REST + notifications
    wsbridge.ts        Bun-native websockets bridged to automerge-repo's server adapter
    repo.ts            Automerge repo + NodeFS storage; pages, title→slug sync
    store.ts           workspace/page registry: PrismaStore (Prisma Next) or MemoryStore
    devdb.ts           local Prisma Postgres via @prisma/dev + schema apply
    api.ts             agent REST API (edits planned on a string copy → applied as splices)
    notifications.ts   @mention scanning across all pages (cursor-keyed, debounced)
    skill.ts           invite snippet + /skill.md
  shared/   doc schema, attribution, mentions, slugs/ids, identity colors
  web/      React + CodeMirror 6 (routing shell in main.tsx)
    editor/            automerge sync, presence carets, syntax concealment +
                       emoji/colors (livePreview), structural editing (blockEdit),
                       drag handles + block menu (blockHandles), editable table
                       widgets (tableWidget), author spotlight, comment highlights,
                       @-autocomplete, formatting commands
    components/        header/avatars, sidebar, ⌘K palette, comments panel with
                       @mention autocomplete, invite-agent modal

Decisions worth stealing:

  • One Automerge doc per page holds title, content, and comments. Comment anchors and mention bookkeeping use Automerge cursors, so they track their text across anyone's edits.
  • Registry in Postgres, content in CRDTs. Listing, routing, and slugs are relational queries; the hot collaborative path never touches the database.
  • Bun-native websockets feed automerge-repo's server adapter through an ~80-line bridge (wsbridge.ts) instead of depending on ws's Node server internals.
  • Presence is ephemeral broadcast carrying Automerge cursors; each peer resolves them against its own doc state, so cursors stay correct however far apart peers are.
  • Attribution from history (attribution.ts): each change is diffed against its parents; insertions become cursor pairs that resolve in the current doc. Agents are tagged via change messages, humans via session actors.

Auth model (v1)

One public workspace, org-level permissions, a single agent bearer token generated on first boot (data/config.json). The web UI is unauthenticated; agent REST requires the token. The invite embeds the token — treat it like a password and rotate it by deleting it from data/config.json. Accounts and private workspaces come later.

What's next

  • Multiple workspaces + sign-in (the UI already sketches it — try New workspace)
  • Real accounts; per-agent tokens with scopes
  • Webhook/push notification channel for agents (today: long-poll)
  • Export/import, history timeline scrubbing (Automerge keeps full history already)

License

Apache-2.0

About

No description, website, or topics provided.

Resources

License

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors