Profile
Back to NewsBack
GitHub Trending 33 min
Reader Mode
kgoedecke/doop: The open-source alternative to Paper.design. A multiplayer design canvas where humans and AI agents design together, live. MCP built in.

kgoedecke/doop: The open-source alternative to Paper.design. A multiplayer design canvas where humans and AI agents design together, live. MCP built in.

14 hours ago

Doop, the open-source alternative to Paper.design: a live canvas where Claude Code streams a pricing section while ChatGPT and Claude Code cursors move across the frames

CI License: AGPL-3.0 Doop Cloud PRs welcome Discord

**Doop is the open-source alternative to Paper.design — a multiplayer design canvas for humans _and_ AI agents. Every design lives on a shareable Canvas** (/c/) holding Frames — artboards that render real HTML in sandboxed iframes. People edit in the browser; AI agents edit through the built-in MCP server, streaming their designs in live. Everyone sees everything as it happens: cursors, presence, frame edits, agent status, and an activity feed.

Think of it as Claude Design for whichever agent you already use: **Claude Code, Codex, ChatGPT, Cursor, OpenCode** or any other MCP client designs on the same canvas, and the built-in Doop Agent runs on GPT, Claude, Gemini, DeepSeek, Kimi or Qwen models (every agent and model).

A Doop canvas with three frames for a travel app (landing hero, mobile itinerary and a booking card) and two collaborators' live cursors

  • Design with agents, not prompts-and-refresh — connect Claude Code (or any MCP client) once,
then watch it sketch, stream and self-review designs on your canvas, next to your cursor.
  • A built-in Doop Agent — queue a card or @mention a role and it designs on its own, no client
to connect. Runs on the model account each user connects (a ChatGPT subscription, or an OpenAI, Claude, Gemini or OpenRouter key), or on the server's ANTHROPIC_API_KEY for as many free tasks as RESIDENT_TASK_LIMIT grants (none by default) (setup); the first-canvas welcome performance is scripted and runs without any of it.
  • Pages within a canvas — organize frames into named pages from the Layers panel. Add, rename,
drag to reorder, double-click to rename, or right-click to rename and delete empty pages. Page edits support Undo/Redo; selected frames can move between pages. Agents can list pages with get_canvas, manage them with set_canvas_pages (passing the original list as expectedPages to protect collaborators), and target frames with pageId. Each page can wear a canvas colour of its own, as in Figma: with nothing selected, the sliders icon on the right opens the Page panel with its Background swatch (agents: set_page_background); cleared, the page shows the theme paper again.
  • Clickable prototypes — wire elements to other frames (click, hover, press, key or delay;
navigate, overlay, back, scroll or link, with transitions) in Prototype mode or straight in the HTML, then click through the flow with Play or share /c//proto. Agents use the same data-doop-* attributes plus get_prototype / set_prototype_start.
  • A real canvas editor — draw frames, shapes, pen paths and text, edit any element's fill,
type and flex layout, snap to rulers and guides, zoom from 8% to 1600% (the editor).
  • True multiplayer — live cursors, presence, follow mode, per-frame editing indicators,
undo/redo, threaded comments on elements or the canvas, and an activity feed, all over one WebSocket room.
  • Design memory — pin exemplar frames, capture decisions, and let the distiller propose durable
style rules that every agent follows.
  • Private by default — invite collaborators by email or share a link that can view, comment
or edit; agents inherit exactly their human's access.
  • Self-host in one command — docker compose up, or bun run dev with zero configuration
(embedded Postgres, no external services required).

Works with any agent

Doop isn't tied to one model vendor. Every agent below designs through the same MCP server, works as the person who connected it, and shows up live on the canvas with its own cursor, status and tasks. Connect AI in the top bar walks you through each one.

The Connect AI dialog: choose Claude Code, Claude, ChatGPT, Codex, Cursor or any other MCP client

| Agent | How it connects | | -------------------------------------------- | ------------------------------------------------------------------------------- | | Claude Code | claude mcp add, OAuth in the browser (setup) | | Claude (web and desktop) | Add to Claude in Connect AI (setup) | | Codex (CLI and desktop) | codex mcp add, OAuth in the browser (setup) | | ChatGPT | A custom MCP server in ChatGPT, guided by Connect AI → ChatGPT | | Cursor | Add to Cursor in Connect AI | | OpenCode, VS Code Copilot, OpenClaw and more | Any MCP client, over OAuth or with an agent key (setup) | | Headless agents (n8n, Mastra, CI) | An agent key as a bearer header (setup) |

No agent of your own? The built-in Doop Agent runs on the model account you connect: a ChatGPT subscription, or an OpenAI, Claude, Gemini or OpenRouter key. OpenRouter brings DeepSeek, Kimi, Qwen, GLM and MiniMax (details).

Quickstart

git clone https://github.com/kgoedecke/doop && cd doop
bun install
bun run dev

Doop builds and installs with bun (bun.lock is the only lockfile); the server itself runs on Node.

  • Web app: http://localhost:4300
  • API + WebSocket + MCP server: http://localhost:4400 (the web port proxies /api, /ws,
/mcp, /.well-known, /i/, /ingest and /local-agent to it)

bun run dev and bun run build also package the Claude Desktop extension with a Node script, so Node must be on your PATH as well.

Everything works with no configuration: data persists to an embedded Postgres (PGlite) in data/pg, and every optional integration (SMTP, stock photos, object storage, analytics) degrades gracefully until its variable in .env.example is set (the ones that matter). The built-in Doop Agent runs once a user connects a model account in Settings (or on the server's ANTHROPIC_API_KEY if you raise RESIDENT_TASK_LIMIT) — agents you connect yourself over MCP need no key.

Or self-host the production build with Docker:

BETTER_AUTH_SECRET=$(openssl rand -hex 32) docker compose up -d   # app + Postgres on :4400

Production build without Docker: bun run build && bun run start (single server on :4400 serving everything). Set DATABASE_URL to use a real Postgres — same code path as PGlite.

Prefer not to run anything? doop.design is the hosted version.

Once you sign in, Home is where you start: describe what you want and Doop makes a canvas with drafts queued for the Doop Agent, or pick up one of your canvases, workspaces and connected agents.

Doop's home: the agent prompt with suggestion chips over a gradient wave, the canvas grid, and a rail with workspaces and connected agents

The canvas editor

The bottom toolbar holds the tools; every one has a single-key shortcut. Shapes drawn on the canvas become standalone elements, and shapes drawn inside a frame become positioned HTML/SVG layers of that frame, so agents read and edit them like any other markup.

| Tool | Key | What it does | | ----------- | --- | ------------------------------------------------------------------------------------- | | Select | V | Click to select, drag the background for a marquee, ⇧/⌘-click to add to the selection | | Hand | H | Pan (Space or the middle button pans from any tool) | | Frame | F | Draw an empty frame | | Rectangle | R | Draw a rectangle | | Circle | O | Draw a circle or ellipse | | Vector path | P | Bézier pen: snaps to its own anchors, previews closing the path; Alt skips snapping | | Text | T | Place a text layer | | Comment | C | Pin a comment to an element or to the canvas |

  • Frames — drag to move, pull the corner handle to resize, snap to neighbours and guides.
The Inspector edits name, position, size and the raw HTML, marks the frame ready for dev, exports it, and pins it as a style reference; Version history… in the frame menu previews and restores earlier designs.
  • Elements inside a frame — click to select, drag a corner grip or edge to resize (⇧ keeps the
ratio; a W × H badge shows the size), drag the element to move it within or across frames. The Design tab edits fill, stroke, text (font, size, weight, alignment), opacity, corner radius, rotation, position, flex layout (direction, gap, padding, wrap, alignment) and image fills; HTML shows its source; Prototype wires interactions. The floating toolbar adds Comment, Ask AI (pre-fills an @mention), Code, Edit text and Copy link.
  • Rich text — double-click text to edit it. Select words and use the formatting toolbar for
bold, italic, underline, strikethrough, bulleted lists or numbered lists. ⌘/Ctrl+B, I and U toggle inline formatting; ⌘/Ctrl+⇧+7 or 8 toggles numbered or bulleted lists. Enter continues a list; Enter on an empty item leaves it. With a text layer selected on the canvas, ⌘/Ctrl+B, I, U and ⌘/Ctrl+⇧+X toggle the whole layer and ⌘/Ctrl+⇧+7 or 8 make it a list, as in Figma. Esc finishes editing; formatting saves with the frame and supports undo/redo. With words selected, the Design panel's font, size, weight and colour edit that run alone; selecting the whole layer shows Mixed where its runs differ, and a value set there applies to all of them, as in Figma.
  • Path editing — double-click a path to drag its anchors and Bézier handles, switch points
between corner and curve, or delete them. Alt moves one handle on its own; ⇧ constrains its angle.
  • Layers — a searchable tree of pages, frames and elements: drag to reorder or reparent, hide,
lock, collapse, and right-click an element for copy, paste, duplicate, Copy HTML, Copy selector and Copy link to element.
  • Rulers and guides — ⇧R shows rulers; drag out of one to add a guide. Guides belong to the page,
undo like any edit, and everything snaps to them within 8 screen pixels.
  • Zoom — 8% to 1600%. Wheel pans, ⌘/Ctrl + wheel or a pinch zooms; a 1px pixel grid fades in from
300%. The zoom menu has 50/100/200% presets and the rulers toggle.
  • Panels — the right side panel has Agents, Comments, Activity and Memory tabs;
Layers and the side panel can be resized, and ⌘. hides all UI.
  • Themes — Light, Dark or System under Settings → Appearance.
  • Phones — the canvas works on a phone: one-finger pan, pinch zoom, tap to comment, and bottom
sheets for canvas actions, Layers and the Inspector.

| Shortcut | Action | | ---------------- | ------------------------------------------------------ | | ⌘Z / ⌘⇧Z (or ⌘Y) | Undo / redo your own edits | | ⌘C / ⌘V / ⌘D | Copy, paste, duplicate frames or layers | | ⌫ | Delete the selected element, else the selected frames | | ⌘⇧H / ⌘⇧L | Hide / lock the selection | | ⌥↑ / ⌥↓ | Move a layer up or down (Layers panel) | | ⇧1 / ⇧2 | Zoom to fit everything / the selection | | ⌘0 / ⌘+ / ⌘− | Zoom to 100%, in, out | | ⇧R | Toggle rulers | | ⌘. | Hide or show all UI | | ⌘⌥↩ | Present from the selected frame (Ctrl+Alt+↩ elsewhere) | | ⌘K | Open the Agents composer (on Home: the search palette) | | Esc | Deselect, stop following |

Click a collaborator's avatar to follow their viewport; an agent's avatar flies you to what it is working on. Placeholder frames show where an agent that has picked up a card is about to design.

Comments, imports and exports

Comment on any element or anywhere on the canvas. The Comments tab lists the page's threads (open or resolved) with their replies. @mention an agent role in a comment or a reply (for example @doop) to queue a design card; the agent's answer comes back in the thread. People who miss a comment get an email digest (opt out under Settings → Account), and canvases opened through a view-only or comment-only link show a badge and disable editing.

The import source picker offers Website (one page, or discover a whole site and pick its pages), Live app (the sync snippet), and GitHub repo (through the GitHub App or a fine-grained token: Doop reviews the repo's screens and components and queues cards for the Doop Agent to sketch them and extract the design system into a style guide). Right-click a frame and choose Export… to download PNG, JPG or a vector PDF (of the frame, or of every selected frame as one PDF). Export in the top bar binds any frames of the page into one PDF. A progress toast tracks the render; the desktop app opens its native save panel. Every frame also has a public live image at /i/.png?scale=2 (or .jpg) that re-renders when the design changes, handy as an og:image or in docs.

Figma, Canva and Photoshop (PSD) import and export are currently available in Doop Cloud and not part of the open source.

Integrations

Integrations in the rail is where an account connects the outside tools Doop works with:

| Integration | What it does | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Linear | Read issues through MCP, or delegate an issue to Doop: it designs a canvas and reports the result back to Linear (setup) | | Slack | Mention @Doop with a brief; it designs a canvas and posts the result in the thread (setup) | | Meta Ads | Pull ad creatives onto a canvas on a schedule with an automation |

The Figma integration (import frames as editable HTML, export frames back to Figma) and the Canva integration (import designs as editable frames, send frames back with live text) are currently available in Doop Cloud and not part of the open source. A self-hosted Integrations page shows Linear, Slack and Meta Ads.

The Integrations page on Doop Cloud with cards for Figma, Canva, Linear, Slack and Meta Ads

Outbound webhooks (Settings → Webhooks) POST a signed JSON event to your own URL when a comment is created, replied to or resolved, when a frame is created, or when an agent finishes or fails a board card, with retries and a test ping (events and signatures).

To reference a specific element in feedback, select it and choose Copy link in its floating toolbar, or right-click its Layers row and choose **Copy link to element**. Opening the link selects the element and opens its properties on the correct page. The URL includes the canvas ID, frame ID and URL-encoded CSS selector, so an AI agent can locate it in the frame HTML. Canvas access rules still apply. Selectors that use element IDs survive layout changes; selectors based on sibling positions can change when the HTML is restructured.

Approve screens for development

Select a frame and turn on Development → Mark as approved in the inspector, or right-click one or more frames and choose Mark ready for dev. A green badge appears above the screen and in Layers. Hover the toggle row or a badge to see who approved it and when. Turn the toggle off to move an approved screen back to draft.

Changing the HTML or frame dimensions switches an approved screen to Needs review; review it and turn Mark as approved on again. Moving, renaming, hiding, locking, or moving between pages keeps approval. Copies start as drafts. An approval checks the frame revision so a concurrent edit cannot accidentally be approved, and waits for live design streams to finish.

Agents can read approvalStatus (draft, ready_for_dev, needs_review) and approval attribution in get_canvas and get_frame. Call get_canvas({ canvas_id, approval_status: "ready_for_dev" }) to find approved screens, then re-read get_frame before implementing. Only ready_for_dev is current sign-off. Approval is set by people in the app. In Automations, choose Designs are approved to run a workflow when frames on a selected canvas become ready for dev. Without an enabled workflow, approval only records sign-off.

The REST equivalent is PUT /api/frames/:id/approval with { status: "ready_for_dev" | "draft", expectedUpdatedAt }. The signed-in user's identity supplies the attribution; the endpoint uses the same canvas access rules as editing and returns 409 when the reviewed revision is stale.

Clickable prototypes

Turn frames into a flow you can click through. Switch on Prototype mode (top bar), pick an element and add an interaction in its Prototype tab, or drag the handle on its right edge onto another frame. Each interaction is a trigger (click, hover, press, a key or a delay) and an action (go to a frame, open one as an overlay, go back, close the overlay, scroll, open a link), with an optional transition: dissolve, slide or push. Connectors on the canvas show where every element leads, and a Start flag marks the frame each page's flow begins on.

A travel app prototype in Prototype mode: four iPhone screens with liquid-glass panels over photos, blue connectors from a trending card to the destination screen, from Book stay to the booking sheet overlay and from Confirm and pay to the confirmation, and a Start flag on the first screen

Press Play to run it full screen, or share /c//proto: the player follows the same canvas access rules, and the browser's Back and Forward walk the flow. Frames stay live while you play, so an edit on the canvas shows up on the screen you are looking at.

The prototype player at phone size, three steps of the flow: tapping the Lago di Braies card on Explore, Book stay opening the booking sheet as an overlay, and Confirm and pay dissolving to the confirmation

Interactions are plain attributes on the element in the frame HTML, so they sync, version and export with the frame, and agents write them with the same tools they design with:

<button data-doop-on="click" data-doop-action="navigate:<frameId>" data-doop-transition="push-left">Book stay</button>

Agents check their wiring with get_prototype, which flags links to missing frames, and set the start with set_prototype_start. Both the MCP server and the built-in Doop Agent have them. The full attribute reference is in .context/specs/prototyping.md.

iPhone and iPad app

The SwiftUI iOS app lives in ios/. Open ios/Doop.xcodeproj in Xcode to run it against Doop Cloud or your own HTTPS server. It uses SwiftUI for the app interface and a native canvas that renders frames with the Blitz HTML/CSS engine (Rust, no WebKit), over the same REST API and WebSocket rooms. The app is in development; see the iOS README for validation status and remaining release requirements.

Connect Claude

Choose Connect AI → Claude → Add to Claude. On hosted Doop, this opens Claude's connector form with Doop's name and server URL filled in. Confirm it, sign in to Doop, then start a new Claude chat and paste the canvas prompt from the dialog. No file download, terminal or API key needed.

The updated Doop desktop app also offers Add to Claude for local and self-hosted servers: it opens Claude Desktop's extension installer directly, with your server URL filled in. Confirm the installation in Claude and approve the Doop sign-in. The online connector needs a publicly reachable HTTPS server; localhost connections use the desktop app. Manual extension installation remains available as a fallback. Build and installation details: claude-extension/.

Hook up Claude Code

One command connects Claude Code (or any MCP client) to your canvas:

claude mcp add --transport http doop http://localhost:4300/mcp

That triggers the standard MCP OAuth flow — a browser window opens, you approve, and from then on the agent works as you. Ask it to design something on your canvas id and watch it happen live. Everything in this shot is the real flow: Claude Code announced itself with set_status, created a frame, and is streaming the pricing section in — presence avatar, "for Kai Moreno" attribution, the frame chip, the working strip, and the task in the Agents panel.

Claude Code connected over MCP OAuth, streaming a pricing-section design into a frame while the humans on the canvas watch it work

Watch an agent design

The first canvas after signup comes with a performance: the Doop Agent streams a welcome design in while you watch — status in the working strip, a task in the panel, a pulsing border on the frame it's building.

The Doop Agent streaming a design into a frame, live — working status, agent task panel and pulsing frame border

That welcome performance is scripted (server/demo.ts) — a pre-authored frame replayed through the same machinery real agents use, so it runs with no configuration at all. The Doop Agent proper needs a key.

The Doop Agent

Doop ships a built-in design team that lives in the server and picks work up on its own: queue a board card, @mention a role in a comment, or post task feedback over the API, and it runs without a human in the loop. Roles (Doop builds; specialists own one pass each — UX, copy, brand, accessibility) are defined in shared/agents.ts, and a card can be routed through several in order.

Normally each user's own connected model account pays for their runs (next section). The server can also pay for a free tier, on Anthropic by default, once RESIDENT_TASK_LIMIT is above 0:

ANTHROPIC_API_KEY=sk-ant-...   # in .env, or the environment of your deployment

Same key gates the guideline distiller (server/distill.ts), which proposes durable style rules from your canvas.

The free tier can run on Azure OpenAI instead — useful when your organisation's credits or compliance rules live there:

DOOP_AGENT_PROVIDER=azure
AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_DEPLOYMENT=my-deployment

The distiller stays on ANTHROPIC_API_KEY either way and quietly turns off without it.

Past the free tasks: connect your own ChatGPT

When a user's RESIDENT_TASK_LIMIT free tasks are gone, they don't lose the agent — they connect a model account and the Doop Agent keeps running on it. A connected account takes over immediately, from the very next task: the free tier is a trial that gets people here, not a balance to spend down first, and connecting stops costing the server anything from that moment. The connection is account-level, so it lives at /settings (Home → Settings); the free-tier wall links there rather than carrying its own copy, and "Connect an AI agent" on a canvas stays about MCP clients only. A user can connect five kinds of account; the Gemini cloud pilot and Claude Plan appear only where the operator or the desktop app provides them:

  • ChatGPT subscription — OAuth against auth.openai.com, then inference through the Codex
backend that Plus/Pro/Business plans include. Tokens live in model_accounts and never reach a browser.
  • OpenAI API key — pay-as-you-go on the user's own OpenAI account, no subscription involved.
  • OpenRouter API key — one key for a curated multi-vendor roster: Kimi, Qwen, GLM, DeepSeek,
MiniMax, MiMo and Gemini Flash (the menu lives in shared/modelMenu.ts). Models without image input still work, in a degraded mode: the agent skips screenshot review, verifies through the frame HTML instead, and the picker labels them "no visual review".
  • Gemini API key — Google's Gemini models through their OpenAI-compatible endpoint, plus Nano
Banana image generation.
  • Gemini cloud pilot — an operator-managed prototype running the official Gemini CLI in an
isolated worker using Google sign-in. Disabled by default; see the pilot setup and verification guide.
  • Claude API key — pay-as-you-go on the user's own Anthropic account.
  • Claude Plan (desktop app) — the Doop desktop app runs the Doop Agent through the user's own
local Claude Code login (claude -p), with a per-run scoped MCP token. Once switched on it takes precedence over connected accounts while the app is online. Setup: desktop/README.md.

The ChatGPT, OpenAI, Claude and Gemini kinds get their model menu from the provider's own catalog, asked with that account's credential when the picker opens (server/modelCatalog.ts, GET /api/model-account/models): the Codex backend's catalog for a ChatGPT subscription (the same list the Codex CLI reads, asked as CHATGPT_CODEX_CLIENT_VERSION), OpenAI's and Anthropic's /v1/models, Gemini's /v1beta/models. A newly released model is on the menu without an upgrade; the curated entries above sit on top as recommendations, a curated tier the account cannot run is left off, and the curated menu alone is served if the provider cannot be reached. OpenRouter stays on its curated roster, whose hand-checked vision flags are what the run relies on.

Azure OpenAI is deliberately _not_ a connectable account kind, and the OpenRouter/Gemini endpoints are fixed constants: a user-supplied endpoint would be a URL the server fetches with the run's full context — an SSRF vector — so custom endpoints stay server-level configuration only.

Either way the user picks their model tier in Settings from that provider's curated menu — on OpenAI, gpt-6-astra (the newest flagship; on a ChatGPT subscription it needs Plus or better and OpenAI is still rolling it out per account), gpt-5.6-sol (flagship), gpt-5.6-terra (the default workhorse) or gpt-5.6-luna (cheap and fast). They are paying for it, so the choice is theirs; DOOP_AGENT_OPENAI_MODEL, DOOP_AGENT_OPENROUTER_MODEL and DOOP_AGENT_GEMINI_MODEL only set the default they start on. Note that gpt-5.4 and gpt-5.4-mini retire from ChatGPT-authenticated Codex on 31 August 2026, so pinning a 5.4 id via that env var will break the subscription path after that date.

OpenAI registers no redirect URI for a hosted app, so connecting ChatGPT takes one of three shapes and Doop picks the cheapest one available:

| Where Doop runs | Flow | What the user does | | -------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------- | | Same machine as the browser (dev, self-host) | Loopback catch — Doop holds 127.0.0.1:1455 | Approve in the OpenAI tab. Nothing to copy, no setup | | Hosted (doop.design) | Device code (/api/accounts/deviceauth/*) | Type a short code at auth.openai.com/codex/device | | Device codes disallowed | Browser redirect + paste | Paste the dead localhost:1455 page's address back into Doop |

The device flow needs device code authorization switched on in ChatGPT → Settings → Security (workspace members need an admin to allow it) — that is why the loopback flow, which needs no setting at all, stays the default when Doop is local. All three end at the same server-side PKCE exchange.

Before you turn this on for real users: driving a ChatGPT subscription from a third-party
server is not something OpenAI's terms sanction, and heavy use can get an account rate-limited or
suspended. The API-key path is the fully supported alternative and shares all the same code.
CHATGPT_CONNECT_DISABLED=1 switches the subscription path off and leaves the key path.

Runs are attributed to the human whose card, comment or feedback they picked up, so the person who asked for the work is the person whose account runs it. The translation between the agent's Anthropic-shaped loop and OpenAI's Responses API lives in server/openaiAgent.ts; which credential a run gets is decided in server/agentModel.ts.

With no server key and no connected account the Doop Agent is off, and it fails quietly by design — queued cards and @mentions simply wait for some agent to claim them. The startup banner tells you which state you're in.

All of this is separate from connecting your own agent. Claude Code and any other MCP client authenticate over OAuth and drive the canvas from outside, on your own subscription — never metered. Three paths, same canvas: the Doop Agent on our key (free tier), the Doop Agent on your key, or your own agent over MCP.

| Variable | Default | What it does | | ------------------------------- | --------------------------- | ------------------------------------------------------------------------------------ | | DOOP_AGENT_PROVIDER | anthropic | What the free tier runs on: anthropic \| azure | | ANTHROPIC_API_KEY | _unset_ | Pays for the free Doop Agent tier (default provider) and the distiller | | AZURE_OPENAI_ENDPOINT | _unset_ | The free tier's Azure OpenAI resource, when DOOP_AGENT_PROVIDER=azure | | AZURE_OPENAI_API_KEY | _unset_ | A key of that resource | | AZURE_OPENAI_DEPLOYMENT | _unset_ | The deployment the free tier runs on | | AZURE_OPENAI_API_VERSION | _unset_ | Pins an api-version query parameter; the v1 surface needs none | | AZURE_OPENAI_REASONING_EFFORT | _unset_ | Reasoning effort on Azure runs; unset sends none (non-reasoning-safe) | | RESIDENT_TASK_LIMIT | 0 | Free Doop Agent tasks per account; 0 means a connected account from the first task | | DOOP_AGENT_MODEL | claude-opus-5 | Model for the Doop Agent on the server's Anthropic key | | DOOP_AGENT_OPENAI_MODEL | gpt-5.6-terra | Default tier on a user's account; each user can pick another in Settings | | DOOP_AGENT_OPENAI_EFFORT | medium | Default reasoning effort on a user's OpenAI account; each user can pick low–xhigh | | DOOP_AGENT_OPENROUTER_MODEL | moonshotai/kimi-k2.6 | Default model on a connected OpenRouter key | | DOOP_AGENT_GEMINI_MODEL | gemini-3.7-flash | Default model on a connected Gemini key | | CHATGPT_CONNECT_DISABLED | _unset_ | 1 hides the ChatGPT flow, leaving the API-key path | | DOOP_DISTILL_MODEL | claude-haiku-4-5-20251001 | Model for the guideline distiller |

RESIDENT_TASK_LIMIT is the free-tier meter. By default it is 0: the Doop Agent only runs once the user connects a model account (any of the kinds above) — a connected account is never metered. Connecting your own MCP agent does not lift the meter: it runs on your model when _it_ designs, but resident tasks still bill a credential. Set the limit above 0 to grant that many free tasks on the server's key; everything that triggers resident work counts, including feedback and retries. There is no "unlimited" value: self-hosting with your own key, set it to a large number, since you're paying Anthropic directly either way.

Accounts

The web app requires an account (better-auth, email/password — open signup). Your account name is your identity everywhere: cursors, presence, the activity feed, and feedback attribution are all server-authoritative from the session, and the WebSocket rejects unauthenticated joins. Canvases are private by default, Figma-style: only the owner and people they invite (Share → invite by email, existing doop accounts) can open one. The Share modal also sets what anyone with the link gets: nothing (invite-only, the default), view, comment or edit. From the same modal an owner can file the canvas in a workspace or list it in the Community gallery (/community), where signed-in people browse published canvases and copy them into their own. Your home screen lists your own canvases plus ones shared with you (plus unowned legacy ones, claimable there). Agents connected over MCP act under the account that approved them and get exactly that user's access.

The share modal: invite collaborators by email, see who has access, and choose what the link grants

With SMTP configured (SMTP_HOST etc. — see .env.example), signups require email verification and "forgot password" sends real reset links. Without it, signup stays open and every email is printed to the server log, links included — the flows still work in development.

Set SIGNUP_EMAIL_DOMAINS=jointhetroops.com to restrict new accounts to one email domain, or use a comma-separated list for several domains. Matching is case-insensitive and exact; existing accounts are unaffected. Leave it unset to keep public signup open.

Set REQUIRE_EMAIL_VERIFICATION=false to let people in before they verify — the link is still emailed, it just stops gating sign-in. Admin promotion is deliberately not part of that trade: ADMIN_EMAILS only ever promotes a verified address (see below).

If signup or password reset hangs rather than failing, the cause is almost always a host that blocks outbound SMTP: Railway and most PaaS block 25/465/587. Resend also serves 2465/2587, so SMTP_PORT=2587 is the usual fix.

Env: BETTER_AUTH_SECRET (required in production), TRUSTED_ORIGINS (comma-separated, defaults to the localhost dev origins).

Workspaces and billing (Team plan)

A workspace is a team's shared home for canvases: everyone in it opens every canvas inside, with no per-canvas invites. Create one from the sidebar on the home screen, invite people by email (existing accounts join at once; anyone else joins the moment they sign up with that address), file canvases in it from the Share modal or the canvas menu ("Move to workspace…"), and manage people, roles (owner / admin / member) and billing at /w/. Agents see workspace canvases in list_canvases and can create into one with create_canvas's workspace_id.

Workspaces are the paid part of doop on a hosted deployment. Plans (see shared/billing.ts):

| Plan | Price | What it is | | ---------- | ----------------------------------------- | ------------------------------------------------------------------ | | Personal | free | Your own canvases, per-canvas invites, agents, community, imports. | | Team | $20 / seat / month, or $192 / seat / year | Shared workspaces with roles; seats follow the people list. | | Enterprise | talk to us | SSO, dedicated support, custom terms. |

Self-hosting: billing is off unless you turn it on. Without STRIPE_SECRET_KEY every workspace is active and nothing is locked. With it, a workspace must hold a live per-seat subscription before it can _grow_ — create canvases in it, invite people, take canvases in — and the upgrade modal appears wherever that is attempted. Canvases already in a workspace stay open to its members whatever the subscription does (a failed card never locks a team out of its work). Deleting a workspace hands every canvas back to its owner's personal space and cancels the subscription.

To enable billing: create the product and prices with node scripts/stripe-setup.mjs (prints STRIPE_PRICE_TEAM_MONTHLY / STRIPE_PRICE_TEAM_YEARLY), point a Stripe webhook at /stripe/webhook for checkout.session.completed and customer.subscription.{created,updated,deleted}, and set STRIPE_WEBHOOK_SECRET. Checkout, cards, invoices and cancelling all happen on Stripe's hosted pages; the server only mirrors the subscription onto the workspace (through the webhook, and a direct sync when the browser lands back from Checkout, so local development works without a tunnel).

Instance admins

ADMIN_EMAILS (comma-separated) names the accounts that get the admin role, applied at signup, on email verification, and at boot — so you can name an admin before or after they have an account. This requires SMTP in production: an address only identifies someone once they have proven they own it, and without a mailer signup is open, so anyone could sign up as your address and take the role with it. A production instance without SMTP promotes nobody and warns at boot; set the role directly in the database if that is your setup. Admins get /admin: every canvas and account on the instance, and "view as", which hands them a real but read-only 15-minute session as that user. Being an admin does not widen canvas access itself: the gate in server/access.ts is shared with MCP, so a privileged read there would give every agent holding an admin's token the run of the instance. View-as sessions cannot write, cannot connect agents, and record who is behind them in session.impersonated_by.

SSO (OIDC)

Optional login against an external OIDC provider (Zitadel, Okta, Authentik, Keycloak, etc.), alongside email/password — not a replacement for it. Set OIDC_ISSUER, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET together to enable it; a partial set refuses to boot rather than run with SSO half-configured. OIDC_SCOPES (default openid email profile) and OIDC_PROVIDER_NAME (default SSO, shown on the login button — e.g. Zitadel) are optional. Signing in via SSO links to an existing email/password account when the emails match and the provider marks the email verified, and this works even on an instance with no SMTP configured, where a local account could otherwise never verify on its own. SSO alone never grants the admin role, even for an address listed in ADMIN_EMAILS — an IdP is not trusted as an admin-promotion source, only as an email-ownership check; promotion still requires the normal ADMIN_EMAILS path (verified signup, or syncAdmins at boot for an account SSO has since verified).

Env: see the OIDC block in .env.example.

Sign in with Google

Optional, alongside email/password and SSO. Create an OAuth client (Web application) in the Google Cloud console, add /api/auth/callback/google as an authorised redirect URI, and set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET together (one without the other refuses to boot). The login page shows a "Sign in with Google" button whenever both are set. Account linking and admin promotion follow the same rules as SSO above; SIGNUP_EMAIL_DOMAINS applies to Google (and SSO) sign-ups exactly as it does to email/password.

Sign in with Microsoft

Same shape as Google. Register an app in Microsoft Entra (App registrations, platform Web) with /api/auth/callback/microsoft as a redirect URI, create a client secret, and set MICROSOFT_CLIENT_ID and MICROSOFT_CLIENT_SECRET together. MICROSOFT_TENANT_ID (default common, any Microsoft account) can be organizations, consumers, or your tenant id to make the button an org-only door. Microsoft does not assert email ownership unless the app registration's ID token includes the email and verified_primary_email optional claims; without them a Microsoft sign-in still works but only links to an existing account that is already verified. Everything else (allowlist, admin promotion) follows the SSO rules above.

Sign in with Apple

Two independent halves, documented in .env.example. For the web button, register a Services ID with Sign in with Apple enabled and /api/auth/callback/apple as a return URL, then set APPLE_CLIENT_ID (the Services ID) and APPLE_CLIENT_SECRET (the client-secret JWT signed with your Sign in with Apple key, which Apple caps at six months) together. For the iOS app, set APPLE_APP_BUNDLE_IDENTIFIER to the app's bundle id: the app shows the native Sign in with Apple button and posts the identity token Apple issues for that bundle id to /api/auth/sign-in/social, which the server verifies against Apple's keys. Apple only sends the person's name on their first sign-in, so the app forwards it then; a later sign-in from a fresh install leaves the name as stored. Linking and admin promotion follow the SSO rules above. Apple's "Hide My Email" addresses are on privaterelay.appleid.com, which SIGNUP_EMAIL_DOMAINS rejects unless listed.

Agent auth (MCP OAuth)

The /mcp endpoint requires OAuth. Adding the server in Claude Code / Codex triggers the standard MCP OAuth flow: a browser window opens, you sign in to Doop and approve, and the client stores a bearer token. Every tool call then carries your identity — agent tasks show "for ⟨you⟩" in the Agents panel, and presence tooltips name the owner. Unauthenticated calls get a 401 whose WWW-Authenticate header points at /.well-known/oauth-protected-resource/mcp and asks for openid profile email offline_access (so clients get refresh tokens); that is what kicks off the flow. The approval screen lives at /oauth/consent. Dynamic client registration is enabled, so no manual client setup. ping and resources/list/resources/read answer without a token, and a banned account's agents get a 403 even with a valid one.

The endpoint takes POST only and answers with plain JSON rather than an SSE stream, which is what ChatGPT needs. Tool results also carry structuredContent for an inline frame-preview card (ui://doop/frame-preview-2.html) in clients that render MCP Apps, such as ChatGPT.

In production also set BETTER_AUTH_URL to the public origin — OAuth URLs are built on it.

Deploy

The repo ships a production Dockerfile (client build + Chromium for frame screenshots). Any container host works; Railway/Fly are the least friction:

  1. Create the app from this repo (both auto-detect the Dockerfile).
  2. Add a managed Postgres and set DATABASE_URL. Don't skip this in real deployments —
the PGlite fallback is embedded/single-process and only suits a single instance with a persistent volume mounted at /app/data.
  1. Set BETTER_AUTH_SECRET (long random string) and BETTER_AUTH_URL (the public origin,
e.g. https://doop.example.com). Extra allowed origins: TRUSTED_ORIGINS (comma-separated).
  1. Health check: GET /healthz. The server trusts one proxy hop (trust proxy), so
TLS termination at the platform edge works out of the box.

Local sanity check of the exact production image:

docker build -t doop .
docker run -p 4400:4400 -e BETTER_AUTH_URL=http://localhost:4400 -e BETTER_AUTH_SECRET=dev-only doop

Environment variables

Every variable is optional except the two marked required in production. Each one is declared and validated once at boot (server/env.ts) and documented in .env.example, which also lists endpoint overrides and other rarely needed knobs. This is the short list a self-hoster usually touches:

| What | Variables | Without them | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Core | BETTER_AUTH_SECRET, BETTER_AUTH_URL (both required in production), DATABASE_URL, TRUSTED_ORIGINS, PORT | Embedded PGlite in data/pg, localhost origins | | Sign-in | OIDC_, GOOGLE_, MICROSOFT_, APPLE_, SIGNUP_EMAIL_DOMAINS, ADMIN_EMAILS (Accounts) | Email/password only, open signup | | Email | SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE, EMAIL_FROM, REQUIRE_EMAIL_VERIFICATION | Emails print to the server log; no verification | | Object storage | BUCKET, ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, REGION (any S3-compatible store) | Uploaded assets stored under data/assets | | Doop Agent | ANTHROPIC_API_KEY, RESIDENT_TASK_LIMIT, DOOP_AGENT_, AZURE_OPENAI_ (The Doop Agent) | Runs only on accounts users connect | | Image generation | OPENAI_API_KEY, GEMINI_API_KEY, ARK_API_KEY (Seedream), DOOP_IMAGE_MODEL | Runs only on accounts users connect; Seedream off | | Agent search tools | PEXELS_API_KEY (photos), BRANDFETCH_CLIENT_ID (logos), BACKGROUNDS_ORIGIN, CONTEXT_DEV_API_KEY | No photo search; open-source and favicon logos; local Chromium | | Screenshots | CHROME_PATH, CHROME_NO_SANDBOX (the Docker image sets both up)

... (README truncated for length)

Chat with me