# canvas-drop — llms.txt Agent-readable reference for deploying and extending canvases. See {base}/docs for the full docs. ## Overview canvas-drop Deploy and share the small web tools your org builds, behind your sign-in. This page is for anyone deciding whether to run canvas-drop, or wondering what it does today. By the end you know what a canvas is, what ships, what is still open, and which page to read next for your job. canvas-drop is an open-source (MIT), self-hostable platform where signed-in members of an organization publish and share small web artifacts called canvases . A canvas is static files (HTML, CSS, JS, images). Drop them in and they are live at a URL your colleagues can open once they sign in. It is inspired by Shopify's Quick; canvas-drop is the open way to run the idea on your own infrastructure. The constraint is the product: a small, fixed set of primitives done well, not a general-purpose hosting platform. There is no server-side build step and no secret in the page. A canvas gains backend capability only through six fixed primitives (KV, files, AI, identity, realtime, and admin-granted Connections) served by a zero-config browser SDK, and that backend stays off until the owner or an editor turns it on. Try it A signed-in local instance with no external services ( path URLs, SQLite, local disk, dev auth). You need Node 24 or newer and pnpm: git clone https://github.com/markpasternak/canvas-drop.git cd canvas-drop pnpm install cp .env.example . env pnpm dev # dashboard http://localhost:5173 · server http://localhost:3000 Open http://localhost:5173 , click Create canvas , choose Paste HTML , and paste an index.html . It is created and published in one step and live at http://localhost:3000/c/{slug}/ . To give it a backend, switch on Enable backend in the canvas's Backend tab and add one tag to the page: < script src = "/sdk/v1.js" > < script type = "module" > const me = await canvasdrop. me (); const views = await canvasdrop. kv . increment ( "views" ); The Quickstart walks the rest of the loop: deploy, edit, share, roll back. For the production shape instead, docker compose up --build boots Caddy, an identity-aware proxy (oauth2-proxy) signing in against a bundled demo identity provider (Dex), Postgres, and the app in real proxy auth mode at http://localhost:8080 (sign in as demo@example.com / canvasdrop ). See Self-hosting → Install . What ships today The administration guide covers composed sharing filters, access explanations, activity, offboarding, selected bulk actions, permanent file/data purge, and observed Connections health. Area What you get Read more Publish Paste a single index.html , drag files or a folder, upload a .zip , or PUT a ZIP to the Deploy API with a per-canvas cd_... key. A staged upload sends only the files the server does not already have. Limits: 100 MB per canvas, 25 MB per file, 2 000 files. Create & publish , Deploy API Edit in the browser One mutable draft per canvas: file tree, code editor, autosave, inline preview, and on-page text editing when the draft is a single HTML file. Publish snapshots the draft into a version. The editor Versions Every publish or deploy is an immutable version; the last 10 are kept. Make current rolls back or forward, Restore copies a version into the draft, any version downloads as a ZIP, and non-current versions can be deleted. Versions Share Name people and teams as viewers or editors — that list always applies — then pick General access: Restricted, Whole org, or Public link. Add a password or an expiry. Access is checked on every request and never cached, so a revoke takes effect on the next one. Public link is static-only: anonymous visitors get no primitives. With an org boundary configured ( CANVAS_DROP_ORG_NAME ), Whole org means the canvas's home org. Sharing & access Roles Each person or team on the people list is a viewer or an editor . Editors edit, publish, roll back, change settings and sharing, and rotate the deploy key; deleting, transferring ownership, and the guest-AI switch stay with the owner ( OWNER_ONLY ). Only org members can be editors. A stale save is refused with DRAFT_CONFLICT , so two editors never overwrite each other silently. Ownership transfers to an editor in one step. Roles: viewers and editors Teams Any signed-in user can create a personal team; org members can create org-attached teams. Add people by email, then grant the team viewer or editor access to a canvas. Teams Backend Turn on the backend per canvas, then toggle KV, files, AI, and realtime independently; identity ( canvasdrop.me() ) is on whenever the backend is. A call to a disabled primitive fails with CAPABILITY_DISABLED . A further opt-in, authoring, lets a page create canvases as the signed-in viewer. Capabilities , Browser SDK Connections An admin can define a reusable exact-origin HTTPS profile with selected methods and write-only protected headers, then grant it to specific canvases. Canvas code gets a bounded fetch -like API; no arbitrary backend code runs. Connections Discover Shared lists the canvases already opened to you. The Gallery is opt-in: Public-link canvases and Whole-org canvases their owners list, scoped to your org. An owner can also allow a listed canvas to be used as a template, which others clone. Gallery, description, and tags Agents The MCP server at {base}/mcp (OAuth 2.1, 47 identity-scoped tools) covers everything the dashboard can do, with the same owner/editor/viewer checks. {base}/llms.txt and the agent skill ( {base}/skill.zip ) describe the whole surface for models. MCP server , llms.txt , Agent skill Admin Disable or restore any canvas, reassign owners, feature canvases in the gallery, block users, grant public-link publishing per account, and edit runtime settings (AI key and quotas, screenshots, design skin) without a restart. Configuration Operate SQLite or Postgres, local disk or S3-compatible storage, path or subdomain URLs, dev / proxy / oidc auth: each is a config swap, never a code change. Docker image and compose stack; pnpm backup , pnpm restore , pnpm purge . Install , Deploy , Security model Trust posture Identity comes from the server-side auth context, never from the client. In proxy mode only the trusted proxy may assert it. Secrets stay server-side. AI provider keys, deploy keys, and Connection protected headers never reach the browser. The backend is off by default; each primitive is a per-canvas opt-in. Every canvas starts Restricted. Only its owner or an editor widens access, and Public link is gated by an instance switch plus a per-account grant on the owner. Your infrastructure, your data. No telemetry, no phone-home. The security model states the threat model (a trusted org, not the hostile internet) and the tradeoffs, including path versus subdomain isolation. Status v1 is feature-complete. Milestones M1 through M9 (foundation, hosting and deploy, the dashboard, canvas management, the editor and version model, the five primitives and SDK, admin and hardening, the gallery, AI and realtime) are shipped, along with the post-v1 work in the table above: the access ladder, editor roles and ownership transfer, teams and auth-delegated invites, Shared discovery, the MCP server, staged uploads, custom slugs, clone-as-template, usage stats, optional screenshots and preview covers, design skins, the authoring capability, and admin-granted outbound Connections. Ops and packaging (M10) is the only open milestone. The Docker image and compose stack, vendor-neutral deploy docs, backup/restore tooling (with an automated round-trip test on both dialects), the security review, a secret scan in CI, and the starter examples have shipped. Still deferred: a single-VPS load test and a colleague pilot behind an identity-aware proxy. canvas-drop has not yet run under a real org's load; treat it as ready to self-host and evaluate, not as battle-tested. Not in v1: a CLI (the Deploy API, MCP server, and agent skill cover programmatic deploys), custom domains, multi-org tenancy, an audit-log viewer in the admin panel, KV change subscriptions, and realtime message history. Where to go next New here? Start with the Quickstart . Building a canvas with a backend? Read the SDK overview . Sharing with colleagues or adding editors? See Sharing & access . Running your own instance? See Self-hosting → Install , then Configuration . An AI agent? Read /llms.txt , then connect over the MCP server . Examples and URLs in these docs use {base} (your instance's base URL) and localhost placeholders. Substitute your own instance's address. ## Quickstart Quickstart For anyone running canvas-drop for the first time: by the end of this page you have an instance on your machine, a canvas at a live URL, and the three ways to put content on it (the dashboard, the editor, and the keyed Deploy API). The default profile needs no external services: SQLite, local disk storage, path URL mode, and dev auth that signs you in automatically. Run an instance locally You need Node 24 or newer ( .nvmrc says 24 ) and pnpm; the repo pins pnpm@11.0.9 in package.json . git clone https://github.com/markpasternak/canvas-drop.git cd canvas-drop pnpm install cp .env.example . env pnpm dev URL What it serves http://localhost:5173 The dashboard (Vite dev server with HMR). It proxies /api , /auth , /v1 , /docs , /llms.txt , /skill.zip , /welcome , and /og.png to the server. http://localhost:3000 The Hono server: management API, Deploy API, MCP at /mcp , docs, and the canvases themselves at /c/{slug}/ . Canvas URLs always point here, not at :5173 . curl http://localhost:3000/healthz # 200 {"status":"ok","db":"ok","version":"..."} (503 "degraded" until the DB answers) What pnpm dev does: it loads .env once (Node's --env-file-if-exists , so variables you export in the shell still win), seeds an empty database with sample canvases, then runs the server ( tsx watch ), the dashboard ( vite ), and the browser SDK build (esbuild --watch ) in parallel. Only pnpm dev reads .env ; a production process takes its config from the environment (see Configuration ). In production the server serves the built dashboard itself on :3000 ; the Vite server is dev-only. With CANVAS_DROP_DEV_SEED=0 , a fresh instance starts here: The sample data is 100 canvases with no files across seven owners, 70 of them yours, so the dashboard, Shared , and the gallery are populated on first boot. The seed runs only when the database has no canvases. Command Effect pnpm dev:stop Stops the whole tree (Ctrl-C works too). pnpm dev:restart restarts it. CANVAS_DROP_DEV_SEED=0 pnpm dev Starts without the sample canvases. pnpm reset:data Deletes the local SQLite file and the local storage directory. pnpm dev:fresh resets, reseeds, and starts. CANVAS_DROP_PORT=3001 pnpm dev Moves the server if :3000 is taken; the dashboard proxy follows. CANVAS_DROP_DASHBOARD_PORT moves the dashboard. Data lives at ./data/canvasdrop.db (SQLite) and ./data/storage (uploaded files). Both paths are the defaults of CANVAS_DROP_SQLITE_PATH and CANVAS_DROP_STORAGE_PATH . Who you are signed in as In dev auth mode every request is dev@example.com ( Dev User ), and that account is an admin: CANVAS_DROP_ADMIN_EMAILS defaults to the dev user in this mode. Override the identity with CANVAS_DROP_DEV_USER_EMAIL and CANVAS_DROP_DEV_USER_NAME in .env . dev mode refuses to boot when NODE_ENV=production ; a real instance uses proxy or oidc auth, and can swap in Postgres ( CANVAS_DROP_DB=postgres ) and S3-compatible storage ( CANVAS_DROP_STORAGE=s3 ). Each of those is a config change, not a code change: see Configuration and Install . Prefer Docker? docker compose up --build starts a demo stack at http://localhost:8080 : the app in real proxy auth mode behind oauth2-proxy and a bundled Dex identity provider, plus Postgres. Sign in as demo@example.com / canvasdrop . Only Caddy publishes a port; the app itself is unreachable from the host. Details and the production shape are in Install . Create a canvas Click Create canvas in the dashboard (or open /new ). Pick a source: Source What happens Paste HTML A single index.html , created and published in one step. Upload files Select files, a folder, or one ZIP, review the summary, then click Create and publish . Relative paths are preserved; archives are checked and extracted server-side. Use the API Mints an empty canvas plus a per-canvas key (shown once) and a ready-to-run curl command. The canvas stays unpublished until your first deploy. Review your content first, then set the title, workspace, and audience ( Restricted by default). Custom slug and backend controls are under Optional settings . Paste and upload publish when you explicitly click Create and publish . The result shows your link and lets you save the optional deploy key or continue without it. Every canvas gets a slug and a URL. The default slug is random ( quiet-otter-x7k2 ); a custom slug is lowercase letters, digits, and hyphens, 1 to 63 characters, no leading or trailing hyphen, with a reserved-word list ( api , admin , docs , mcp , and similar). Change it later under Settings → URL & routing → Change slug ; leave the field empty for a fresh random one. The old URL stops working immediately. URL mode Canvas URL Locally path (default) {base}/c/{slug}/ http://localhost:3000/c/quiet-otter-x7k2/ subdomain {slug}.{baseHost} not available on localhost ; for example quiet-otter-x7k2.canvases.example.com Add content A canvas is static files, served as-is. No build step runs on the server. The page at the canvas URL is the index.html at the root (a canvas whose only HTML file sits elsewhere serves that file instead): < html > < body > < h1 > Hello from my canvas Limits: 100 MB per canvas, 25 MB per file, 2 000 files. Each publish creates an immutable version; the last 10 are kept, and identical files are stored once. Give it a backend (optional) The browser SDK gives a canvas six fixed primitives (KV, files, AI, identity, realtime, and admin-granted Connections) with no keys in the page. Identity comes from the signed-in session, and the SDK works out which canvas it is running in from the page URL: < script src = "/sdk/v1.js" > < script type = "module" > const me = await canvasdrop. me (); await canvasdrop. kv . user . set ( "theme" , "dark" ); // this person's private preference The backend is off by default. Turn on Enable backend in the canvas's Backend tab, then the primitives you need: Key-value storage, File storage, AI, Realtime, and Authoring (a canvas that creates canvases; off by default per canvas and per instance, CANVAS_DROP_AUTHORING defaults to off ). Identity ( me() ) is on whenever the backend is on. A primitive is live only when the backend is on, its own toggle is on, and the operator has it available: AI needs CANVAS_DROP_AI_API_KEY set on the server, realtime needs CANVAS_DROP_REALTIME=on (the default). A call to a primitive that is not live throws CapabilityDisabledError . See the SDK overview and Capabilities . For comments or other authored items, configure a named collection inside Key-value storage. Each collection has its own access policy. Files can use named file groups ; Realtime uses named channels . See Data storage to choose shared values, private preferences or collections, and Permissions and defaults for the resource mapping. Connections are granted separately by an administrator. They let a canvas call one pre-approved HTTPS origin with controlled methods and protected headers—for example, a stock-data API that requires a server-side credential or fixed User-Agent . See Connections . Publish and share Two words mean different things here. Publish turns the editor's draft into a new immutable version. Deploy (folder, ZIP, paste, or the Deploy API) publishes a version directly, with no draft step; the New version button on every detail tab does this for an existing canvas. The Editor tab works on a mutable draft with autosave. Publish (⌘↵ or Ctrl+Enter) snapshots it into a new version and points the canvas URL at it. Two people editing the same file cannot silently overwrite each other; a stale save shows a conflict banner instead. The Versions tab lists every kept version: Make current re-points the live URL at it (in either direction), Restore copies one back into the draft, Download ZIP exports it. The Sharing and permissions page sets who can open the canvas, in two parts. People and teams with direct access names people (by email) and teams, each as a viewer or an editor ; that list always applies, and a brand-new email becomes live only after that person's first verified sign-in. General access says who else can open the URL (opening it to the org or the public needs a published version): General access Who else can open the URL Restricted (default) Nobody beyond you, your editors, and the people and teams on the list. A non-owner admin is treated like any member and gets a 404. Whole org Any signed-in member with the link. Public link Anyone with the link, static files only: the primitives are refused for anonymous visitors. Needs the instance's public-links switch (on by default) and the owner account's public-link permission (granted by default, revocable by an admin). On top of any choice you can add a password gate or a share expiry. People and teams on the list see the canvas in Shared at once; a Whole-org share is link-only until you turn on List for your org . The gallery is narrower: it lists only Public-link canvases and Whole-org canvases whose owners turn on List in the gallery . Revoking access takes effect on the next request. Details in Sharing & access . Deploy as an agent Scripts and agents deploy over HTTP with the per-canvas key and no dashboard session. The key is shown once at creation; an owner or editor can mint a new one under Settings → Regenerate key . curl -fsS -X PUT "http://localhost:3000/v1/canvases/{id}/deploy" \ -H "Authorization: Bearer $CANVAS_KEY " \ --data-binary @site.zip # 200 {"url":"http://localhost:3000/c/quiet-otter-x7k2/","version":1,"fileCount":3,"totalBytes":4096,"warnings":[]} The key operates only on its own canvas and never belongs in canvas files (a deploy that embeds one returns a warning). Deploys are limited to 10 per minute per canvas. The same surface offers GET /v1/canvases/{id}/files to read the live version back, POST …/rollback , POST …/unpublish , and a staged upload (begin, send only the blobs the server is missing, finalize) for large or frequently redeployed canvases. See the Deploy API . Agents can also connect once to the instance's MCP server at {base}/mcp (on by default; OAuth 2.1 through your own sign-in) for the full owner and editor surface, install the agent skill , or read the whole contract at /llms.txt . Where next Authors: Create & publish , The editor , Teams . Operators: Install , Configuration , Security model , Deploy . ## llms.txt llms.txt If you are an agent putting a canvas on a canvas-drop instance, start here. The instance serves this page and its companions (Overview, Quickstart, Capabilities, SDK overview, Deploy API, Runtime API, Error codes) as one plain-text file at {base}/llms.txt , meant to be dropped straight into context. It is public: served on the instance's base host ahead of the sign-in gateway, so you can read the contract before you hold any credential. {base} is the instance origin (a fresh local instance is http://localhost:3000 ). There are three ways in. Pick by what you hold. You hold Use Reach A per-canvas secret key ( cd_... ) Deploy API at {base}/v1/canvases/{id}/... that one canvas: deploy, read back, roll back, unpublish An MCP-capable host MCP at {base}/mcp (OAuth 2.1, no key to paste) every canvas the signed-in account owns or edits; 49 tools Code running inside a canvas page Browser SDK at {base}/sdk/v1.js , global canvasdrop the six fixed primitives for that canvas: KV, files, AI, identity, realtime, Connections Two verbs recur below. Publish turns the editor draft into an immutable version. Deploy (the Deploy API, deploy_canvas , the staged upload) publishes directly with no draft step. Both create a new version at the same URL; the last 10 versions are kept. Deploy with a key Get a canvas and its key. A person creates the canvas on the dashboard's Create page with Use the API , or you call create_canvas over MCP. Either mints the canvas plus a one-time secret key, shown once. PUT a ZIP with index.html at its root: curl -fsS -X PUT "{base}/v1/canvases/{id}/deploy" \ -H "Authorization: Bearer $CANVAS_KEY " \ --data-binary @site.zip # 200 {"outcome":"published","url":"...","version":7,"versionId":"01J…","releaseId":null,"publicationToken":"9f2c…","fileCount":12,"totalBytes":348201,"warnings":[]} Verify through the server, not the URL: curl -fsS "{base}/v1/canvases/{id}/files" -H "Authorization: Bearer $CANVAS_KEY " # 200 {"version":7,"fileCount":12,"files":[{"path":"index.html","size":1204,"mime":"text/html","hash":"..."}]} curl -fsS "{base}/v1/canvases/{id}/files?path=index.html" \ -H "Authorization: Bearer $CANVAS_KEY " | sha256sum # the live file's raw bytes; compare the hash with what you shipped The deploy publishes a live version immediately. {id} is the canvas id, not the slug. The Deploy API host is CANVAS_DROP_API_BASE_URL , which defaults to the instance base URL and can differ from the dashboard host; create_canvas and the dashboard both return the exact endpoints for the canvas, so never guess the host. The key is verified per canvas. A missing or unknown key, including a key for an archived, disabled, or deleted canvas, answers 401 {"error":"unauthorized"} ; a key for a different canvas answers 403 . Validation failures answer {"code", "message","path"} : 400 on the ZIP path with EMPTY_DEPLOY , TOO_MANY_FILES , FILE_TOO_LARGE , CANVAS_TOO_LARGE , INVALID_ZIP , INVALID_PATH , ZIP_SLIP_REJECTED , or ZIP_BOMB_REJECTED ; a ZIP body over the canvas cap is refused before it is read with 413 CANVAS_TOO_LARGE . Deploys, staged begin/finalize, and rollbacks are throttled at 10 per minute per canvas ( 429 {"error":"rate_limited"} with Retry-After ). Companion routes, same Bearer key: Route Purpose GET /v1/canvases/{id} {id, slug, url, title, status, publicationState, accessMode, currentVersionId, publicationToken, currentVersion: {id, number, releaseId, createdAt} | null} ( accessMode : restricted | whole_org | public_link — who else can open it beyond the people-and-teams list) GET /v1/canvases/{id}/versions {versions: [{id, number, source, status, createdBy, createdAt, fileCount, totalBytes, releaseId, current}]} GET /v1/canvases/{id}/files the live manifest as JSON; ?path= returns that file's raw bytes; 404 NOT_PUBLISHED before the first deploy POST /v1/canvases/{id}/rollback body {"version": 6} ; makes that ready version current and returns {url, version} ; 404 when no ready version has that number POST /v1/canvases/{id}/unpublish back to Draft: {url, publicationState: "draft", currentVersionId: null} ; 409 CANNOT_UNPUBLISH when not published For large or repeat deploys, the staged flow sends only changed blobs: POST /v1/canvases/{id}/uploads with {"manifest":[{"path","hash","size"}]} (sha256 hex) returns {uploadId, missingHashes} ; PUT /v1/canvases/{id}/uploads/{uploadId}/blobs/{hash} with the raw bytes of each missing blob returns 204 ; POST /v1/canvases/{id}/uploads/{uploadId}/finalize returns the same DeployResult . A session lives 15 minutes and finalizes once. Staged errors use the same {code, message} shape at a mapped status: size caps 413 ; an unknown or foreign uploadId 404 UPLOAD_HANDLE_INVALID ; UPLOAD_ALREADY_FINALIZED and UPLOAD_IN_PROGRESS 409 ; UPLOAD_EXPIRED , UPLOAD_MISSING_BLOB , BLOB_HASH_MISMATCH , INVALID_MANIFEST 400 . Limits: 100 MB per canvas, 25 MB per file, 2 000 files. Full contract: Deploy API . Two publishers, one canvas. When a local tool and a CI job can both ship the same build, add ?releaseId=&expectedPublicationToken= to PUT .../deploy (or the same two fields in the staged begin/finalize bodies). Outcomes: 200 outcome:"already_current" (your release is live already; nothing created), 409 PUBLICATION_CHANGED (the publication changed since you read the token; body carries current ), 409 RELEASE_NOT_CURRENT (your release exists only in history; roll back to it or ship a new release), else a normal published result with the new token. Every publication change — any deploy, an editor publish, a rollback, an unpublish — rotates the token and never reuses a value. Read back before deploying and treat each conflict as a reassess signal, not a retry; Canvas Drop does not know which commit is newest. Recipe and examples: Coordinate two publishers . Connect over MCP Add {base}/mcp to an MCP-capable host. First use runs OAuth 2.1 against canvas-drop itself (RFC 8414/9728 discovery, Dynamic Client Registration, PKCE S256 ), sends you through the instance's normal org sign-in, and returns a 1 h access token plus a rotating refresh token. Every call re-checks that the account is still active. Transport is Streamable HTTP, stateless; calls are limited to 120 per minute per account and request bodies to 110 MiB. An instance with CANVAS_DROP_MCP=off has no /mcp endpoint at all. A first session, as tool calls: whoami {} -> { id , email, name, orgs, teams, isGuest } create_canvas { "title" : "Retro board" } -> { id , slug, url, apiKey, deploy, ... } apiKey is returned once deploy_canvas { "id" : "" , "files" : [{ "path" : "index.html" , "content" : "

Hi

" }] } -> { outcome : "published" , url, version : 1, versionId, releaseId : null , publicationToken, fileCount : 1, totalBytes, warnings : [] } get_canvas_file { "id" : "" , "path" : "index.html" } -> { version, path, size, mime, hash, encoding : "utf8" , content } Scope and roles. Tools act on the canvases the account owns or edits. A canvas you hold no role on reads as canvas not found , for admins too. Each tool has a minimum role: any (identity, lists, create, teams), editor (everything on a canvas), or owner ( delete_canvas , transfer_canvas ). An editor calling an owner-only tool, or setting guestAiEnabled / guestAiCap through update_canvas , gets OWNER_ONLY: ... ; get_canvas echoes the list as ownerOnlyActs: ["delete", "transfer", "guest_ai"] . id parameters are canvas ids (team tools take team ids), never slugs. Results are JSON in a text content block; failures are isError: true with the text CODE: message . An admin-disabled canvas stays readable but every mutation fails DISABLED: ... ; an archived canvas refuses deploy and publish with NOT_ACTIVE . Group Tools Identity, lists, create ( any ) whoami , list_canvases , list_shared_canvases , create_canvas , clone_canvas Read ( editor ) get_canvas , list_versions , get_canvas_file , get_canvas_usage , list_access , search_people Deploy ( editor ; publishes live immediately) deploy_canvas , begin_deploy , add_files , finalize_deploy — optional releaseId / expectedPublicationToken on deploy_canvas , begin_deploy , finalize_deploy coordinate two publishers (see Deploy with a key) Lifecycle ( editor unless marked) rollback_canvas , unpublish_canvas , delete_version , preview_version_prune , prune_versions , archive_canvas , unarchive_canvas , delete_canvas (owner), transfer_canvas (owner) Settings ( editor ) update_canvas , set_capabilities , set_canvas_slug , set_canvas_preview , regenerate_deploy_key ; list_canvas_connections reads the admin-granted profiles Sharing ( editor ) grant_access , invite_to_canvas , revoke_access , set_access_role Draft loop ( editor ) get_draft , read_draft_file , write_draft_file , delete_draft_file , rename_draft_file , publish_draft , restore_draft Teams ( any ) list_teams , create_team , rename_team , delete_team , add_team_member , remove_team_member , cancel_team_invite , list_team_members Working notes: Typical flow: create_canvas , deploy, verify. create_canvas returns the canvas view, the one-time apiKey , and a deploy block ( apiBase , zipUpload , staged.begin / stageBlob / finalize , readback , and a ready-to-run curl with the key filled in). A new canvas is empty and Restricted ( private in the API); its URL serves content only after a deploy. Prefer curl for bytes. deploy_canvas takes exactly one of zipBase64 or files: [{path, content, encoding?}] ( utf8 default, or base64 ) and, like add_files , inlines file content into the model context. When you can run shell commands, use the deploy block's curl, whole ZIP or staged; keep deploy_canvas for a small first publish without a shell. Verify through the server, not the URL. The live URL is behind org sign-in: a signed-out GET is redirected to login ( oidc ) or answered 401 ( proxy , dev ) unless the canvas is on the public_link rung. Check the returned {version, fileCount} , list_versions , the readback URL, or get_canvas_file (no path lists the manifest; a path returns the content as utf8 or base64 ; over 256 KiB it returns truncated: true instead). list_canvases returns owned and edited canvases, each with role ( "owner" or "editor" ) and owner ; the role parameter ( owned or edited ) narrows. query is a forgiving filter over title, description, tags, and slug (case, accent, and whitespace insensitive; multiple words AND). tags is any-match. sort is updated (default), created , title , or popular (views over the last 30 days). limit defaults to 50, max 100. list_shared_canvases lists canvases you can open but do not manage: a direct grant, a listed team share, or a listed whole-org share. Draft loop: write_draft_file , delete_draft_file , and rename_draft_file take expectedHash (the file's current hash from get_draft / read_draft_file , or "none" for a path you expect absent); a mismatch fails DRAFT_CONFLICT with the current hash and last writer. Without expectedHash the write still fails DRAFT_CONFLICT when a different user wrote the file last, so two editors never overwrite each other silently. write_draft_file with create: true refuses an existing path ( PATH_EXISTS ). publish_draft snapshots the draft into a live version and returns {version, versionId, fileCount, totalBytes} . update_canvas fields: title (max 200), description (max 2000, or null ), tags (max 20, each max 50 chars; one set serves list filtering and the gallery), access , discoverability ( link_only by default; listed shows a Team or Whole-org canvas in Shared and makes a Whole-org canvas gallery-eligible), teamIds , password (or null to clear), sharedExpiresAt (unix ms, or null ), spaFallback , previewMode ( auto or off ; set_canvas_preview with an image sets custom ), linkPreview (a non-public canvas unfurls with its title, description and custom cover instead of the sign-in card), galleryListed , galleryTemplatable , and the owner-only guestAiEnabled / guestAiCap . Refusals you will meet: SHARE_REQUIRES_PUBLISH (sharing needs a published canvas), ORG_REQUIRED , PUBLIC_LINKS_DISABLED (instance switch off), PUBLIC_NOT_ALLOWED (the owner may not publish publicly), PUBLIC_LINK_OWNER_GATED (an editor asked for public_link on such a canvas), TEAM_REQUIRED / TEAM_FORBIDDEN . set_capabilities takes backendEnabled , kv , files , ai , realtime , authoring . People: list_access entries are viewer or editor ; the owner also gets transferCandidates . grant_access takes exactly one of email or teamId plus role ; a new email is pending until its first verified sign-in through the instance's identity provider; legacy guests are viewers only ( GUEST_VIEWER_ONLY ). set_access_role changes an entry's role. transfer_canvas takes a user id (never an email) of an existing editor ( NOT_ELIGIBLE otherwise) and returns {ok, canvas, previousOwnerEditor, publicLinkReverted} : the previous owner keeps editor access while their account is active, and a public_link rung reverts when the new owner lacks the entitlement. Tenancy: whoami returns orgs , teams , and isGuest (true only when an org boundary is configured and you belong to none). create_canvas.orgId : omit to default to your only org, pass null for a personal canvas; an org you do not belong to fails ORG_FORBIDDEN . Under an active org boundary the whole_org rung needs an org-homed canvas ( ORG_REQUIRED ). Versions: list_versions carries a downloadUrl per version ( {base}/mcp/canvases/{id}/versions/{n}/download , a ZIP fetched with the same MCP access token as a Bearer). delete_version removes a non-current version only ( CURRENT_VERSION otherwise). Parameters and return shapes for every tool: MCP server . Browser SDK inside a canvas < script src = "/sdk/v1.js" > < script type = "module" > const me = await canvasdrop. me (); // { id, email, name, avatarUrl, kind, canvasRole, teams, permissions } await canvasdrop. kv . user . set ( "last-visit" , Date . now ()); const views = await canvasdrop. kv . user . increment ( "visits" ); // 1 on the first call, then 2, 3, ... One global, window.canvasdrop ; there is no cd alias and no version property. Zero config: the slug and API base are read from the page URL ( /c/{slug}/ in path mode, {slug}.{host} in subdomain mode), every call goes to {apiBase}/v1/c/{slug}/... with the session cookie, and no key ever reaches the page. The root-relative src resolves on the canvas's own origin in both modes. The canvas must have Backend switched on (see Capabilities below); /sdk/v1.js sits behind the same sign-in as the canvas. me() returns { id, email, name, avatarUrl, kind, canvasRole, teams, permissions } ; teams are this canvas's team grants the caller belongs to, each { id, name, role } . kind is "member" ; "guest" appears only for retained legacy guest sessions, since new Add person grants materialize as signed-in users. kv (shared) and kv.user (per viewer, keyed server-side) have the same five methods: get(key) returns the value or null ; set(key, value) stores any JSON except null ; delete(key) is idempotent; list({ prefix?, cursor?, limit? }) returns { entries: [{ key, value }], nextCursor } ( limit 1 to 1000, default 100); increment(key, by = 1) returns the new number and fails NOT_NUMERIC on a non-numeric value. Key max 512 bytes, value max 64 KiB, 10 000 shared and 1 000 per-user keys per canvas. files : upload(file) returns { id, name, size, url } with an absolute url ; list() returns [{ id, name, size, mime, createdAt }] ; delete(id) ; url(id) is synchronous. 25 MiB per file, 1 GiB per canvas. ai : chat(messages, { model, system?, maxTokens? }) returns { text, usage: { inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens }, cost } ; stream(messages, options) returns an AsyncIterable of text deltas with no usage. model is required and must be on the instance allowlist ( MODEL_NOT_ALLOWED ). Messages are { role: "user" | "assistant", content } ; the system prompt goes in options.system ; maxTokens defaults to 1024, cap 8192. The provider key stays server-side. realtime.channel(name) returns a handle with publish(event, data) (fire and forget, buffered while reconnecting), subscribe(handler) where handler receives { event, data, from: { id, name, canvasRole } } and the call returns void , unsubscribe() (clears every handler on the channel), presence() resolving to [{ id, name }] , onPresence , onJoin , onLeave , and close() . One shared socket per page with automatic reconnect; 30 connections per canvas, 16 KiB per message. There is no generic .on(...) . connections.fetch(profile, path, init?) makes a bounded server-side request through an admin-granted profile. path must be root-relative; init.method must be one the admin selected. The profile fixes one exact HTTPS DNS origin and adds write-only protected headers last. Upstream 4xx/5xx remain normal Response objects; platform policy failures throw. Public-link viewers remain static-only. canvases (the authoring capability, off by default per canvas and per instance): publish , update , list , revoke let a signed-in viewer create and manage a share canvas from a page. Full signatures and types: SDK overview . Raw routes: Runtime API . Sharing and access One access rung per canvas, set on the Share tab or with update_canvas.access . Access is evaluated on every request, so a revoke, an expiry, or a role change takes effect on the next request; a canvas you may not open reads as 404 . The people-and-teams list ( grant_access / revoke_access ) always applies: a viewer or editor row, or membership of a granted team, opens the canvas at every access value. access (General access) says who else can: access Who else can open it private nobody beyond the owner, the editors, and the list ( Restricted ). specific_people and team are legacy aliases of this value — accepted, stored, treated identically whole_org any signed-in org member; discoverability: "listed" shows it in Shared for them and makes it gallery-eligible public_link anyone with the link, while the instance switch is on and the owner may publish publicly ( canPublishPublic ); static only for everyone except the owner and editors: every primitive answers 403 STATIC_ONLY An editor grant, direct or through an editor-role team, also manages the canvas. Editors skip the password gate and the share expiry. A password lock answers 403 PASSWORD_REQUIRED on the runtime API until the viewer passes the gate; a share expiry ( sharedExpiresAt ) turns other viewers away with 404 SHARE_EXPIRED once it passes. Details: Sharing & access . Capabilities backendEnabled is off by default. With it on, kv , files , ai , and realtime default on and authoring defaults off; each toggles independently. Effective rule: identity = backend ; kv = backend && capKv ; files = backend && capFiles ; ai = backend && capAi && provider key configured ; realtime = backend && capRealtime && CANVAS_DROP_REALTIME=on (the default); authoring = backend && capAuthoring && CANVAS_DROP_AUTHORING=on (default off ). An off feature answers 403 {"code":"CAPABILITY_DISABLED","capability":"kv", "backendEnabled":false,"reason":"backend_off"|"feature_off"|"operator_disabled", "hint":"..."} ; the SDK throws CapabilityDisabledError . Toggle from the canvas's Backend tab, PATCH /api/canvases/{id}/capabilities , or set_capabilities . Details: Capabilities . Errors Every failure carries a stable string code ; branch on it, not on message text. The SDK throws CanvasdropError with .code and .status , plus six subclasses: NotAuthenticatedError (any 401), NotFoundError (any 404), CapabilityDisabledError (403 CAPABILITY_DISABLED , with .hint ), QuotaExceededError ( QUOTA_EXCEEDED , GUEST_AI_CAP , KEY_LIMIT , and every 413 size code), PublishFailedError (502, from canvasdrop.canvases.publish , with the new canvas .id ), and UpdatePartialError (502, from canvasdrop.canvases.update when the settings saved but the bundle deploy failed, with .stage and .current ). Every other code arrives as a plain CanvasdropError . kv.get returns null for a missing key instead of throwing. Codes you will meet most on the runtime API: 401 {"error":"unauthorized"} (no session; oidc redirects to login instead), NOT_FOUND / ARCHIVED / OWNER_ONLY / SHARE_EXPIRED 404, DISABLED 403, PASSWORD_REQUIRED 403, STATIC_ONLY 403, CAPABILITY_DISABLED 403, CROSS_CANVAS_FORBIDDEN / CROSS_SITE_FORBIDDEN 403, MODEL_NOT_ALLOWED 403, INVALID_BODY 400, KEY_TOO_LARGE / VALUE_TOO_LARGE / FILE_TOO_LARGE 413, KEY_LIMIT 409, NOT_NUMERIC 409, QUOTA_EXCEEDED 429 (409 on files), CONNECTION_LIMIT 429, RATE_LIMITED 429, AI_STREAM_TRUNCATED / AI_UPSTREAM_ERROR 502. Full table: Error codes . Rate limits Defaults; each is an env var the operator can change. Surface Default Keyed by Env var Deploy API ( deploy , uploads begin, finalize , rollback ) 10/min canvas CANVAS_DROP_RATELIMIT_DEPLOY_PER_MIN Runtime API /v1/c/{slug}/... 120/min user + canvas CANVAS_DROP_RATELIMIT_CANVAS_API_PER_MIN Runtime AI /v1/c/{slug}/ai/... 10/min user CANVAS_DROP_RATELIMIT_AI_PER_MIN Runtime Connections 60/min per actor/profile; 600/min per profile actor + canvas + profile; profile CANVAS_DROP_CONNECTIONS_ACTOR_PER_MIN , CANVAS_DROP_CONNECTIONS_PROFILE_PER_MIN MCP /mcp 120/min account CANVAS_DROP_RATELIMIT_CANVAS_API_PER_MIN For a packaged, installable version of this guidance, see the Agent skill . Runtime permission contract canvasdrop.me() exposes server-derived canvasRole and permissions . Shared kv.set/delete/increment and shared file mutations require owner/editor. Use kv.user for private preferences and submissions.get/set/delete(collection) for the caller's own vote/form; submissions.list/remove/clear require owner/editor. Do not use client-supplied author IDs or shared counters for viewer votes. Private attachments use files.upload(file, { scope: "submission" }) . AI and Connections default to owner/editor audiences; opt viewers in using set_capabilities fields aiAudience / connectionsAudience: "viewers" . Authoring admits every member by default; set authoringAudience: "editors" to limit it. Unconfigured realtime shared publishing requires owner/editor; participants: channels allow attributed viewer messages, visible to all subscribers. Role denials are PERMISSION_DENIED and never disappear by hiding UI controls. For cleanup use preview_version_prune then prune_versions with the exact returned numbers and expectedVersionIds ; never infer bytes recovered from an estimate. For multiple authored items use kv.collection(name) after configuring the resource through set_capabilities.runtimePolicy . Five data/file presets: personal, submissions, contributions, managed, collaborative. Default modes initialize newly added resources; existing policies remain explicit. expectedRuntimePolicy must match get_canvas.runtimePolicyRevision (initially null), or saving fails with POLICY_CONFLICT. Author identity is immutable and server-derived. Read/create/update/ delete/increment can be customized; mutations also require read. File attachments use {collection, recordId} and inherit the record's rights. Named channels configure subscribe/publish/seePresence/participatePresence, and Connections configure audience and methods. me().resources exposes effective rights. Full schema and examples: /docs/sdk/permissions . ## Capabilities Capabilities Give a canvas a backend, one feature at a time, and know exactly when a call from the page will work. This page is for a canvas's owner or editor. A canvas is static until you switch Enable backend on in its Backend tab. With the backend on, five features toggle independently: KV, files, AI, realtime, and authoring. Identity ( me() ) has no toggle; it is on whenever the backend is on. Outbound Connections also has no owner-controlled feature toggle: each reusable profile is granted to the canvas by an instance administrator and remains gated by the same Backend master switch. How features and resources fit together The six runtime primitives are backend features. Three support named resources with their own settings: Key-value storage → collections of authored records, File storage → file groups of standalone uploads, and Realtime → channels for messages and presence. Each collection, group or channel can have different permissions. A policy controls access to a resource; it does not define which records belong to it. Attachments inherit their parent record's permissions. AI uses audience and budget settings, Connections uses per-profile audience/method rules within administrator grants, and Identity reports the caller's role and rights. Authoring is a separate capability for creating other canvases. Enable the feature first, then configure the resources your app uses and reference their names in its code. Adding a comments collection configures its access; the app still implements commenting and creates the records. For simple settings or private preferences, the data feature also provides fixed shared and personal key-value scopes. Start with Data storage and Permissions and defaults . Turn on the backend Open the canvas and go to the Backend tab. Switch Enable backend on. It is off by default. You can also set it when you create the canvas: Enable backend (optional) on the create page, or "backendEnabled": true on POST /api/canvases and POST /api/canvases/paste . Check the feature toggles. Key-value storage , File storage , AI , and Realtime are on by default, so they go live as soon as the backend is on (where the instance supports them). Authoring starts off and stays off until you turn it on. Then call the features from the page through window.canvasdrop , without browser secrets. The simple personal-storage example needs no named resource: < script src = "/sdk/v1.js" > < script type = "module" > const me = await canvasdrop. me (); // on whenever the backend is on await canvasdrop. kv . user . set ( "last-visit" , Date . now ()); // needs Key-value storage on Agents and scripts flip the same switches. The MCP tool set_capabilities and PATCH {base}/api/canvases/{id}/capabilities take the same body: any subset of backendEnabled , kv , files , ai , realtime , authoring as booleans, and the audience fields described below. Omitted fields are unchanged. { "backendEnabled" : true , "realtime" : false } The response is the canvas view. Every canvas view (the management API and the MCP get_canvas tool included) carries two objects: capabilities (what is stored) and effective (what runs right now, after the instance switches below are applied). Owners and editors can change capabilities; viewers cannot. Every change is audited ( capabilities_update , with the list of changed fields) and applies on the next request. Turning the backend or realtime off also drops the canvas's live realtime sockets. A canvas an admin has disabled refuses the change with 409 DISABLED . The toggles Backend tab row Key Stored default What it gives the canvas SDK Enable backend backendEnabled off The master switch; nothing below runs without it Identity (no toggle) identity follows the backend The signed-in viewer: id, email, name, avatar me() Key-value storage kv on Shared values, private preferences and authored collections kv File storage files on Upload, list, delete, and serve files files AI ai on Server-side model calls; no provider key in the page ai Realtime realtime on Ephemeral pub/sub and presence over WebSockets realtime Connections no feature flag no grants Bounded requests to exact HTTPS origins an admin attached to this canvas connections Authoring authoring off A signed-in viewer creates and manages canvases from the page, as themselves canvases The feature toggles are disabled in the UI while the backend is off. Their stored values are kept, so switching the backend back on restores the same set. The Identity row reads Always on when the backend is on and Off when it is not. When a feature is effective A feature runs only when every gate in its row is open: the backend, its own toggle, and (for AI, realtime, and authoring) an instance switch the operator controls. The server applies this rule on each request; the Backend tab shows the outcome, and effective in the API is the same answer. Feature Backend on Its toggle on Instance switch Identity ( me() ) yes none none KV yes yes none Files yes yes none AI yes yes An AI provider key is configured: CANVAS_DROP_AI_API_KEY , or the Provider API key an admin sets in Admin → Settings Realtime yes yes CANVAS_DROP_REALTIME=on (the default) Connections yes an enabled profile is attached CANVAS_DROP_CONNECTIONS_ENCRYPTION_KEY is available when protected headers are configured Authoring yes yes CANVAS_DROP_AUTHORING=on (default off ), or Authoring enabled set by an admin in Admin → Settings KV and files have no instance switch. Their toggles enable the feature; the caller still needs the role required by the operation. When your toggle is on but the instance switch is off, the toggle stays on and the row is labelled Disabled by your administrator for this instance. The AI key and the authoring switch are read per request, so an admin's change applies immediately. Realtime follows CANVAS_DROP_REALTIME as set when the server started; the admin panel shows it but cannot change it. Limits Each feature has fixed ceilings. Exceeding one returns the error named in the last column, not CAPABILITY_DISABLED ; see error codes . Feature Limit Admin-adjustable Error KV 64 KB per value, 512 bytes per key; 10 000 shared keys and 1 000 per-viewer keys per canvas the key counts VALUE_TOO_LARGE , KEY_TOO_LARGE , KEY_LIMIT Files 25 MB per file, 1 GB per canvas both FILE_TOO_LARGE , QUOTA_EXCEEDED AI Models on the allowlist ( CANVAS_DROP_AI_MODELS ); spend caps of CANVAS_DROP_AI_USER_DAILY_USD (default 5 ) per viewer per day and CANVAS_DROP_AI_CANVAS_MONTHLY_USD (default 50 ) per canvas per month allowlist and both caps MODEL_NOT_ALLOWED , QUOTA_EXCEEDED Realtime 30 concurrent connections per canvas, 16 KB per message no CONNECTION_LIMIT (socket close 4429 ) Connections 8 KiB URL; 32/16 KiB caller headers; 256 KiB request; 2 MiB response; 10 s; 3 redirects; 60/min actor+canvas+profile; 600/min profile; 5 concurrent/canvas; 50/process env-only REQUEST_TOO_LARGE , RESPONSE_TOO_LARGE , UPSTREAM_TIMEOUT , CONNECTION_RATE_LIMIT , CONNECTION_LIMIT When a feature is off A call to a feature that is off fails with a 403 whose body names the gate that failed: { "code" : "CAPABILITY_DISABLED" , "capability" : "kv" , "backendEnabled" : false , "reason" : "backend_off" , "hint" : "This canvas's backend is off (the master switch, off by default). Turn it on in the dashboard Backend tab, the set_capabilities MCP tool, or PATCH /api/canvases/:id/capabilities {\"backendEnabled\": true}." } reason is backend_off , feature_off , or operator_disabled ; hint says what to turn on. The SDK throws a CapabilityDisabledError ( err.code === "CAPABILITY_DISABLED" , err.status === 403 ) and exposes the server hint as err.hint , which is also the error message: try { await canvasdrop. kv . set ( "count" , 1 ); } catch (err) { if (err. code === "CAPABILITY_DISABLED" ) console . log (err. hint ); else throw err; } Realtime reports the same condition over the socket: a connection opened while realtime is off receives one { "type": "error", "code": "CAPABILITY_DISABLED", "capability": "realtime" } frame and is closed with code 4403 . A socket that is already open when realtime is turned off is closed with 4403 too. The SDK turns both into the same CapabilityDisabledError and does not reconnect. See error codes for the full list. Public links are static-only On the Public link rung ( public_link ) the server serves the canvas's files to anyone with the URL and refuses every primitive with 403 STATIC_ONLY for everyone except the canvas's owner and editors. Signed-in org members are refused too. The Backend tab shows a warning when a public-link canvas has its backend on. If the canvas needs a backend for its audience, share it on a more restricted rung; see Sharing & access . Authoring Authoring lets a signed-in org member viewing your canvas create a new canvas from the page, as themselves, through canvasdrop.canvases.publish(...) . The new canvas is created under the viewer's own account and appears in their dashboard; they can update , list , and revoke it later. Legacy guest sessions and public-link visitors cannot use it. Because it mints canvases, authoring is the one feature whose stored flag starts off, and its instance switch ( CANVAS_DROP_AUTHORING ) is off by default as well. The operator also sets the policy a publish is checked against: Policy Env var Default Canvases one viewer may publish per day CANVAS_DROP_AUTHORING_USER_DAILY_MAX 20 Canvases one viewer may publish in total CANVAS_DROP_AUTHORING_USER_TOTAL_MAX 200 Access rungs a publish may request CANVAS_DROP_AUTHORING_ALLOWED_RUNGS private,specific_people,whole_org,public_link Longest allowed share expiry, in days CANVAS_DROP_AUTHORING_MAX_EXPIRY_DAYS 0 (no cap) Whether a share expiry is required CANVAS_DROP_AUTHORING_REQUIRE_EXPIRY false An admin can change the two quotas at runtime in Admin → Settings; the rung and expiry policy is env-only ( specific_people is a legacy alias of private ). Requiring an expiry applies only to Whole org and Public link publishes. Restricted publishes ( private , specific_people , or team ) do not require one. See the authoring SDK reference . Clones start static Duplicating a canvas, or using a gallery template, creates a new canvas with the backend off and the feature flags at their defaults (KV, files, AI, and realtime on; authoring off). KV data, files, and usage are not copied. The new owner turns the backend on when they need it. Why off by default Capabilities are enforced by the server, per request, from the signed-in session: the canvas can ask, the server decides. Canvas files never carry a secret, and a static canvas has no backend surface at all. Turning a capability on is normally a per-canvas choice its owner or an editor makes. Connections is the deliberate exception: only an admin defines profiles and attaches or revokes their grants, while owners and editors inspect the non-secret authority in the Backend tab. Who can use the backend Feature switches control availability; canvas roles control each operation. Raw shared KV and shared files require owners/editors for mutations. Configured collections and file groups support Personal, Private submissions, Shared contributions, Managed content and Collaborative content. Viewers can contribute and manage their own records without permission to edit the canvas itself. me().permissions and me().resources report effective rights. A denied operation returns PERMISSION_DENIED . Participation and permissions starts with a default for new resources: Read only, Participation or Collaboration. Add a named collection, file group or channel using that default. Expand Advanced permissions only when a resource needs a different preset or operation-level rights. Review the affected resources before saving. Changing the default preserves existing policies. Attachments inherit their record's policy. Full policy and API guide . The Backend tab also has AI access and Connection access , each set to Owners and editors by default. Choose All signed-in viewers to permit an interactive audience to use that backend. Authoring access works the same way for page-driven authoring, but defaults to All signed-in viewers so existing canvases keep working; choose Owners and editors to limit creating, updating, listing and unpublishing shares from this page to the canvas's managers. Public links remain static-only for viewers. An audience choice cannot enable a disabled feature, supply an AI key, grant a Connection profile, or bypass quotas and legacy guest restrictions. PATCH /api/canvases/{id}/capabilities and MCP set_capabilities accept aiAudience , connectionsAudience and authoringAudience , each "editors" or "viewers" . The management canvas view exposes all three alongside capabilities and effective . Omitted values stay unchanged. Only owners and editors can change them. The same endpoint and MCP tool accept runtimePolicy plus the exact previous runtimePolicyRevision as expectedRuntimePolicy (initially null). A stale or missing revision returns POLICY_CONFLICT . Per-Connection policies can override the default audience and narrow methods; administrator grants still bound them. { "backendEnabled" : true , "ai" : true , "aiAudience" : "viewers" , "connectionsAudience" : "editors" , "authoringAudience" : "editors" } For an existing installation, read the runtime upgrade guide before deploying these defaults. ## Overview Browser SDK You have a static canvas and want it to keep data, accept files, call a model, know who is looking, or sync between viewers. One