Have you checked for existing requests?
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:
- validates the token (exists, not used, not revoked, not expired),
- 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),
- shows a minimal branded page: "Connect your {channel} account to {workspace name}" with a single button,
- 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:
- Separate table vs. extending the existing invitation model?
- Should connect links be a Community feature or Enterprise/white-label only?
- 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
Have you checked for existing requests?
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.hasWorkspaceAccess({ workspaceId, user })→notFound()when the user is not a member of the workspace carried instate.The
stateitself 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:
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):2. Generation
POST /v1/inboxes/connect-linksandDELETE /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:apps/builder/src/lib/platform-credential-owner.tsalready covers white-label domains; theworkspaceIdbranch covers the platform host),state = { workspaceId, connectLinkId, referer }.3. Callback
In
callback.ts, before thegetCurrentUser()gate:state.connectLinkIdis present, load the link, verify it matchesstate.workspaceIdand the channel being completed, and verify it is still valid;usedAtand storeresultIntegrationIdin the same transaction (single use; a second callback with the same link must fail);auditService) withcreatedByUserIdas the actor and avia: connect_linkmarker;The existing white-label relay hop (
resolveRelayTargetonstate.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 inapps/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
/connect/{token}page and the callback branch per IP.packages/business/src/quota-enforcement/service.ts) must be applied at generation time and re-checked at completion time.Out of scope
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.tsandchannels-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:apps/builderroute 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):Describe alternatives you've considered
No response
Additional context
No response