A practical field guide from Automation Ace.
Short answer: to use managed app auth in a Cloudflare Worker, store your platform's secret key (for example Nango or Pipedream Connect) as a Worker secret, keep a D1 or KV table that maps your users to their connection IDs, create short-lived connect sessions from a Worker route so users can connect their apps, save the connection ID from the platform's auth webhook, and call third-party APIs with plain fetch through the platform's authenticated proxy. The Worker never stores or refreshes OAuth tokens, which suits a stateless edge runtime.
Platform endpoints and Worker features change; check Cloudflare Workers docs and your platform's docs before shipping.
Short answer
wrangler secret putthe platform's secret key.- Create a D1 table: user ID, integration, connection ID, status.
- Add a route that creates a connect session for the signed-in user.
- Add a webhook route that verifies and saves new connection IDs.
- Proxy API calls with
fetchplus the connection ID. - Flag connections that fail auth and offer reconnect.
Why managed auth suits Workers
Workers are short-lived and globally distributed. Building OAuth yourself would mean storing refresh tokens, preventing two locations from refreshing the same token at once, and encrypting secrets at rest. With managed auth, the Worker only holds one secret key and a list of connection IDs, and the platform does the stateful part. For Worker basics, see receiving webhooks with Cloudflare Workers and serverless functions compared.
Worker-specific considerations
| Worker consideration | What to do |
|---|---|
| Runtime | Workers run fetch-based JavaScript, not full Node.js. Plain fetch to the platform's REST API always works; some Node SDKs need the nodejs_compat flag or may not run at all |
| Secrets | Use Worker secrets (wrangler secret put) and read them from env; never ship them to the browser |
| State | Store user-to-connection mappings in D1 or KV; store no tokens |
| Auth for your own users | Verify your session cookie, JWT, or Cloudflare Access identity before using a connection |
| Scheduled work | Cron Triggers can run syncs or checks for connections that need attention |
| Limits | Keep requests short; stream proxy responses back instead of buffering large bodies |
Step 1: Store the secret key
# Store the managed auth platform's secret key as a Worker secret
npx wrangler secret put NANGO_SECRET_KEY
# For local development, put it in .dev.vars (and keep that file out of git)
NANGO_SECRET_KEY=...
Step 2: Map users to connection IDs
-- D1: map your users to their connection IDs (no tokens stored here)
CREATE TABLE app_connections (
user_id TEXT NOT NULL,
integration TEXT NOT NULL, -- e.g. "hubspot", "slack"
connection_id TEXT NOT NULL, -- from the platform's auth webhook
status TEXT DEFAULT 'ok', -- ok | needs_reconnect
updated_at TEXT,
PRIMARY KEY (user_id, integration)
);
Bind the database to the Worker as DB in your Wrangler config. The table holds IDs only; the tokens stay with the platform.
Step 3: Create connect sessions and call the proxy
// src/index.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
const user = await getUser(request, env); // YOUR auth: session, JWT, Access
if (!user) return new Response("Unauthorized", { status: 401 });
// 1) Start a connection: return a short-lived connect session token
if (url.pathname === "/connect/hubspot" && request.method === "POST") {
const r = await fetch("https://api.nango.dev/connect/sessions", {
method: "POST",
headers: { Authorization: `Bearer ${env.NANGO_SECRET_KEY}`,
"Content-Type": "application/json" },
body: JSON.stringify({
tags: { end_user_id: user.id, organization_id: user.orgId },
allowed_integrations: ["hubspot"],
}),
});
const { data } = await r.json();
return Response.json({ sessionToken: data.token });
}
// 2) Use the connection: call HubSpot through the proxy
if (url.pathname === "/api/contacts") {
const connectionId = await env.DB
.prepare("SELECT connection_id FROM app_connections WHERE user_id = ? AND integration = 'hubspot'")
.bind(user.id).first("connection_id");
if (!connectionId) return Response.json({ error: "not_connected" }, { status: 409 });
const res = await fetch("https://api.nango.dev/proxy/crm/v3/objects/contacts?limit=10", {
headers: { Authorization: `Bearer ${env.NANGO_SECRET_KEY}`,
"Provider-Config-Key": "hubspot",
"Connection-Id": connectionId },
});
return new Response(res.body, { status: res.status,
headers: { "content-type": res.headers.get("content-type") ?? "application/json" } });
}
return new Response("Not found", { status: 404 });
},
};
Your frontend calls /connect/hubspot, then opens the platform's connect UI with the returned session token. Afterwards, /api/contacts returns HubSpot data without the Worker ever seeing a HubSpot token.
Step 4: Save connection IDs from the webhook
// 3) Receive the platform's auth webhook and save the connection ID
if (url.pathname === "/webhooks/nango" && request.method === "POST") {
const raw = await request.text();
if (!(await verifyNangoSignature(raw, request.headers, env))) { // per Nango's docs
return new Response("Invalid signature", { status: 401 });
}
const evt = JSON.parse(raw);
if (evt.type === "auth" && evt.success) {
await env.DB.prepare(
"INSERT INTO app_connections (user_id, integration, connection_id, status, updated_at) " +
"VALUES (?, ?, ?, 'ok', datetime('now')) " +
"ON CONFLICT(user_id, integration) DO UPDATE SET connection_id = excluded.connection_id, " +
"status = 'ok', updated_at = excluded.updated_at"
).bind(evt.tags.end_user_id, evt.providerConfigKey ?? "hubspot", evt.connectionId).run();
}
return new Response("ok");
}
Verify the webhook signature exactly as your platform documents before trusting it, and read the body as text first so the signature check sees the original bytes. More on webhook handling: what a webhook is.
Step 5: Handle reconnects and errors
- Auth failures: when the proxy reports invalid credentials, set
status = 'needs_reconnect'and show a Reconnect button that opens a reconnect session. - Rate limits: pass 429s back to the client with
Retry-After, or retry with backoff in the Worker; see 429 errors. - Background checks: a Cron Trigger can check connections daily; see Cloudflare Cron Triggers.
Other platforms from a Worker
| Platform | From a Worker |
|---|---|
| Nango | REST via fetch: connect sessions, proxy with Provider-Config-Key and Connection-Id headers |
| Pipedream Connect | REST via fetch: get an API access token with your OAuth client credentials, then call the Connect API proxy with the external user ID and account ID |
| Composio | REST API or an SDK that supports the Workers runtime; tools and connected accounts are scoped by your user ID |
| Arcade | REST API or SDK to run tools for a user ID and return authorization links when needed |
| Zapier MCP | Call it as a remote MCP server for your own account's actions, not for end-user connections |
Compare them in the best managed app auth platforms. The same pattern works in any JavaScript backend; see calling third-party APIs from JavaScript without managing OAuth tokens.
Security checklist
- Authenticate your own user before looking up their connection; never accept a connection ID from the browser.
- Restrict which API paths the Worker will proxy, rather than forwarding any path the client sends.
- Use scoped platform API keys where offered (for example, a key that can only create connect sessions for the connect route).
- Verify webhook signatures; reject unsigned events.
- Log request IDs and status codes, not tokens or full response bodies.
Building an agent on Workers? See how an agent can use a user's apps without seeing their tokens and how to limit which app actions an agent can take.
Frequently asked questions
Can I use Nango or Pipedream Connect in a Cloudflare Worker?
Yes. Call their REST APIs with fetch: create connect sessions, receive auth webhooks, and send API requests through the authenticated proxy with your secret key and the user’s connection ID. Store the secret key as a Worker secret.
Where should a Cloudflare Worker store OAuth tokens?
Ideally nowhere. With managed app auth, the platform stores and refreshes tokens, and the Worker stores only a mapping from your users to connection IDs in D1 or KV.
Do Node.js SDKs work in Cloudflare Workers?
Some do with the nodejs_compat flag, and some don’t. Plain fetch calls to the platform’s REST API always work and are often the simplest choice in a Worker.
How do I keep a Worker proxy from becoming an open relay?
Authenticate your own users first, look up their connection ID server-side, and allow only the specific API paths and methods your app needs instead of forwarding arbitrary requests.
How does a Worker know when a user connects an app?
The managed auth platform sends an auth webhook with the connection ID and the tags you set, such as your user ID. A Worker route verifies the signature and saves the connection ID.
Disclaimer: Managed app auth platforms, their SDKs, endpoints, plans, and features change quickly; details reflect public documentation and coverage at the time of writing, so confirm current details with each provider before building. Automation Ace is not affiliated with Composio, Nango, Pipedream, Arcade, or the other companies named here unless a link is marked as an affiliate link. This article may include links to apps, products, or services; some links may be affiliate links, which means Automation Ace may earn a commission at no extra cost to you.