{"slug": "show-hn-canvas-multiplayer-html-mcp-first", "title": "Show HN: Canvas, multiplayer HTML (MCP-first)", "summary": "Canvas, an agent-agnostic multiplayer HTML deck editor, launches with MCP-first design allowing AI agents like Codex and Claude Code to propose edits as diffs for team review. The standalone tool features slide locks, threaded comments, versioning, and export to HTML, PDF, and PowerPoint, aiming to streamline collaborative presentation creation with AI assistance.", "body_md": "Agent-agnostic multiplayer editor for HTML decks. Each slide can be owned by a user; Codex, Claude Code, or another MCP-compatible agent proposes edits as diffs the team reviews. Threaded comments and slide locks keep parallel work coherent. Canvas is standalone with its own Supabase project and auth; see [ADR-0004](/BVCampos/canvas/blob/main/docs/adr/0004-canvas-standalone.md) for the split and [ADR-0009](/BVCampos/canvas/blob/main/docs/adr/0009-agent-agnostic-clients.md) for the client model.\n\n**Why this exists, in one screen:**[DESIGN.md](/BVCampos/canvas/blob/main/DESIGN.md)** Canonical vocabulary (Deck, Slide, Edit, Snapshot, MCP Token, …):**[CONTEXT.md](/BVCampos/canvas/blob/main/CONTEXT.md)** Schema:**[app/supabase/migrations/](/BVCampos/canvas/blob/main/app/supabase/migrations)(sequential; 0000 is the workspace tenancy foundation, 0001+ are Canvas-specific)\n\n**Auth**— Google + magic link (standalone Supabase project, see[ADR-0004](/BVCampos/canvas/blob/main/docs/adr/0004-canvas-standalone.md))**Deck list, create / import**— paste or upload an HTML deck; the parser decomposes it into slides + theme + nav and lifts inline base64 images out to Storage**3-pane editor**— slide list / live iframe preview / right rail (comments + proposals + danger)** Slide locks**— 15-min soft lock; warns when people or their agents converge on the same slide** Proposals**—`canvas_deck_edit`\n\nrows with status`pending → applied | rejected`\n\n. Inline diff, slide-over sheet, and a per-deck/inbox view**Threaded comments**— top-level pinned to a slide (anchor at fractional x,y), replies thread under. Mentions stored as a uuid array** Versioning**— every applied edit produces a new immutable`canvas_slide_version`\n\n; the slide table's`html_body`\n\n/`slide_styles`\n\n/`title`\n\nare denormalized caches kept in sync by the apply-edit RPC. Restores are forward-only**Snapshots**— named deck-wide cuts (manual + auto on export / share / consolidate / pre-restore). Stored as theme/nav + a`(position → slide_version_id)`\n\nmap; cheap to create**Connections**— per-user MCP tokens plus an optional personal OpenRouter key. Bearer`/api/mcp`\n\nworks with streamable-HTTP MCP clients; in-app chat can use the local Codex/Claude bridge or OpenRouter per turn**Export**— HTML, PDF, and PowerPoint with auto-snapshots and visible render progress\n\n```\ncd app\ncp .env.example .env.local            # fill in the keys (see below)\nnpm install\nnpm run dev                           # http://localhost:3001\n```\n\n| Var | Notes |\n|---|---|\n`NEXT_PUBLIC_SUPABASE_URL` |\n`https://<your-project-ref>.supabase.co` (browser-safe, not a secret) |\n`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` |\nBrowser-safe `sb_publishable_...` |\n`SUPABASE_SECRET_KEY` |\nServer-only `sb_secret_...` . Used by the HTML deck parser (uploads assets via service role), MCP token revocation, and the workspace-create server action (RLS blocks direct INSERT on `public.workspaces` ). Never expose to the browser, never commit |\n`CANVAS_CREDENTIAL_ENCRYPTION_KEY` |\nServer-only base64-encoded 32-byte key (`openssl rand -base64 32` ) used to AES-256-GCM encrypt personal OpenRouter API keys. Keep stable across deploys; never expose or commit |\n`NEXT_PUBLIC_APP_URL` |\n`http://localhost:3001` locally; `https://canvas.21xventures.com` in prod |\n\n**Bring your own Supabase project:** create a project, run every migration in `app/supabase/migrations/`\n\nin order (`supabase db push`\n\nfrom `app/supabase/`\n\n, or paste each file into the SQL editor), then copy the URL + keys from your project's API settings. `app/.env.example`\n\nis the source of truth for which vars are needed.\n\n```\nnpm run dev             # next dev on :3001\nnpm run build           # next build\nnpm run start           # next start on :3001\nnpm run lint            # eslint (next config) — must be clean before merging\nnpm run test            # vitest run (parser + MCP server unit tests)\nnpm run test:watch      # vitest watch\nnpm run e2e             # full path against live Supabase + dev server (see below)\nnpm run sweep-orphans   # delete Storage objects whose deck row no longer exists\n```\n\nEnd-to-end exercise of the full Canvas v1 path against a live Supabase project. Requires `npm run dev`\n\nrunning in another terminal, plus `E2E_USER_ID`\n\nand `E2E_WORKSPACE_ID`\n\nset to a user + workspace that exist in your project. Imports the seed deck at `app/tests/fixtures/seed-deck.html`\n\n(gitignored — drop in any full HTML deck), mints an MCP token for the test user, walks the JSON-RPC surface, fetches preview / asset / export endpoints, then cleans up everything it created. Source: [app/scripts/e2e-canvas.mts](/BVCampos/canvas/blob/main/app/scripts/e2e-canvas.mts).\n\nOne-shot maintenance — lists every object under the `decks`\n\nbucket and deletes any whose `deck_id`\n\npath segment has no matching `canvas_deck`\n\nrow. Safe to run repeatedly. Source: [app/scripts/sweep-orphans.mts](/BVCampos/canvas/blob/main/app/scripts/sweep-orphans.mts).\n\n```\n21x-canvas/\n├── README.md                       you are here\n├── CONTEXT.md                      domain glossary — names of things\n├── DESIGN.md                       why it exists, v1 success criteria\n├── docs/adr/                       architectural decisions (planned)\n└── app/                            the Next.js app\n    ├── AGENTS.md                   gotchas for AI coding assistants (Next 16, proxy.ts)\n    ├── CLAUDE.md                   includes AGENTS.md\n    ├── src/\n    │   ├── proxy.ts                middleware (Next 16 renamed `middleware.ts` → `proxy.ts`)\n    │   ├── app/\n    │   │   ├── (auth)/             login flow\n    │   │   ├── api/                preview, asset, export, mcp routes\n    │   │   ├── canvases/           list, new, [id] editor, inbox, history, proposals\n    │   │   └── settings/mcp/       per-user MCP token issuance\n    │   ├── components/             shared UI + proposal-diff / proposal-iframe\n    │   └── lib/\n    │       ├── auth/               server-action helpers + user resolution\n    │       ├── canvas/             parser, assembler, mcp server, edit RPCs\n    │       └── supabase/           ssr / browser / admin clients\n    ├── supabase/migrations/        0001 schema → 0005 proposals\n    ├── tests/                      vitest specs + fixtures\n    └── scripts/                    e2e + sweep-orphans\n```\n\nVitest covers the deck parser and the MCP server. `tests/fixtures/mini-deck.html`\n\nis the committed minimal fixture; `tests/fixtures/seed-deck.html`\n\n(a larger real-world deck) is **gitignored** — drop in any full HTML deck if you want to run the e2e or reproduce parser bugs against a complex layout. Any `*.real.html`\n\nyou drop into `tests/fixtures/`\n\nis similarly gitignored.\n\nThis app runs on **Next 16.2 + React 19.2**, which has breaking changes vs. anything older. The most likely-to-trip-you-up ones:\n\n- Middleware lives in\n[app/src/proxy.ts](/BVCampos/canvas/blob/main/app/src/proxy.ts), not`middleware.ts`\n\n. The exported function is`proxy`\n\n, not`middleware`\n\n. - Server Components are the default; client components must be explicitly marked.\n- React 19's\n`react-hooks`\n\nlint rules are stricter — setState in effects and refs-during-render are errors, not warnings. Use the \"adjust state on prop change\" pattern (track previous prop, compare during render) rather than effects when resetting derived state.\n\nWhen in doubt about an API, read the relevant guide in `app/node_modules/next/dist/docs/`\n\nbefore writing code. See [app/AGENTS.md](/BVCampos/canvas/blob/main/app/AGENTS.md).\n\n**Project:** bring your own Supabase project (the reference deployment is named`canvas-prod`\n\n)**Region:**`us-east-1`\n\n, Postgres 17**Schema:** Canvas tables live in`public`\n\nwith the`canvas_*`\n\nprefix (not a separate Postgres schema — avoids the \"exposed schemas\" config step in Supabase Studio). Tenancy tables (`workspaces`\n\n,`users`\n\n,`workspace_memberships`\n\n,`workspace_invites`\n\n) sit in`public`\n\nwithout a prefix; they're a verbatim port of the workforce-management foundation — see migration 0000 and[ADR-0004](/BVCampos/canvas/blob/main/docs/adr/0004-canvas-standalone.md).**Storage bucket:**`decks`\n\n(created by migration 0003; assets under`assets/{deck_id}/...`\n\n)**Migrations:** apply in order via`supabase db push`\n\nfrom the`app/supabase/`\n\ndirectory, or pasted into the dashboard SQL editor. They're idempotent enough that re-applying is generally safe, but read the file before you do**RLS:** every Canvas table uses`public.is_workspace_member`\n\n/`public.is_workspace_admin_or_owner`\n\n.`public.workspaces`\n\nhas**no INSERT policy for**— workspaces are created only via the service-role admin client (`authenticated`\n\n`createWorkspaceAction`\n\nin`src/lib/auth/actions.ts`\n\n)\n\nThe MCP route resolves token → user/workspace and then enforces deck access explicitly. Content writes are propose-first; the optional trusted fast lane is deck-scoped, patch-only, and requires render verification.\n\n**Domain:**`canvas.21xventures.com`\n\n(DNS / hosting TBD — Vercel likely)**Auth callback:** Canvas uses its own OAuth client. Wire`https://<your-project-ref>.supabase.co/auth/v1/callback`\n\ninto that client's redirect URIs, and put your production app URL into Supabase Auth → Redirect URLs.\n\n`npm run lint`\n\n— clean`npm test`\n\n— green`npx tsc --noEmit`\n\n— clean- If you touched migrations, sanity-check the SQL against the dashboard before pushing (RLS is easy to break)", "url": "https://wpnews.pro/news/show-hn-canvas-multiplayer-html-mcp-first", "canonical_source": "https://github.com/BVCampos/canvas", "published_at": "2026-07-13 16:53:44+00:00", "updated_at": "2026-07-13 17:05:33.527805+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["Canvas", "Codex", "Claude Code", "Supabase", "OpenRouter", "21xventures"], "alternates": {"html": "https://wpnews.pro/news/show-hn-canvas-multiplayer-html-mcp-first", "markdown": "https://wpnews.pro/news/show-hn-canvas-multiplayer-html-mcp-first.md", "text": "https://wpnews.pro/news/show-hn-canvas-multiplayer-html-mcp-first.txt", "jsonld": "https://wpnews.pro/news/show-hn-canvas-multiplayer-html-mcp-first.jsonld"}}