Skip to content
threada Docs

Marketplace OAuth installs

Threada supports a shared OAuth install path for the first runtime-bound marketplace integrations: GitHub, Atlassian/Jira, Notion, Asana, monday.com, and Intercom, plus contact-scoped HubSpot and Salesforce installs and ticket-scoped Zendesk installs.

This is a runtime integration surface, not a marketplace approval signal. A provider is live only after the provider app/listing is approved and the provider-owned install path has been tested in production or a reviewer-safe environment.

Architecture

  1. An authenticated Studio/admin user starts install from the pack connector readiness panel or directly at /api/v1/integrations/{provider}/oauth/start.
  2. public-bff encrypts and authenticates a stateless OAuth state payload containing provider, tenant, installer user, return URL, optional provider resource host, optional provider resource context, nonce, and expiry.
  3. The provider redirects to /api/v1/integrations/{provider}/oauth/callback.
  4. public-bff exchanges the code, discovers the provider-owned install resource when the provider exposes one, writes the OAuth credential JSON to Secret Manager, and stores only the generated secret_ref in runtime automation.
  5. runtime-automation executes actions through the runtime adapter and reads only the generated Secret Manager credential for the tenant/provider.

public-bff owns the external OAuth edge. Runtime automation owns integration state and execution. Provider tokens are never stored in MongoDB, Pulumi config, logs, or evidence artifacts.

Provider App Setup

Create one provider app/integration per environment and register these callback URLs:

  • GitHub: https://api.<env>.threada.ai/api/v1/integrations/github/oauth/callback
  • Atlassian/Jira: https://api.<env>.threada.ai/api/v1/integrations/atlassian/oauth/callback
  • Notion: https://api.<env>.threada.ai/api/v1/integrations/notion/oauth/callback
  • Asana: https://api.<env>.threada.ai/api/v1/integrations/asana/oauth/callback
  • monday.com: https://api.<env>.threada.ai/api/v1/integrations/monday/oauth/callback
  • Intercom: https://api.<env>.threada.ai/api/v1/integrations/intercom/oauth/callback
  • HubSpot: https://api.<env>.threada.ai/api/v1/integrations/hubspot/oauth/callback
  • Salesforce: https://api.<env>.threada.ai/api/v1/integrations/salesforce/oauth/callback
  • Zendesk: https://api.<env>.threada.ai/api/v1/integrations/zendesk/oauth/callback

Use https://api.threada.ai/... for production.

Provider-owned marketplace install buttons and reviewer notes should point at Studio public install aliases, not directly at BFF OAuth routes. Studio resolves the authenticated tenant, requires exactly one enabled Studio-compatible pack, and sends the installer through the same connector readiness return path used by the in-app connector panel:

  • Slack: https://studio.threada.ai/integrations/slack/install
  • GitHub: https://studio.threada.ai/integrations/github/install
  • Atlassian/Jira: https://studio.threada.ai/integrations/atlassian/install
  • Notion: https://studio.threada.ai/integrations/notion/install
  • Asana: https://studio.threada.ai/integrations/asana/install
  • monday.com: https://studio.threada.ai/integrations/monday/install
  • Intercom: https://studio.threada.ai/integrations/intercom/install
  • HubSpot: https://studio.threada.ai/integrations/hubspot/install
  • Salesforce: https://studio.threada.ai/integrations/salesforce/install
  • Zendesk: https://studio.threada.ai/integrations/zendesk/install

These aliases are executable install entrypoints only for providers with a BFF OAuth implementation and a Studio alias listed above. Zendesk installs require the target Zendesk account subdomain; Studio appends it as zendesk_subdomain when the installer starts OAuth from the connector readiness panel, and provider-owned links may include the same query parameter. monday.com installs require a reviewer/customer board mapping before OAuth starts; Studio appends board_id or monday_board_id and may append group_id or monday_group_id when the installer selects a board/group from the connector readiness panel. Provider-owned reviewer links may include the same query parameters. Do not create OAuth aliases for Zapier, Teams, Google Workspace, Freshworks, Okta, or cloud marketplaces until the corresponding provider-approved OAuth or acquisition flow exists in runtime code. Google Workspace, Zapier, Teams, Datadog, PagerDuty, Discord, Freshdesk, and Okta use the separate marketplace directory setup alias. Slack keeps its OAuth setup alias for command installs and also exposes the authenticated admin credential install route for governed post-message setup. Datadog, PagerDuty, Discord, Freshdesk, Okta, and Slack expose that route at /api/v1/integrations/{provider}/directory-install. Those providers are documented in Marketplace follow-on setup and support; Google Workspace is documented in Google Workspace setup and support; Zapier is documented in Zapier setup and support; Teams is documented in Microsoft Teams setup and support; Freshdesk is documented in Freshworks Marketplace for Freshdesk.

Secret Manager

Seed these Secret Manager values per environment before enabling the matching marketplace install routes:

  • marketplace-oauth-state-secret-<env>
  • github-oauth-client-id-<env> and github-oauth-client-secret-<env>
  • atlassian-oauth-client-id-<env> and atlassian-oauth-client-secret-<env>
  • notion-oauth-client-id-<env> and notion-oauth-client-secret-<env>
  • asana-oauth-client-id-<env> and asana-oauth-client-secret-<env>
  • monday-oauth-client-id-<env> and monday-oauth-client-secret-<env>
  • intercom-oauth-client-id-<env> and intercom-oauth-client-secret-<env>
  • hubspot-oauth-client-id-<env> and hubspot-oauth-client-secret-<env>
  • salesforce-oauth-client-id-<env> and salesforce-oauth-client-secret-<env>
  • zendesk-oauth-client-id-<env> and zendesk-oauth-client-secret-<env>

Non-prod Pulumi mounts those optional secrets and seeds disabled __NO_AUTH__ versions for unset provider credentials. Prod leaves optional marketplace secrets unmounted by default; enable prod mounting with services:publicBffOptionalMarketplaceSecretRefs or PUBLIC_BFF_OPTIONAL_MARKETPLACE_SECRET_REFS set to all, none, or a comma-separated list of secret refs such as marketplace-oauth-state-secret,notion-oauth-client-id,notion-oauth-client-secret. Selected OAuth providers must include both client id and client secret refs. The services stack owns the optional prod Secret Manager containers; run it once to create those containers, add real secret versions out-of-band, then enable the selector. Every selected prod ref must already have a valid Secret Manager version before the deploy that mounts it. If a prod optional container was created out-of-band before Pulumi owned it, import or reconcile it before selecting the ref.

Installed tenant OAuth and marketplace directory credentials are created at runtime under threada-marketplace-oauth-<env>-<provider>-<tenant_hash>. public-bff has a minimal custom writer role for that prefix, and internal-runtime has conditioned Secret Manager accessor IAM for the same prefix.

marketplace-oauth-state-secret-<env> must be a non-placeholder random secret with at least 32 bytes, for example output from openssl rand -base64 32. public-bff rejects the configured value at startup if it is too short or a known placeholder; if the value is absent, install routes fail closed as service-unavailable until the secret is mounted. Rotating this value invalidates only in-flight OAuth install callbacks; installed provider credentials are stored separately.

The stored runtime secret_ref intentionally points at the stable Secret Manager secret resource, not at a specific version. Runtime secret resolution therefore reads versions/latest, which keeps manual rotation and future token refresh flows from pinning actions to stale credentials.

Runtime Binding

The OAuth callback upserts an enabled runtime integration with a deterministic tenant/provider integration_id, the provider IntegrationType, and the Secret Manager secret_ref. Reinstalling updates the same integration rather than creating duplicates. Marketplace installs also store a marketplace_capability_set and marketplace_allowed_action_types metadata contract; runtime adapters must enforce that contract before executing any provider write.

When install_source=marketplace_oauth or install_source=marketplace_directory_credential, runtime action handlers fail closed if marketplace_allowed_action_types is missing or does not include the requested action. The check runs before provider-specific endpoint validation, Secret Manager reads, token parsing, or outbound HTTP. That makes the marketplace descriptor, OAuth scopes or directory credential contract, installed integration metadata, and executable runtime action boundary one contract rather than separate launch-check paperwork. The same guard is wired into runtime-bound follow-on and store action handlers such as Zapier, ServiceNow, Freshdesk, Datadog, PagerDuty, Okta, and Slack when those actions are executed from marketplace-installed metadata.

The current v1 executable actions are:

  • GitHub issue creation
  • Atlassian/Jira issue creation and update through the governed Jira issue-context binding
  • Notion page creation
  • Asana task creation
  • monday.com item creation
  • Intercom ticket creation
  • HubSpot contact creation/update
  • Salesforce contact creation/update
  • Zendesk ticket creation
  • Freshdesk ticket creation
  • Datadog event posting
  • PagerDuty incident triggering
  • Okta user upsert
  • Slack channel post-message

Most current v1 installs are one enabled runtime integration per tenant/provider. Reinstalling the same provider updates that stable integration. Notion, Asana, monday.com, Intercom, and HubSpot are keyed by provider-owned identity, so reinstalling the same workspace or portal updates the same integration while different workspaces, monday.com boards, or portals produce separate runtime bindings. Supporting multiple GitHub orgs, Salesforce orgs, monday.com accounts beyond board-level mapping, Zendesk subdomains, or other provider accounts in the same Threada tenant requires adding provider account identity to the runtime integration key and provider-specific identity probes before marketplace launch gates can treat multi-account installs as supported.

Atlassian/Jira installs use OAuth 2.0 3LO with audience=api.atlassian.com and prompt=consent, discover authorized Jira Cloud resources through Atlassian’s accessible-resources endpoint, and store the selected Atlassian Cloud ID as the provider resource key. Install URLs may include atlassian_site, jira_site, subdomain, or resource; the BFF normalizes that value to a single https://<site>.atlassian.net origin and fails closed when the token grants no Jira site or multiple Jira sites without an explicit site selection. The v1 runtime metadata uses the jira_issue_context_v1 capability set and an explicit jira_create_issue/jira_update_issue action allowlist; the Jira runtime adapter still rejects any other Jira action before reading credentials or contacting Atlassian. Broader Jira write claims still require separate provider scopes, allowlist updates, and approval evidence.

Notion installs require the OAuth token response workspace_id; the callback stores it as notion_workspace_id, uses it as the provider resource key, and stores optional notion_workspace_name and notion_bot_id metadata. The runtime endpoint URL is fixed to https://api.notion.com/v1. Token responses without a stable workspace ID fail closed before Secret Manager storage or runtime integration upsert. The v1 Notion action still requires the target database to be explicitly shared with the Notion connection and mapped through the governed parent_database_id action configuration.

Asana installs use the configured workspaces:read tasks:write OAuth scopes. After token exchange, the callback calls Asana workspace discovery, binds the runtime integration to the selected authorized workspace GID, and stores the workspace ID, plus the workspace name when Asana returns it, in runtime metadata and stores the workspace ID in the Secret Manager credential payload. Install URLs may include asana_workspace, workspace, or resource; the BFF requires one of those values when the token can see more than one workspace and fails closed before Secret Manager storage or runtime integration upsert when the selected workspace is not authorized. The runtime endpoint URL is fixed to https://app.asana.com/api/1.0, and the runtime adapter revalidates that host/path before reading credentials. Project mapping remains an explicit governed runtime configuration and mismatched project/workspace payloads are rejected.

monday.com installs use the configured boards:read boards:write OAuth scopes and require a selected board_id before OAuth starts. The signed OAuth state carries that board mapping plus optional group_id; after token exchange the callback verifies the selected board and group through the monday.com API, then stores the OAuth credential in Secret Manager under the selected board, upserts a board-keyed runtime integration, and stores board_id and optional group_id metadata. The runtime endpoint URL is fixed to https://api.monday.com/v2, and the runtime adapter revalidates that endpoint and rejects payloads targeting a different board or configured group before reading credentials.

OAuth credentials that include an expires_at value fail closed after expiry unless that provider has an implemented runtime refresh path. Zendesk refreshes expired marketplace OAuth credentials on use with the central OAuth client configuration, writes the refreshed tenant credential as a new Secret Manager version, and requires reconnect only when the refresh token is missing, revoked, or rejected.

Salesforce installs use PKCE and require the OAuth token response instance_url; the callback only accepts Salesforce-owned HTTPS instance hosts, stores the normalized instance origin on the runtime integration, and runtime execution revalidates that host before sending bearer-token requests.

Intercom v1 uses a Developer Hub app configured with the write_tickets permission for governed ticket creation. Intercom permissions are configured in Developer Hub; Threada does not send a dynamic scope parameter on the Intercom OAuth authorization URL. After token exchange, the callback calls Intercom /me, stores the returned app.id_code as intercom_workspace_id, uses that workspace ID as the provider resource key, and stores the official Intercom API origin on the runtime integration. If Intercom returns a regional workspace, the runtime endpoint is pinned to the matching official regional API host, and runtime execution revalidates that Intercom host before reading credentials. Conversation read/sync, notes, tags, replies, Canvas apps, packaged knowledge sync, and generic inbox automation require separate runtime actions, provider permissions, allowlist updates, and App Store evidence before launch copy can claim them.

HubSpot installs use the HubSpot OAuth 2026-03 token introspection endpoint after code exchange to discover the authorized portal hub_id without placing the access token in a URL path. The callback stores the hub_id as hubspot_portal_id, uses it as the provider resource key, stores optional portal domain/user/app metadata, and sets the runtime endpoint to https://api.hubapi.com. HubSpot v1 install scopes are intentionally contact-only (crm.objects.contacts.read and crm.objects.contacts.write). Runtime execution only allows hubspot_upsert_contact for marketplace OAuth installs and validates that the configured endpoint targets the HubSpot CRM API origin before reading the stored access token.

Salesforce v1 uses the minimum OAuth scope set needed for REST contact writes, with the contact-only boundary enforced by runtime action allowlists because Salesforce OAuth scopes are not object-specific enough for that distinction. Deal, ticket, opportunity, and packaged knowledge workflows require broader provider scopes, runtime allowlist updates, and separate marketplace evidence before launch copy can claim them.

Zendesk v1 install scopes are tickets:write for governed ticket creation. Zendesk uses account-subdomain-scoped global OAuth URLs, so the installer requires a single Zendesk account subdomain and stores the normalized https://<subdomain>.zendesk.com endpoint on the runtime integration. The v1 runtime binding allows zendesk_create_ticket and runtime token refresh; ticket update, comments, macros, Marketplace request headers, revocation and disconnect UX, and packaged knowledge workflows require separate runtime allowlist and Zendesk review evidence before launch copy can claim them.

Launch/readiness checks must run after these runtime paths are deployed and provider accounts/apps are configured.