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 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 activeThe 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
userIdacross people, or their credentials blur together. - Catalog discovery:
protobox.integrations.list()andprotobox.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.
Related
- Managed auth & connections — the full connection lifecycle reference
- Agent tool-calling — using what you just connected
Ship integrations inside your SaaS
Connect your users' apps, hand tools to your agents, and ship integration features — without building OAuth, vaults, or provider SDKs yourself.
Agent tool-calling
Hand a connected user's tools to OpenAI, Anthropic, the Vercel AI SDK, LangChain, or Google GenAI — and run the model's tool-calls server-side with that user's credentials.