Identity
Know who is looking at your canvas. This page is the reference for
canvasdrop.me(), the identity primitive on the canvasdrop global that
<script src="/sdk/v1.js"> defines in every canvas. me() returns the
signed-in viewer, resolved from the server-side session: the page never handles
a token, and a viewer cannot claim to be someone else. By the end you can greet
the viewer by name, key shared data per person, and handle every error me()
returns.
The canvas needs Enable backend on in its Backend tab. Identity has no toggle of its own: it is available whenever the backend is on (see Capabilities).
const me = await canvasdrop.me();
// {
// id: "0190a3f2-7c4e-7a1b-9d2f-3c5e6a7b8c9d",
// email: "someone@example.com",
// name: "Alex Rivera",
// avatarUrl: null,
// kind: "member",
// canvasRole: "viewer",
// permissions: { canSubmit: true, canWriteSharedData: false, /* … */ }
// }
document.querySelector("#greeting").textContent = `Hi, ${me.name}`;
Call it once per page load and keep the result. The runtime API is rate-limited
per viewer per canvas (120 requests a minute by default, one budget shared by
me(), kv, and files; ai has its own), so a me() on every render spends
budget for nothing.
Signature
me(): Promise<Me>
interface Me {
id: string;
email: string;
name: string;
avatarUrl: string | null;
kind: "member" | "guest";
canvasRole: "owner" | "editor" | "viewer";
permissions: RuntimePermissions;
}
| Field | Type | Notes |
|---|---|---|
id |
string |
Stable per-user id. Key per-user data on this, never on email. |
email |
string |
The viewer's email. |
name |
string |
Display name. |
avatarUrl |
string | null |
Avatar URL, or null when the identity provider gives none. |
kind |
"member" | "guest" |
"member" for a signed-in org user. "guest" is retained only for legacy guest sessions; see below. There is no "anonymous": a signed-out visitor never reaches the runtime API. |
The projection is deliberately minimal. It carries no admin flag and no org membership, so canvas code cannot tell an admin from any other member. That information stays on the dashboard side.
Where the identity comes from
me() calls GET {base}/v1/c/{slug}/me with the viewer's session cookie
(credentials: "include"). The SDK works out the slug and the API origin from
window.location; there is nothing to configure.
| URL mode | Canvas page | me() request |
|---|---|---|
path |
{base}/c/{slug}/ |
{base}/v1/c/{slug}/me (same origin) |
subdomain |
https://{slug}.canvases.example.com/ |
https://canvases.example.com/v1/c/{slug}/me (the base host, with credentialed CORS) |
The server resolves the user from the session and returns the projection above. Nothing in the page identifies the viewer, so nothing in the page can be edited to impersonate someone.
The same server-side id is what scopes the per-viewer KV
namespace (canvasdrop.kv.user) and what appears as from.id
on realtime messages and in presence lists. You never pass
it; the server already knows it.
Legacy guest sessions
People you add under People and teams with direct access sign in through the org's normal
login and arrive as "member", so on a current instance that is the only
kind you will see. "guest" remains for guest sessions retained from older
instances. A guest's id is namespaced guest:<inviteId> so it never collides
with an org user id; its name is its email and its avatarUrl is null.
Guests hold no org capabilities, and AI for guests has its own per-canvas switch
(see AI).
Errors
me() rejects with a CanvasdropError (or a subclass); branch on err.code.
The codes you will actually meet:
| Code | Status | When |
|---|---|---|
CAPABILITY_DISABLED |
403 | The canvas's backend is off. err.hint names the switch: the dashboard Backend tab, the set_capabilities MCP tool, or PATCH /api/canvases/:id/capabilities. Thrown as CapabilityDisabledError. |
STATIC_ONLY |
403 | The canvas is at the Public link rung. Every backend primitive, me() included, is refused for anyone who is not the owner or an editor, signed in or not. |
RATE_LIMITED |
429 | Too many runtime-API calls from this viewer on this canvas within the last minute. Back off and retry. A plain CanvasdropError, not a QuotaExceededError. |
NOT_AUTHENTICATED |
401 | No session. Rare in practice: a viewer who reached the canvas has already signed in. In proxy and dev auth modes the gateway answers 401; in oidc mode it redirects to login instead. Thrown as NotAuthenticatedError. |
Access is re-checked on every call, so me() can also fail later in a session
when the viewer's access changed after the page loaded: PASSWORD_REQUIRED
(403) when the password gate must be passed again, DISABLED (403) when an
admin disabled the canvas, and a NotFoundError (404) when the share expired or
was revoked or the canvas was archived. A page reload sends the viewer back
through the normal entry flow.
try {
const me = await canvasdrop.me();
greet(me.name);
} catch (err) {
if (err.code === "CAPABILITY_DISABLED") {
// backend is off for this canvas; err.hint says what to switch on
} else if (err.code === "STATIC_ONLY") {
// public-link canvas: static for everyone but the owner and editors
} else {
throw err;
}
}
The error classes are also exported by @canvas-drop/sdk for instanceof
checks; the error codes reference lists every code.
Per-user data without plumbing
Most canvases do not need me().id at all. canvasdrop.kv.user scopes every
key to the current viewer on the server, so per-person state is
await canvasdrop.kv.user.set("draft", text) with no id in sight. Reach for
me() when you want to show the viewer's name or avatar, or when the shared
page needs to show controls for the current role. Use
submissions for votes or forms that editors should review.
Private kv.user preferences remain unreadable by other users, including editors.
Canvas role and permissions
me().resources additionally exposes configured collections and file groups as
operation rights {read, create, update, delete, increment}, each {own, any}.
Channels expose {subscribe, publish, seePresence, participatePresence} booleans;
granted Connections expose {invoke, methods} after audience/admin restrictions.
permissions.canCreateCanvas describes enabled page-driven authoring for members,
separate from editing existing canvases. For per-record controls, compare me().id
with the server-derived record.authorId. See Permissions and defaults.
canvasRole is the effective role on this canvas. Ownership wins, followed by a
live direct or team editor grant; everyone else admitted by the access rules is
viewer. A platform admin does not gain a canvas role through their admin flag.
The role is independent of whether backend features are enabled.
permissions contains these boolean fields, after applicable feature switches,
instance availability, lifecycle, and audience settings are applied:
| Fields | Meaning |
|---|---|
canEditContent, canManageVersions |
Owner/editor management of content and version history. |
canReadSharedData, canWriteSharedData |
Shared KV reads for admitted viewers; writes only for owners/editors. |
canSavePreferences |
Caller-only kv.user data. |
canSubmit, canManageSubmissions |
Own submissions for viewers; review and management for owners/editors. |
canUploadSharedFiles, canUploadSubmissionFiles |
Shared uploads for owners/editors; private submission uploads for viewers. |
canUseAi, canUseConnections |
Current audience allows this caller and the feature is available. Connections also needs an available admin grant. |
canPublishSharedEvents, canPublishParticipantEvents |
Shared-channel publishing for owners/editors; participants: publishing for admitted viewers. |
These are a snapshot for rendering controls, not a credential. Refresh after a
permission error or a known access change; every server operation rechecks access.
A permission being true does not promise success: quotas, model/profile-specific
rules, validation and legacy guest AI restrictions still apply. me() is returned
with Cache-Control: private, no-store.
const { canvasRole, permissions } = await canvasdrop.me();
editButton.hidden = !permissions.canWriteSharedData;
submitButton.disabled = !permissions.canSubmit;
roleLabel.textContent = canvasRole;