Outbound Connections
Use canvasdrop.connections.fetch when a canvas needs data from a third-party
HTTP API that cannot be called directly from the browser—for example, an API
that does not allow browser CORS or requires a fixed User-Agent or API key.
Connections is a controlled request forwarder, not a Lambda runtime. Canvas Drop never runs canvas-supplied server code. An instance administrator creates a reusable profile for one exact HTTPS origin, chooses the allowed standard methods, stores any protected headers, and grants the profile to individual canvases. The protected values remain encrypted on the server.
Fetch through a profile
Suppose an administrator created the profile key stocks, approved
https://api.example.com, allowed GET, fixed its User-Agent, and granted it
to your canvas:
const symbol = "ACME";
const response = await canvasdrop.connections.fetch(
"stocks",
`/v2/quote?symbol=${encodeURIComponent(symbol)}`,
{ headers: { accept: "application/json" } },
);
if (!response.ok) {
// This is the upstream's status and bounded response body, not a platform error.
throw new Error(`Stock provider returned ${response.status}`);
}
const quote = await response.json();
The signature is:
connections.fetch(
profile: string,
path: string,
init?: {
method?: "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
headers?: RequestInit["headers"];
body?: RequestInit["body"];
signal?: RequestInit["signal"];
},
): Promise<Response>
path must start with one /. Absolute URLs and protocol-relative paths are
rejected in the SDK before a request is sent. The path and query are appended to
the profile's exact origin. Canvas code cannot change its scheme, hostname, or
port.
Only a method the administrator selected for that profile is accepted. Request
bodies work for POST, PUT, PATCH, and DELETE; GET and HEAD do not send
one. headers accepts the normal browser HeadersInit shapes. Canvas cookies,
authorization, forwarding, hop-by-hop, compression, and host headers are never
passed through. Supplying a caller header with the same name as a protected
profile header is rejected; the protected value is never replaced or exposed.
Responses and errors
The promise resolves to a native Response for every response the approved
upstream returned, including 4xx and 5xx. Check response.ok or
response.status just as you would with fetch.
Canvas Drop policy and transport failures throw CanvasdropError instead. The
stable codes include:
| Code | Status | Meaning |
|---|---|---|
CONNECTION_NOT_GRANTED |
404 | The profile is not attached to this canvas. |
CONNECTION_DISABLED |
503 | An administrator disabled the profile. |
CONNECTION_KEY_UNAVAILABLE |
503 | The external encryption key needed for protected headers is unavailable. |
METHOD_NOT_ALLOWED |
405 | The method is unsupported or not approved for this profile. |
DESTINATION_BLOCKED |
403 | Origin, DNS answer, redirect, or content encoding crossed the connection boundary. |
REQUEST_TOO_LARGE |
413 | URL, headers, or body exceeded a request limit. |
REQUEST_TIMEOUT |
408 | A public request body did not arrive within the connection deadline. |
RESPONSE_TOO_LARGE |
502 | The upstream response exceeded the buffer limit. |
CONNECTION_RATE_LIMIT |
429 | A connection-specific request bucket is spent. |
CONNECTION_LIMIT |
429 | A per-canvas or process-wide in-flight limit is full. |
CONNECTION_DAILY_LIMIT |
429 | The public grant's shared daily request cap is spent. Resets at midnight UTC; do not retry immediately. |
UPSTREAM_TIMEOUT |
504 | The total DNS, request, redirect, and response deadline expired. |
UPSTREAM_UNAVAILABLE |
502 | DNS or the approved upstream failed. |
The normal canvas gates still run first: canvas access, password, lifecycle, the public grant when required, the broad runtime rate limit, and the Backend master switch. Detaching a grant, disabling its profile or Backend, revoking access, adding a password, expiring/archiving/disabling/deleting the canvas blocks the next request.
Fixed safety bounds
The first release is deliberately small and buffered:
| Boundary | Default |
|---|---|
| Relative URL | 8 KiB |
| Caller headers | 32 headers, 16 KiB total |
| Request body | 256 KiB |
| Response body | 2 MiB |
| Total deadline | 10 seconds |
| Redirects | 3, exact-origin only; DNS is revalidated at every hop |
| Rate | 60/min per actor + canvas + profile; 600/min per profile |
| In flight | 5 per canvas; 50 in the server process |
The operator can set these read-only deployment values through the
CANVAS_DROP_CONNECTIONS_* environment variables. Multi-process coordination
is not part of this first release.
Security boundary
Canvas Drop accepts only HTTPS DNS origins—no IP literals, credentials, path,
query, or fragment in the profile. Every DNS answer must be public. Mixed
public/private answers are rejected, and the chosen public address is pinned to
the TLS socket while the approved hostname remains the certificate, SNI, and
Host name. Redirects must retain the exact origin and are re-resolved before
the next socket is opened. Responses are identity-encoded, bounded, no-store,
and expose only safe headers; cookies, redirects, CORS, server, and hop-by-hop
headers are removed.
Protected header names are visible only to administrators as “configured”; the
values are never returned by an API or loaded into the dashboard. Owners,
editors, and list_canvas_connections over MCP see only the profile key, label,
origin, allowed methods, availability, and any public policy.
One residual trust boundary remains: the approved upstream receives the protected value and can deliberately reflect it in its own response body. Use a least-privilege credential scoped to that upstream and profile. Operators should also restrict the Canvas Drop server's network egress as defense in depth.
Public connections (opt-in)
Public visitors cannot use a connection by default. An administrator can enable
Public access on one canvas grant in Admin → Connections → Manage. This
requires exact endpoint paths, a subset of the profile's methods, and a daily
request cap. This is an exception for that named connection only: anonymous
visitors still cannot use me(), KV, files, AI, realtime, or authoring.
The canvas must have an active public link, Backend on, and the named Connection audience set to viewers (or inherit the viewers audience). Passwords, expiry, the instance public-link switch, owner publishing permissions, and profile revocation still apply. Turning public access off blocks the next request.
For public calls, query strings, encoded paths and redirects are refused. Only the exact configured paths are forwarded. The existing body, response, egress, rate and concurrency limits also apply. Public rate buckets use the server-resolved client IP; logs retain a per-day, per-canvas keyed hash rather than the address. The shared daily counter is database-atomic, survives restarts and policy edits, and counts admitted attempts, including unfinished uploads and upstream failures. Public body reads have the same configured deadline as upstream calls. It is a request cap, not a monetary budget. Public callers can submit any body the approved endpoint accepts, so approve only endpoints and credentials suitable for public use.
The administrator API is
PUT /api/admin/connections/:id/canvases/:canvasId/public with
{"policy":{"paths":["/v1/analyze"],"methods":["POST"],"requestsPerDay":2000}}.
Use {"policy":null} to disable it. Owners can read the policy through the
existing manager API and list_canvas_connections MCP tool, but cannot enable it.
Check availability without requesting the visitor's identity:
const access = await canvasdrop.connections.status("analyzer");
// { invoke: true, methods: ["POST"], publicAccess: true }
if (access.invoke && access.methods.includes("POST")) {
// Show the analysis action. Calls can still fail if limits or policy change.
}
status uses GET /v1/c/:slug/connection-status/:profile, returns only those three
fields, and is never cached. publicAccess means this caller uses the public
grant; signed-in owners and invited participants retain their existing rights.
When adding status to an existing canvas, version the SDK script URL (for
example /sdk/v1.js?release=public-connections-1) so returning visitors get it.
Audience permission
A named Connection policy can override the canvas audience and restrict HTTP
methods. In runtimePolicy.connections, set {audience: "viewers", methods: ["GET"]}
for the intended profile key. Administrator grants and approved methods remain an
upper bound. none denies invocation, including owners/editors; omit methods to
use the administrator's allowed set, or use [] to deny every method. Read effective
rights from connections.status(key), or me().resources.connections[key] for signed-in users. External item ownership must be
enforced by the upstream service. See Permissions and defaults.
This feature defaults to owners and editors (connectionsAudience: "editors"). To allow
signed-in viewers, an owner or editor selects All signed-in viewers under
Connection access in the Backend tab, or sets connectionsAudience: "viewers" with
set_capabilities / PATCH /api/canvases/{id}/capabilities.
A disallowed caller receives 403 PERMISSION_DENIED (PermissionDeniedError)
before a provider or upstream request starts. The setting leaves existing
capability, public/static-only, quota and operator policy gates in place.
Check me().permissions when rendering the feature's controls.