Embedded sessions

Let your signed-in users connect and manage their own apps without Arc0 accounts.

An embedded session delegates a short-lived set of connection-management permissions to a user already authenticated by your application. Your backend holds the business API key. The browser holds a challenge-bound ticket, then an opaque session and a separate CSRF value in memory. End users do not need an Arc0 dashboard login.

The browser packages are prepared for release and have not been published. Use the workspace packages or locally packed artifacts until release. For agent-led setup, use the coding-agent guide.

Backend setup

Use an unbound project key with embedded:manage and connections:manage. Provision the user with upsertUser and retain the returned project-local ID. Register exact origins separately for Development and Production:

import { Arc0 } from '@arc0/sdk';
const arc0 = new Arc0({
  baseUrl: 'https://api.arc0.ai',
  apiKey: process.env.ARC0_API_KEY!, // Server only.
});
await arc0.registerEmbeddedManagementOrigins(['https://app.example.com']);

Register origins during trusted backend setup, never on each browser request. HTTPS is required outside HTTP loopback Development origins. Wildcards and suffix matches are refused. Embedded popups use these registrations for their result destination; hosted Connect sessions with a returnUrl use the separate Connect return-origin list.

Your ticket endpoint

The browser package calls getTicket({browserChallenge, signal}). Your authenticated, CSRF-protected backend route must resolve its own user's Arc0 ID and return the mint result. It must never trust a browser-supplied end user, project, origin or scope list. For example, inside your existing authenticated handler:

const user = await requireAuthenticatedUser(request);
await requireSameOriginAndCsrf(request);
const { browserChallenge } = await validatedChallengeBody(request);
const ticket = await arc0.mintEmbeddedManagementTicket({
  endUserId: user.arc0EndUserId,
  browserChallenge,
  origins: ['https://app.example.com'],
  scopes: ['catalog:read', 'connections:read', 'connections:manage'],
});
return Response.json(ticket, { headers: { 'Cache-Control': 'no-store' } });

The authentication, CSRF and validation helpers above are your application's existing middleware. Validate the challenge as a 43-character base64url S256 digest. A read-only manager needs connections:read; the catalog and configured branding need catalog:read. A connection-only manager retains default branding with visible Arc0 attribution when that branding scope is absent. connections:manage permits Connect, reconnect and revocation. Manage does not implicitly include read. Custom-app scopes are separate and require server-side exposure rules; these components display the registry catalog and personal connections.

Lifetime and revocation

Tickets default to 120 seconds, are single-use and bind to a fresh browser verifier. Sessions default to 600 seconds. There is no refresh token: an expired client requests a fresh ticket through your authenticated backend. Failed writes are never replayed automatically. Reauthenticate or retry explicitly when access expires or is denied.

Call client.dispose() on sign-out or identity change, and revoke delegated sessions on your backend where immediate server-side termination is needed:

await arc0.revokeEmbeddedManagementSessions({ target: 'subject', endUserId });

Removing a registered origin, disabling the subject or revoking the issuer key stops subsequent session requests. A Connect flow already admitted has its own bounded lifetime; revoking the parent session does not cancel that previously issued flow.

Browser boundary

Tickets, verifier, embedded token and CSRF value never belong in URLs, logs, analytics, localStorage, sessionStorage or cookies. The client exchanges JSON with credentials: 'omit', then sends Authorization: Embedded … and X-Arc0-CSRF on all reads and writes. The server also checks the browser's exact Origin.

OAuth runs in a top-level popup. Its separate hosted Connect capability uses an existing URL fragment; it is not the embedded session credential. Popup results are accepted only from the configured API origin and the exact opened window, for the matching flow. Treat results as UI signals; use signed webhooks for backend lifecycle decisions and refresh connections for current state.

Continue with web components or React and headless hooks.

On this page