Authoring
Let a signed-in viewer of your canvas create and manage canvases from the page —
as themselves, with real ownership and no secret in the browser. A share created here
is a managed artifact: you publish it once, update it in place (same URL) as its
content changes, list your shares with their live status, and revoke one when it's
done. The canvasdrop.canvases namespace wraps publish, update, list, and revoke.
This is the authoring capability. It is off by default and higher-privilege than the other primitives: the owner turns it on in the Backend tab and the operator must enable it for the instance. Guests and public-link visitors can't use it — creation needs a signed-in org member.
// Create a durable share once.
const share = await canvasdrop.canvases.publish({
title: "Team roadmap — Q3 snapshot",
access: "public_link", // "private" | "specific_people" | "whole_org" | "public_link" | "password"
tags: ["roadmap"],
metadata: { sourceApp: "product-roadmap", sourceKind: "roadmap-share", theme: "light" },
expiresAt: Date.now() + 7 * 864e5, // optional; the operator may require/limit it
bundle: zipBlob, // a static-site zip (Blob or ArrayBuffer)
});
// share.url is the live canvas; it now appears in the viewer's own dashboard.
// Later — update IN PLACE. The URL never changes; a new version is deployed.
await canvasdrop.canvases.update(share.id, {
bundle: newZipBlob, // omit to change only settings/metadata
metadata: { ...share.metadata, itemCount: 42, generatedAt: Date.now() },
});
// The management view — filter to this app's shares.
const mine = await canvasdrop.canvases.list({ sourceApp: "product-roadmap" });
// → [{ id, url, title, status, access, expiresAt, updatedAt, metadata, ... }]
// Retire a share. Its URL goes dead, but it STAYS in list() as status "revoked".
await canvasdrop.canvases.revoke(share.id);
Methods
| Method | Returns |
|---|---|
publish(options) |
Promise<AuthoredCanvas> — creates, deploys, and configures a new share |
update(id, options) |
Promise<AuthoredCanvas> — replaces the bundle and/or settings at the same URL |
list(filter?) |
Promise<AuthoredCanvas[]> — the viewer's shares (incl. revoked/expired), optionally filtered |
revoke(id) |
Promise<void> — make a share's public URL unavailable; it stays listed as revoked |
AuthoredCanvas
What publish, update, and each list entry return — a share plus its management state:
| Field | Type | Notes |
|---|---|---|
id / url |
string |
stable id and public URL (the URL never changes across updates) |
title / tags |
string / string[] |
|
access |
string |
the current access rung |
status |
"live" | "expired" | "revoked" | "private" |
derived; precedence revoked › expired › private › live |
createdAt / updatedAt |
number |
unix ms |
expiresAt / revokedAt |
number | null |
share expiry; when it was revoked |
createdBy |
string |
the creator (owner) id |
version |
string | null |
the current version id — advances on every bundle deploy (a change signal) |
bundleUpdatedAt |
number |
last-write stamp (deploy or settings change) |
sourceApp / sourceKind |
string | null |
pulled from metadata (the filterable dimensions) |
metadata |
Record<string, unknown> |
your free-form blob, round-tripped verbatim |
publish options
| Field | Type | Notes |
|---|---|---|
title |
string |
required |
bundle |
Blob | ArrayBuffer |
required — the static-site zip |
slug |
string? |
omit for a readable-random slug |
tags |
string[]? |
|
access |
"private" | "specific_people" | "whole_org" | "public_link" | "password"? |
operator restricts the allowed set; "password" = a public link protected by password |
password |
string? |
required when access is "password" |
expiresAt |
number? |
unix ms; the operator may require an expiry and cap how far out it may be |
metadata |
Record<string, unknown>? |
free-form structured state (sourceApp/sourceKind/theme/…), bounded in size |
update options
Every field is optional — omitted fields leave the share unchanged. password and
expiresAt accept null to explicitly clear them. Omit bundle to change only
settings/metadata (the URL and current version stay put).
| Field | Type | Notes |
|---|---|---|
bundle |
Blob | ArrayBuffer? |
a new static-site zip → a new immutable version at the same URL |
title |
string? |
|
tags |
string[]? |
|
access |
"private" | "specific_people" | "whole_org" | "public_link" | "password"? |
re-checks the same operator gates as publish |
password |
string | null? |
set, or null to clear |
expiresAt |
number | null? |
set, or null to clear |
metadata |
Record<string, unknown>? |
replaces the stored blob |
publish and update each send one multipart/form-data request
(credentials: "include"): a JSON metadata part plus (optionally, for update) the
zip bundle part.
list filter
list() returns every share the viewer authored — including revoked and expired
ones, so a management UI can show them. Pass a filter to narrow server-side:
await canvasdrop.canvases.list({ sourceApp: "product-roadmap" });
await canvasdrop.canvases.list({ sourceKind: "roadmap-share", tags: ["q3"] });
{ sourceApp?, sourceKind?, tags? } — sourceApp/sourceKind match the values inside
metadata; tags matches shares carrying every listed tag.
Update, revoke, and status
- Update is in place.
updatedeploys a new immutable version to the same canvas — the public URL never changes, and version history + one-click rollback are preserved. Use it instead ofpublishwhen re-sharing the same artifact. - Revoke keeps the record.
revokemakes the public URL unavailable (readers get not-found) and stampsrevokedAt, but the share stays inlist()asstatus: "revoked". A revoked share is terminal — to share again,publisha fresh one. Callingupdateon a revoked share rejects withSHARE_REVOKED. - Status is derived from access + expiry + revoked state:
revoked›expired›private›live. Only aliveshare is reachable by a reader. - Reader isolation. A public/permitted reader receives only the shared static
content — never
metadata,createdBy,status, or any management field. Those are assembled only on the authenticated management API.
Ownership, quotas, and authorization
- The new share is owned by the viewer who called
publish. It counts against their normal canvas ownership and a per-viewer authoring quota (a daily and an all-time cap the operator sets). - Quota is consumed the moment the canvas is created by
publish— aPublishFailedErrorstill counts, since the canvas exists.updatedoes not consume quota (it edits an existing share).revokedoes not refund quota. update,list, andrevokeare scoped to the viewer's own shares (or an admin's); a share id you don't own reads as not-found.expiresAtuses the same share-expiry mechanism as the dashboard, so it only means something on a shareable rung (public_link/password/whole_org/specific_people).
Errors
Every method rejects with a CanvasdropError subclass — catch the one you care about,
or read err.code / err.status.
- Not a signed-in member (a guest / public visitor) →
NotAuthenticatedError(status: 401,code: "NOT_AUTHENTICATED"). - Authoring off (backend off, the per-canvas toggle off, or the operator switch off) →
CapabilityDisabledError(status: 403,code: "CAPABILITY_DISABLED"). - Per-viewer daily or all-time cap hit →
QuotaExceededError(status: 429,code: "QUOTA_EXCEEDED", with ascopeofuser_dailyoruser_total). - Invalid request (bundle or metadata too large, a disallowed access rung, a
missing/over-max expiry) →
CanvasdropError(status: 400/413,code: "INVALID_BODY"). updateon a share that's already revoked →CanvasdropError(status: 409,code: "SHARE_REVOKED") — publish a new share instead.- The canvas was created but its deploy or share-config failed →
PublishFailedError(status: 502,code: "PUBLISH_FAILED"). Its.idis the new canvas's id, so you can retry the publish orcanvasdrop.canvases.revoke(id).
See error codes. The underlying HTTP endpoints live under
/v1/c/<slug>/authoring — see the Runtime API.