{"id":51332983,"url":"https://github.com/markpasternak/canvas-drop","last_synced_at":"2026-07-02T00:30:59.550Z","repository":{"id":365973637,"uuid":"1268183025","full_name":"markpasternak/canvas-drop","owner":"markpasternak","description":"Self-hostable, open-source platform to deploy \u0026 share small static web apps (\"canvases\") inside your org — paste/drag/PUT a folder, get an instant secure URL. Five zero-config backend primitives (KV, files, AI, identity, realtime), an MCP server for AI agents, no telemetry.","archived":false,"fork":false,"pushed_at":"2026-06-19T17:27:58.000Z","size":7255,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T18:26:43.039Z","etag":null,"topics":["ai-agents","developer-tools","drizzle-orm","hono","internal-tools","llm","low-code","mcp","mcp-server","model-context-protocol","open-source","paas","postgres","react","self-hostable","self-hosted","sqlite","static-hosting","typescript","web-hosting"],"latest_commit_sha":null,"homepage":"https://canvas-drop.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/markpasternak.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-13T08:27:14.000Z","updated_at":"2026-06-19T17:49:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/markpasternak/canvas-drop","commit_stats":null,"previous_names":["markpasternak/canvas-drop"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/markpasternak/canvas-drop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markpasternak%2Fcanvas-drop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markpasternak%2Fcanvas-drop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markpasternak%2Fcanvas-drop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markpasternak%2Fcanvas-drop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markpasternak","download_url":"https://codeload.github.com/markpasternak/canvas-drop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markpasternak%2Fcanvas-drop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35028642,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","developer-tools","drizzle-orm","hono","internal-tools","llm","low-code","mcp","mcp-server","model-context-protocol","open-source","paas","postgres","react","self-hostable","self-hosted","sqlite","static-hosting","typescript","web-hosting"],"created_at":"2026-07-02T00:30:55.083Z","updated_at":"2026-07-02T00:30:59.536Z","avatar_url":"https://github.com/markpasternak.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# canvas-drop\n\n**Your org's place to ship the small web tools you build with AI. Drop a folder, get a secure URL, behind your own SSO, on infrastructure you control.**\n\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![CI](https://github.com/markpasternak/canvas-drop/actions/workflows/ci.yml/badge.svg)](https://github.com/markpasternak/canvas-drop/actions/workflows/ci.yml)\n![Node 24](https://img.shields.io/badge/node-%3E%3D24-3c873a.svg)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/site/assets/tour.webp\" alt=\"canvas-drop: the dashboard, Shared, in-browser editor, gallery, sharing, backend, and admin\" width=\"100%\"\u003e\n\u003c/p\u003e\n\nAI builds a working dashboard, form, or internal tool in minutes. Then it stalls, because there is nowhere safe and instant to put it: emailing a zip, or spinning up a project on someone else's cloud outside your org boundary, or waiting on a platform team. canvas-drop closes that last mile. It is the creation-and-sharing layer for AI-built tools, prototypes, dashboards, demos, and lightweight internal apps. Self-hosted inside your trust boundary, MIT-licensed, with **no telemetry and no phone-home**.\n\nIt is inspired by [Quick](https://shopify.engineering/quick), Shopify's internal \"drop a folder, get a URL\" platform that shifted their culture to demos over memos. Quick lives inside Shopify and there was no open way to run the same idea on your own infrastructure with a real backend, so I built canvas-drop and took it further.\n\nFull documentation lives at **[canvas-drop.com/docs](https://canvas-drop.com/docs)**.\n\n---\n\n## Why canvas-drop\n\n- **Distribution is the problem it solves.** AI build tools make the artifact. canvas-drop is where it lands: every canvas sits behind your org sign-in, on infra you control, reachable at an unguessable URL the moment it deploys. No new vendor, no data leaving the boundary.\n- **Agents ship from the workflow they already use.** A keyed deploy API, an installable [agent skill](docs/site/agents/skill.md), and a connect-once [MCP server](docs/site/agents/mcp.md) let an AI agent create and ship a canvas from your editor, terminal, or CI with no human in the loop. The building happens where you already work, not inside a chat box bolted onto the tool.\n- **A real backend when you need it.** Five built-in primitives (KV, files, AI, identity, realtime) added with one `\u003cscript\u003e` tag, no provisioning, **no secrets in the browser**. Stay fully static when you do not.\n- **A rich, scriptable API.** The whole lifecycle is programmable: create, deploy (including a content-addressed staged upload that sends only changed bytes), read-back and verify, versions, rollback, unpublish, sharing, and the full draft-editor loop. The agent contract is one machine-readable page at `{base}/llms.txt`.\n- **Versioned, content-addressed storage.** Every publish is an immutable version; roll back to any of the last 10 in one click. Blobs are keyed by hash and versions are manifests over shared blobs, so re-deploys write only what changed.\n- **A deliberate sharing ladder.** Private, Specific people (signed-in members or pending external people), a Team (a self-serve group — personal *friends \u0026 family* or org-attached), Whole org, or a static Public link that admins can disable globally or revoke per account. Team and Whole-org shares can stay link-only or be listed in **Shared** for people who already have access. Invites are **auth-delegated**: a brand-new person gets access on their first sign-in through your configured auth — no app-owned passwords or magic-link accounts.\n- **Run anywhere.** Database, storage, URL mode, and auth all sit behind interfaces, so the same image runs on a laptop, a $5 VPS, or a corporate cloud. Swapping a driver is a config change, never a code change.\n- **Rich admin and configuration, sensible defaults.** It runs the moment you clone it, with a sane default for every choice, so there is nothing to configure to start. When you want control it is all there: an admin panel (all-canvases governance, takedown and restore, the AI model allowlist, global quota defaults) plus typed env config for drivers, limits, and per-capability gates, validated at boot so a bad combination fails loud instead of silently.\n\n\u003e Quick is files plus a tiny API behind an identity proxy. canvas-drop keeps that simplicity and adds the rich API, the versioned storage model, the five primitives, and the sharing ladder, so the same \"drop a folder\" gesture also covers the parts an org actually needs.\n\n---\n\n## Quickstart (local dev)\n\nRequires **Node 24** and **pnpm**. Clone to a running instance in well under five minutes:\n\n```bash\ngit clone https://github.com/markpasternak/canvas-drop.git\ncd canvas-drop\npnpm install\ncp .env.example .env       # defaults: path mode, SQLite, local storage, dev auth\npnpm dev                   # server + dashboard in watch mode (Ctrl-C stops both)\n```\n\n| URL | What |\n|-----|------|\n| **http://localhost:5173** | The **dashboard** (Vite HMR). Develop here; it proxies `/api`, `/auth`, and `/v1` to the server. |\n| **http://localhost:3000** | The **Hono server**: API, deploy endpoints, and hosted canvases. |\n\n`dev` auth auto-logs-in a fake local user, so there is zero setup. `curl http://localhost:3000/healthz` confirms the server is alive. Port already in use? `CANVAS_DROP_PORT=3001 pnpm dev`.\n\n---\n\n## Deploy a canvas\n\nEvery publish produces the same thing: an immutable version served at an unguessable URL. The first three are in the dashboard; the fourth is for scripts and agents.\n\n1. **Drag a folder or ZIP** with `index.html` and its assets.\n2. **Paste HTML** for a single-file artifact (often what an AI just wrote).\n3. **Edit in the browser**: a file manager plus CodeMirror editor work against a mutable **draft** with autosave; an explicit **Publish** snapshots it into an immutable version.\n4. **Deploy API**: `PUT` a ZIP with the canvas's secret key. `deploy = live`, no draft loop.\n\n```bash\ncurl -X PUT \"$BASE_URL/v1/canvases/$CANVAS_ID/deploy\" \\\n  -H \"Authorization: Bearer $CANVAS_KEY\" \\\n  --data-binary @site.zip\n```\n\nThe key operates only on its own canvas and **never belongs in canvas files**. `GET /v1/canvases/:id`, `GET …/versions`, `GET …/files` (read back the live version to verify), `POST …/rollback`, and `POST …/unpublish` round out the surface. For large or frequently re-deployed canvases there is a **staged upload** (`begin` then per-blob `PUT` then `finalize`): a content-addressed manifest sends only changed files, and bytes go straight to the server instead of through an agent's context. See [canvas-drop.com/docs](https://canvas-drop.com/docs).\n\nYou can also **clone** any active canvas you own, or a gallery-listed template, as an unpublished draft with a fresh slug and key.\n\n---\n\n## Backend in five primitives\n\nAdd one tag, no build step, no keys, no config:\n\n```html\n\u003cscript src=\"/sdk/v1.js\"\u003e\u003c/script\u003e\n```\n\nThe global `canvasdrop` appears. Identity rides the signed-in session; the canvas is identified from its own URL. Every method throws a typed, `instanceof`-catchable error.\n\n```js\nconst me = await canvasdrop.me();                       // { id, email, name, avatarUrl, kind }\nconst votes = await canvasdrop.kv.increment(\"votes\", 1);\nawait canvasdrop.kv.user.set(\"theme\", \"dark\");          // per-viewer scope\nconst f = await canvasdrop.files.upload(input.files[0]); // { id, name, size, url }\nfor await (const delta of canvasdrop.ai.stream(messages, { model })) out.textContent += delta;\nconst ch = canvasdrop.realtime.channel(\"room\"); ch.subscribe(render); ch.publish(\"cursor\", { x, y });\n```\n\n| Primitive | What it gives a canvas |\n|-----------|------------------------|\n| **KV** | Shared (`kv.*`) and per-viewer (`kv.user.*`) key/value with `list` and atomic `increment`. |\n| **Files** | Per-canvas upload/list/delete; served as safe, non-executable bytes. |\n| **AI** | Anthropic-first proxy behind a provider abstraction: streaming, model allowlist, metered quotas. The provider key stays server-side. |\n| **Identity** | `me()`: id, email, name, avatar, and `kind` (`member` or `guest`), resolved from org auth, never the client. |\n| **Realtime** | Ephemeral broadcast plus presence per canvas. Revoking a share drops the socket instantly. |\n\nThe full, agent-optimized contract is served live at **`{base}/llms.txt`**. See also [`docs/sdk.md`](docs/sdk.md).\n\n---\n\n## For AI agents\n\ncanvas-drop treats agents as first-class authors. Three ways in, all thin clients of the same service layer:\n\n- **Deploy API**: a per-canvas key `PUT`s files straight to a live version (above).\n- **Agent skill**: an installable skill ([`docs/site/agents/skill.md`](docs/site/agents/skill.md)) with the full SDK contract, so any coding agent builds canvases out of the box.\n- **MCP server** ([`docs/site/agents/mcp.md`](docs/site/agents/mcp.md)): a connect-once remote server at `{base}/mcp`, signing in through your org's own login (OAuth 2.1). It exposes identity-scoped tools at **full dashboard parity**, so anything a person can do in the UI (deploy, version, roll back, share, edit the draft, set the preview cover, clone, read usage) an agent can do over MCP. On by default; disable with `CANVAS_DROP_MCP=off`.\n\n---\n\n## Configuration\n\nEverything is set by environment variables, validated at boot, with a precise message on an invalid combination. Full surface in [`.env.example`](.env.example). Swappable drivers:\n\n| Concern | Options | Env |\n|---------|---------|-----|\n| Database | SQLite, Postgres | `CANVAS_DROP_DB` |\n| Storage | local disk, S3-compatible (AWS S3, MinIO, Cloudflare R2) | `CANVAS_DROP_STORAGE` |\n| URL mode | path, subdomain | `CANVAS_DROP_URL_MODE` |\n| Auth | `proxy` (recommended prod), `oidc`, `dev` | `CANVAS_DROP_AUTH_MODE` |\n| Email (invite notifications) | `log`, `smtp`, `mailgun`, `noop` | `CANVAS_DROP_EMAIL_DRIVER` |\n| Tenancy (org boundary, off by default) | name an org so guests can't see whole-org canvases | `CANVAS_DROP_ORG_NAME` |\n\nThe blessed production profile is **subdomain mode plus an identity-aware proxy** (e.g. Cloudflare Access) verifying a signed JWT, with Postgres and S3.\n\n**Tenancy (optional).** Set `CANVAS_DROP_ORG_NAME` to draw a member-vs-guest boundary: members (by verified email domain) can share to the **whole org**, while brought-in guests only see canvases they're invited to. It's **inert until named** — deploy first, migrate later. Migrating an existing instance is a dry-run-first cutover; see [`docs/tenancy.md`](docs/tenancy.md).\n\nDay-to-day operation lives in the in-app **admin panel**: the all-canvases list with usage, disable/takedown/restore, the AI model allowlist, and global quota defaults. Operator-tunable settings are editable there or via env; the auth and rate-limit hot path stays read-only.\n\n---\n\n## Self-host (Docker, ~5 min)\n\nRequires **Docker** and **Docker Compose v2**. The repo ships a one-command demo stack that runs canvas-drop in its real `proxy` mode behind an identity-aware proxy (oauth2-proxy) and a bundled demo IdP (Dex), so you can try the production shape with zero external setup:\n\n```bash\ndocker compose up --build          # first build takes a few minutes; add -d for background\n# then open http://localhost:8080  and log in as  demo@example.com / canvasdrop\ndocker compose down -v             # tear down and wipe data\n```\n\nThe app verifies a Dex-signed JWT against Dex's JWKS, the same cryptographic trust path you run in production, with Postgres for data and an optional MinIO profile for S3. The app is never exposed directly; only the proxy is.\n\n\u003e **The demo stack is for local evaluation only.** Its secrets and the `demo@example.com` login are public placeholders, and it runs on plain HTTP in path mode. Rotate every secret and follow the graduation checklist before any real use.\n\nGoing to production is a configuration change: copy [`.env.production.example`](.env.production.example), point the proxy/JWKS at your real IdP, move to subdomain mode behind real TLS, and rotate all secrets. Full walkthrough: [`docs/site/self-hosting/deploy.md`](docs/site/self-hosting/deploy.md).\n\n---\n\n## Security model\n\ncanvas-drop runs inside a **trusted organization**: everyone reaching it has passed org SSO, and an email-domain allowlist keeps outsiders out. That posture deletes whole problem classes (anonymous abuse, spam, public bot threats). What remains is a short list of **hard invariants that must never break** ([`BUILD_BRIEF.md` §12.0](BUILD_BRIEF.md)):\n\n1. **No impersonation.** Identity always comes from the server-side auth context, never anything the client sends.\n2. **No credential or canvas theft.** API keys and tokens are hashed at rest and shown once; canvas passwords are argon2id.\n3. **No unauthorized access.** A canvas is reachable only by its owner and whoever its access rung allows. An admin gets no special access to canvases it does not own; cross-owner admin power is limited to the dedicated admin routes (list, disable/enable/restore). Everything else 404s.\n4. **No cross-canvas reach in subdomain mode.** Each canvas is its own browser origin and cannot read, write, or act on another's data.\n5. **Lifecycle is honored instantly.** Revoke, expiry, disable, delete, slug-regen, and key-regen take effect on the next request and drop live sockets.\n\n**URL-mode isolation is a real choice.** Subdomain mode (`{slug}.example.com`) gives full browser-origin isolation and is recommended for any multi-user production deployment. Path mode (`host/c/{slug}/`) shares one origin and is perfect for localhost and trusted single-user hosting; multi-user path mode requires an explicit opt-in and an admin warning. No secrets ever reach the browser; every endpoint is Zod-validated; uploads are zip-slip checked and served as inert bytes; an audit log records auth, deploys, sharing, AI usage, and admin actions.\n\n---\n\n## Architecture\n\n```\napps/server        Hono server, one role-routed process: API, deploy, hosted canvases\napps/dashboard     Vite + React SPA dashboard\npackages/shared    zod config, dual-dialect Drizzle schema, shared types\npackages/sdk       the zero-config browser SDK (the `canvasdrop` global)\ndocs/              BUILD_BRIEF, plans, compounding learnings, docs-site source\n```\n\n**Dual-dialect is sacred:** SQLite and Postgres stay in lockstep via shared column helpers, and the CI matrix runs the full suite on both. **Config is the only `process.env` reader.** Everything load-bearing sits behind an interface, so the four drivers swap by config alone.\n\n---\n\n## Status\n\n**v1 is feature-complete and hardening toward a public release**, built unit-by-unit from [`BUILD_BRIEF.md`](BUILD_BRIEF.md) with CI green on both dialects at every merge. M1 through M9 plus a wave of post-v1 work (the sharing ladder, Shared discovery, the MCP server at full dashboard parity, clone-as-template, staged uploads, custom slugs, optional canvas screenshots, and an admin-flippable [design-skin layer](docs/site/self-hosting/configuration.md#design-skins)) have shipped; ops/packaging (M10) is the only milestone still in progress — its Docker image/compose, **backup/restore + scheduled maintenance** ([`docs/ops.md`](docs/ops.md)), and secret-scan are in, with the single-VPS load test and the IAP colleague pilot still deferred. See [`docs/plans/`](docs/plans/).\n\n\u003e **Maturity, honestly:** canvas-drop is **not yet running in production anywhere serious.** It boots, passes a dual-dialect suite, and self-hosts via Docker, but it has not been battle-tested under a real org's load yet. That is exactly what is next. Self-host reports, issues, and PRs are very welcome.\n\n---\n\n## Commands\n\n```bash\npnpm dev          # server + dashboard in watch mode\npnpm test         # full suite, BOTH dialects (sqlite + pglite) in-process\npnpm lint         # biome check\npnpm format       # biome check --write (also sorts imports)\npnpm typecheck    # tsc --noEmit across server, sdk, dashboard\npnpm build        # build all workspace packages\npnpm purge [days] # reclaim storage from soft-deleted canvases (dry-run supported)\npnpm backup \u003cdir\u003e # full instance backup (DB + storage) → a portable directory\npnpm restore \u003cdir\u003e # restore a backup into a fresh, empty instance\n```\n\nDeleting a canvas is a soft-delete (a tombstone row). `pnpm purge` is the maintenance sweep that reclaims the heavy data; it reads the same config as the server. **`backup`/`restore`** capture and recover the whole instance (every table + every content-addressed blob) in a driver-agnostic format — so they also migrate between drivers (SQLite↔Postgres, local↔S3). In production these are subcommands of the server binary (`node … apps/server/dist/index.js {backup,restore,purge}`), so cron runs them with the app image. Full runbook + a recommended backup/purge cron schedule: [`docs/ops.md`](docs/ops.md).\n\n---\n\n## Credits\n\nInspired by [Quick: An internal hosting platform for the AI era](https://shopify.engineering/quick) by Daniel Beauchamp and Alex Pilon, and Daniel's [thread](https://x.com/pushmatrix/status/2064722585019969727) on how a folder of files, a lightweight API, and some trust changed how Shopify builds. MIT licensed; not affiliated with Shopify.\n\n## Contributing\n\ncanvas-drop is built by humans and AI coding agents working from the same contract, following the [compound engineering](https://every.to/guides/compound-engineering) practice from Every: plan the work, build one unit at a time with its tests, and capture every non-obvious learning so each pass (human or agent) compounds on the last. Work flows from plans in [`docs/plans/`](docs/plans/), CI green on both dialects before merge. Start with [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`AGENTS.md`](AGENTS.md); institutional learnings compound in [`docs/solutions/`](docs/solutions/).\n\n## License\n\nMIT, see [`LICENSE`](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkpasternak%2Fcanvas-drop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkpasternak%2Fcanvas-drop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkpasternak%2Fcanvas-drop/lists"}