guides

ByteChef Embedded, Part 1: Let Your Users Connect Their Apps

ByteChef Embedded, Part 1: Let Your Users Connect Their Apps
6 min read
Ivica Čardić

TL;DR: Before a customer can automate anything in your product, they have to connect their own accounts. ByteChef Embedded makes that a two-piece job: (1) your backend signs a JWT identifying one of your users as a ByteChef connected user, and (2) your frontend calls one hookuseConnectDialog from @bytechef/embedded — which mounts a hosted ConnectDialog that runs the whole OAuth/API-key handshake, stores and refreshes the credentials, and doubles as the manage surface once a connection exists. This is part one of a hands-on series that walks the embedded sample app feature by feature. We start where every embedded integration starts: connecting an app.

If you're embedding automation inside your own SaaS product, the very first wall you hit isn't workflows — it's connections. Before your customer can "sync new leads to their CRM," they have to connect their CRM, with their OAuth grant, stored securely, refreshed forever. Do that yourself and you've signed up to build and maintain OAuth for Salesforce, and HubSpot, and Slack, and the next hundred apps your customers ask for. That's not a feature; it's a department.

ByteChef Embedded exists to delete that work, and this series shows exactly how — using the real embedded sample app (a Next.js frontend + a small Node backend) as our guide. Over the coming parts we'll build out each way your users can create automations. But all of it rests on this first step, so that's where we begin: letting your users connect their apps.

The Auth Model: Connected Users, Not ByteChef Accounts

Here's the crucial idea that makes embedded work: your end users never get a ByteChef account. They're your users. ByteChef knows them only as connected users, identified by a token your backend signs.

The sample app's backend does exactly this — one endpoint that mints a JWT:

const payload = {
  sub: externalUserId,          // your app's user id
  name: name || externalUserId,
  iat: Math.floor(Date.now() / 1000),
};

const token = jwt.sign(payload, BYTECHEF_PRIVATE_KEY, {
  algorithm: 'RS256',
  expiresIn: TOKEN_EXPIRY,
  keyid: BYTECHEF_KID,
});

You hold the private key; ByteChef verifies with the matching public key. The sub claim is your user id (externalUserId) — so a token says, in effect, "this is my user #1234, acting as themselves." Your frontend fetches that token from your backend and never touches the key:

const token = await getToken();   // POSTs to your /api/token

That's the entire identity story. Every embedded call the frontend makes carries this JWT (plus an X-ENVIRONMENT header), and ByteChef scopes everything — connections, workflows, executions — to that connected user. (It's the same connected-user model behind the Embedded MCP server.)

Listing the Integrations You Offer

Your users can only connect what you've published. The sample app fetches your catalog straight from the embedded API with the bearer token:

const res = await fetch(`${BYTECHEF_APP_BASE_URL}/api/embedded/v1/integrations`, {
  headers: { Authorization: `Bearer ${token}`, 'X-ENVIRONMENT': BYTECHEF_ENVIRONMENT },
});

Back comes the list of integrations you've defined — title, icon, category, and whether this user already has an instance connected. Render them as a grid of cards, and you've got an "Integrations" page with zero bespoke per-app code.

The Star of Part One: useConnectDialog

Now the actual connecting. It's one hook:

import { useConnectDialog } from '@bytechef/embedded';

const { openDialog, closeDialog } = useConnectDialog({
  baseUrl,
  environment,        // DEVELOPMENT | STAGING | PRODUCTION
  integrationId,      // which integration the user clicked
  jwtToken,           // the connected-user token
});

// ...
openDialog();         // mounts the hosted connect flow

Call openDialog() and ByteChef mounts its ConnectDialog into a portal in your page. From the user's side it's a clean, hosted modal: pick the account, get bounced through the provider's OAuth consent (or enter an API key), and land back connected. From your side, the authorize URL, the callback, the code exchange and the scope handling all run inside that portal — per provider — and the tokens that come back are stored encrypted against this connected user, in this environment, then refreshed for as long as the connection lives. You never see or hold a credential.

The dialog owns its own lifecycle, so its closing is something you observe rather than subscribe to: the hook hands back openDialog and closeDialog, and the sample app watches the #connect-dialog-portal node empty out to know when to refresh its list. That's it — that's a production-grade "Connect your Salesforce" button, for any of ByteChef's 280+ connectors, without you writing a single OAuth line. If you have used Paragon, the ConnectDialog is ByteChef's answer to its Connect Portal.

After the Connect: the Same Dialog Manages It

A connection is not a one-time event. Users come back to change what is switched on, or to revoke it altogether, and that does not need a second UI: openDialog() already covers both cases.

When the dialog opens it looks for an existing instance, the one named by integrationInstanceId if you passed it, otherwise the first this connected user already has. Find one and it opens in manage mode instead of the auth flow:

const {openDialog} = useConnectDialog({
  baseUrl,
  environment,
  integrationId,
  jwtToken,
  integrationInstanceId,   // optional: manage one specific connection
});

From there the user can toggle which of your published workflows run for their account, edit the inputs a workflow asks them for, or disconnect. Each is a call ByteChef makes against /api/embedded/v1/integration-instances/... on their behalf. Disconnecting deletes the instance and closes the dialog, so the next openDialog() starts the auth flow again.

That fallback to the first instance is why the sample app's integration card can stay so simple: it flips its label between "Connect" and "Connected" purely on whether integrationInstances is empty, and calls the same hook either way.

What You Didn't Build

Step back and total up what the ConnectDialog absorbed:

  • Per-provider OAuth flows (×280), callbacks, scope handling.
  • Encrypted credential storage, scoped per user and environment.
  • Token refresh, forever.
  • A manage-connection UI: toggle workflows, edit their inputs, disconnect.
  • Environment separation (Development / Staging / Production) out of the box.

Your side of the contract was two small pieces: sign a JWT in your backend, call one hook in your frontend. That's the whole promise of embedded — the integration surface your customers see is yours; the integration machinery is ByteChef's.

What's Next in This Series

Connecting an app is the foundation. Everything else is about turning that connection into automation — and the sample app's Automations page offers five different front doors to do it, each of which gets its own part:

  1. New from Template — start from a pre-built workflow you've published.
  2. New from Embedded Workflow Builder — drop ByteChef's full visual builder into your app.
  3. New from Prompt — generate a workflow from a natural-language description.
  4. New from Chat — build one conversationally.
  5. New from Custom Workflow Builder — craft your own builder UI on ByteChef's APIs.

We'll take them one at a time. But every one of them assumes the user has already connected the apps their automation touches — which is exactly what you just shipped.

Want to follow along? Clone the embedded sample app, sign a JWT, and drop useConnectDialog into a page — your first "Connect" button is minutes away.

Subscribe to the ByteChef Newsletter

Get the latest guides on complex automation, AI agents, and visual workflow best practices delivered to your inbox.