Search

Search the documentation

Protobox
Use cases

Connect your users' apps

Add a "Connect Gmail" button to your product — hosted OAuth per end-user, managed token refresh, multi-account, and clean offboarding.

Scenario: Acme (a support SaaS) wants each support team to connect their own Gmail and HubSpot, so Acme's assistant can read context and send replies as that team. Every connection must be isolated per user, tokens must refresh themselves, and closing an account must revoke access.

How it fits together

"Connect Gmail" clicked entity('user_123').connect('gmail') authorizeUrl redirect user to authorizeUrl user consents OAuth callback (hosted by Protobox) connection active credential stored in vault Your UI Your backend Protobox Gmail
You never touch the provider OAuth dance or store a token — redirect to a URL, wait for active.

Connect one end-user

import { ProtoboxSDK } from '@protoboxai/sdk';

const protobox = new ProtoboxSDK({ apiKey: process.env.PROTOBOX_API_KEY! });

// `user_123` is YOUR id for this user — any stable string.
const user = protobox.entity('user_123');

// 1. Start the hosted OAuth flow
const { authorizeUrl } = await user.connect('gmail');

// 2. Send the user's browser there (from your UI)
//    Protobox hosts the callback and stores the credential in its vault.

// 3. Wait until the connection is usable
await user.waitUntilActive('gmail');

GitHub App integrations install rather than authorize: await user.install('github') returns an installUrl. Integrations that use plain API keys (no OAuth) connect at the workspace level with protobox.connections.connectApiKey({ integrationId, apiKey }).

Don't poll in production — use the webhook

waitUntilActive polls, which is fine in a script. In your product, register a platform webhook and flip your UI when connection.activated arrives for that user:

// webhook handler (see the webhooks recipe for verification)
if (event.type === 'connection.activated') {
  const { integrationId, callerId } = event.data; // callerId = your userId
  await markConnected(callerId, integrationId);   // your DB, your UI state
}

Show connection state in your UI

const connections = await user.listConnections();
// [{ integrationId: 'gmail', status: 'active', ... }, ...]

Render your integrations page from this — Protobox is the source of truth for what's connected, so there's no state to sync.

Browser-side connect: mint a connect token

Everything above runs on your backend, where your workspace API key lives. The moment the browser talks to Protobox directly — a connect popup, an embedded catalog — identity must live in the credential, not in a parameter. A page that says "connect me as user_123" could say any user id; a connect token can't.

Mint one server-side for your signed-in user (this is the only place your product names a user to Protobox):

// Your backend — the workspace API key never reaches the page.
const { token, expiresAt } = await protobox.connections.createConnectToken({
  userId: 'user_123',      // YOUR id for the signed-in user
  integrationId: 'gmail',  // optional: lock the token to one integration
  scopes: ['https://www.googleapis.com/auth/gmail.readonly'], // optional cap
  ttlSeconds: 300,         // clamped to [60, 900]
});

Hand token to the page and initialize the connect client — from here the browser calls Protobox's token-authenticated surface directly, and never names a user:

// Your page. Reference implementation ships in the SDK repo today
// (examples/saas-connect-demo/lib/connect-client.ts — the future @protoboxai/connect).
init({ connectToken: token });
const connection = await openConnect('gmail'); // popup → consent → resolves active

The engine takes the user from the token on every call: a tampered or mismatched user id is rejected, expired/revoked tokens stop resolving within their ≤15-minute TTL, and browser calls are additionally gated by your workspace's registered origins. Registering an origin and revoking tokens are workspace-key operations — the browser can do neither.

Multiple accounts per user

A user with a work and a personal Gmail connects each under a named account key:

const work = user.account('work');
const { authorizeUrl } = await work.connect('gmail');

Tool calls made through an account scope use that account's credential.

Offboarding

When a user disconnects an app — or deletes their Acme account — revoke server-side:

// appCredentialsId comes from the rows in user.listConnections()
await user.disconnect(appCredentialsId);

Protobox revokes and deletes the stored credential. Nothing lingers in your systems, because nothing was ever in your systems.

Handling "not connected" at call time

If an agent calls a tool for a user whose connection is missing or expired, execution fails with an auth-shaped error rather than a generic 500 — surface reconnect, not retry:

import { isConnectionNotReadyError } from '@protoboxai/sdk';

try {
  await user.execute({ name: 'GMAIL_GMAIL_USERS_MESSAGES_SEND', arguments: args });
} catch (err) {
  if (isConnectionNotReadyError(err)) {
    // show your "Reconnect Gmail" button → user.connect('gmail') again
  }
}

Production notes

  • One entity per human (or per tenant seat). Entities are your isolation boundary; don't share one userId across people, or their credentials blur together.
  • Catalog discovery: protobox.integrations.list() and protobox.integrations.actions('gmail') give you everything needed to render your own "app store" page, including tool names and schemas.
  • Branding the consent screen: by default the OAuth consent names the managed Protobox app. To make it name your product, bring your own OAuth client — see White-label.

On this page