Skip to content

Feature request: single-use "connect links" to authorize a channel without workspace membership #1220

Description

@naybu256

Have you checked for existing requests?

  • I have searched the existing issues and found no similar request.

Feature Area

🔧 Settings, Channels & Integrations

Is your feature request related to a problem? Please describe.

Feature request: single-use "connect links" to authorize a channel without workspace membership

Problem

Connecting an Instagram, TikTok, Messenger (or any OAuth channel) currently requires that the person who holds the social account session is also a logged-in ChatbotX user with access to the target workspace. The OAuth callback enforces both:

  • apps/builder/src/app/integrations/[...integration]/callback.ts ~L377: getCurrentUser()notFound() when there is no session.
  • same file ~L388–L399: hasWorkspaceAccess({ workspaceId, user })notFound() when the user is not a member of the workspace carried in state.

The state itself is only base64 JSON (integrations/instagram/src/apis/auth.ts, integrations/tiktok/src/apis/auth.ts), so the session is what actually binds the authorization to the workspace.

In practice, for agencies and multi-account operators, the person who can complete the OAuth dialog is very often not the workspace owner:

  • a social media manager or field operator who holds the Instagram / TikTok login on their phone,
  • a client who owns the account but should never see the agency's workspace,
  • an onboarding contractor connecting dozens of accounts for many different workspaces.

Today the only workaround is to invite that person as a workspace member with a role high enough to create channels, which also exposes the inbox, contacts and flows of that workspace. Sharing the social account's credentials with a workspace member is the other workaround, and it is worse.

Several API-first tools already solve this with a hosted "linking URL" (Ayrshare, Unipile, bundle.social): the workspace generates a link, sends it to whoever holds the account, that person authorizes, the channel lands in the right workspace. ChatbotX would be, to my knowledge, the first open-source inbox platform to offer it.

Proposed solution

Add connect links: signed, single-use, expiring URLs generated from a workspace for a given channel type, which allow completing the OAuth flow with no ChatbotX session.

1. Data model

New table (or a new kind of row in the existing invitation model, packages/business/src/invitation/service.ts):

channel_connect_link
  id
  workspaceId
  channelType          -- instagram | instagram-facebook | tiktok | messenger | zalo | threads | whatsapp
  reconnectIntegrationId  nullable   -- reconnect variant, see below
  tokenHash            -- only the hash is stored
  createdByUserId
  expiresAt            -- default 7 days, max 30
  usedAt               nullable
  revokedAt            nullable
  resultIntegrationId  nullable

2. Generation

  • UI: Settings → Channels → Add channel → "Create connect link" (next to the current "Connect" button). Shows the URL once, with expiry, and a list of pending / used / revoked links with a Revoke action.
  • Optional API: POST /v1/inboxes/connect-links and DELETE /v1/inboxes/connect-links/{id} on the Platform API, so operators can generate links programmatically and hand them over from their own systems.

The link points to a public page, e.g. /connect/{token}, which:

  1. validates the token (exists, not used, not revoked, not expired),
  2. resolves the platform-credential owner from the workspace, not from a current user (the host-first resolution in apps/builder/src/lib/platform-credential-owner.ts already covers white-label domains; the workspaceId branch covers the platform host),
  3. shows a minimal branded page: "Connect your {channel} account to {workspace name}" with a single button,
  4. redirects to the provider's OAuth dialog with state = { workspaceId, connectLinkId, referer }.

3. Callback

In callback.ts, before the getCurrentUser() gate:

  • if state.connectLinkId is present, load the link, verify it matches state.workspaceId and the channel being completed, and verify it is still valid;
  • run the existing per-channel completion logic for that workspace (token exchange, integration creation, webhook subscription) exactly as it runs today for a logged-in member;
  • mark the link usedAt and store resultIntegrationId in the same transaction (single use; a second callback with the same link must fail);
  • write an audit entry (auditService) with createdByUserId as the actor and a via: connect_link marker;
  • redirect to a neutral public "Connected — you can close this tab" page. No redirect into the workspace, no session created.

The existing white-label relay hop (resolveRelayTarget on state.referer) must still run first so that the branded origin is restored before credentials are resolved.

4. Reconnect variant

Tokens expire and get revoked (TikTok after 30 days of inactivity, Meta on password change). Allow generating a connect link for an existing integration (reconnectIntegrationId), reusing the reconnect handlers in apps/builder/src/lib/channel-reconnect.ts. The callback must verify that the authorized account matches the existing integration's account id, as the current reconnect path already does.

Security considerations

  • Token: 32 random bytes, only the hash stored, compared in constant time.
  • Single use, TTL, revocable, bound to one workspace and one channel type. A link for Instagram cannot be completed with a TikTok authorization.
  • The link grants the ability to add one channel to one workspace, nothing else: no read access, no session, no member role.
  • Rate limit the public /connect/{token} page and the callback branch per IP.
  • Generation requires the same workspace permission as connecting a channel today.
  • Quota enforcement (packages/business/src/quota-enforcement/service.ts) must be applied at generation time and re-checked at completion time.
  • Audit log entries on generation, revocation, use and failed attempts.

Out of scope

  • Any form of impersonation or "login as" for resellers.
  • Programmatic creation of workspace members.
  • Changes to the OAuth apps or scopes themselves.

Contribution

I am happy to implement this as a PR (schema, service, public page, callback branch, UI, tests following the existing patterns in apps/builder/__tests__/channel-reconnect-handlers.test.ts and channels-create-platform-owner.test.ts). Opening this issue first as suggested in CONTRIBUTING.md to agree on the shape before writing more than three lines. Questions I would like maintainers' input on:

  1. Separate table vs. extending the existing invitation model?
  2. Should connect links be a Community feature or Enterprise/white-label only?
  3. Preferred location for the public page: apps/builder route or a dedicated public route group?

Describe the solution you'd like

Add connect links: signed, single-use, expiring URLs generated from a workspace for a given channel type, which allow completing the OAuth flow with no ChatbotX session.

1. Data model

New table (or a new kind of row in the existing invitation model, packages/business/src/invitation/service.ts):

channel_connect_link
  id
  workspaceId
  channelType          -- instagram | instagram-facebook | tiktok | messenger | zalo | threads | whatsapp
  reconnectIntegrationId  nullable   -- reconnect variant, see below
  tokenHash            -- only the hash is stored
  createdByUserId
  expiresAt            -- default 7 days, max 30
  usedAt               nullable
  revokedAt            nullable
  resultIntegrationId  nullable

Describe alternatives you've considered

No response

Additional context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovements to existing features or codebasefeature-requestNew feature proposals or functional ideas

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions