Skip to main content
Integrations connect app agents and generated app code to external services such as Linear, HubSpot, Slack, Gmail, Calendar, and other APIs without exposing credentials to the agent runtime or sandboxed iframe. Second supports these credential shapes:
  • an integration grant records what one app asked to use, including provider domain, key slug, auth mode, permission groups, setup steps, app, and requester metadata
  • a static credential stores app-scoped API keys, bot tokens, or private app tokens for static-secret grants
  • an OAuth provider config stores one workspace/provider OAuth client, such as a customer-owned Google OAuth app
  • a connected account stores one user’s OAuth connection metadata and token secret references for one provider config
A configured credential for one app never silently powers another app. If Roadmap Tracker asks for Linear read access and Sprint Writer asks for Linear write access, those are separate grants and separate setup decisions even though both use linear.app. OAuth adds a second boundary: a provider client configured by an admin does not grant access to any user’s data by itself. Each user connects their own account. Agent tools resolve the account from the server-created app-agent run record; app-callable integration actions resolve the account from the current authenticated app viewer.

How it works

Static-secret custom tools:
If the current app grant is missing, unconnected, not configured, or missing a required secret, the endpoint returns a random entry from the tool’s mockData instead of using another app’s credential. OAuth custom tools use the same approved agents.json boundary, but the credential lookup is per triggering user:
App-callable integration actions use the same approved agents.json boundary and the same hardened HTTP executor, but they are called by app code through the iframe bridge:
The iframe sends only toolName and input. It never sends endpoint URLs, secret placeholders, OAuth metadata, credential IDs, or provider tokens. There is no separate OAuth refresh service, cron, sidecar, or Kubernetes job. Refresh happens synchronously inside the existing Next.js API path when a tool needs an access token. The “refresh server” is the provider token endpoint.

Data model

integrations stores app grants:
integration_credentials stores secret material and configured snapshots:
Read models expose configured secret names, never secret values or Vault IDs. OAuth provider configs store workspace/provider OAuth client metadata:
Connected accounts store per-user OAuth state:
Provider keys are workspace-local grouping keys, not a hardcoded registry. The builder discovers official OAuth URLs and scopes from provider docs and writes them into agents.json and integration-setup.json; runtime safety comes from admin approval plus URL/scope invariants.

integration-setup.json

The builder creates integration-setup.json only when this app needs setup. The same provider can appear in many apps because the grant identity includes appId and keySlug.
When the builder calls present_integration_setup, the worker reads this file and posts it to /api/internal/integration-requirements. That sync is idempotent for the current app: listed grants are upserted, and grants no longer present for the app are removed. OAuth setup items use the same outer shape but declare auth.type = "oauth2" instead of static secrets:
For local OAuth smoke tests, run the dev server without portless:
Use the resulting http://localhost:<port> app URL and register the exact redirect URI shown in Second, such as http://localhost:4198/api/oauth/callback. Portless *.second.localhost URLs are useful for normal development, but providers such as Google do not treat them as loopback OAuth redirect URIs. The packaged npx --yes @second-inc/cli local runtime should follow the same plain loopback shape for OAuth-capable local runs; portless is only a npm run dev convenience.

agents.json custom tools

Custom tools reference the same app key with integration.keySlug. If omitted, Second normalizes the slug to "default".
present_agents validates that custom tools include integration metadata, endpoint method, and endpoint URL. Static-secret tools include a named {{secrets.NAME}} placeholder, OAuth tools include auth metadata, and public unauthenticated tools may omit both when the provider’s official API requires no credentials. Draft and published runtime calls then verify the requested tool still appears in the approved agents.json payload before credentials are injected or, for public tools, before the bounded public request is executed. Top-level appTools use the same custom HTTP shape, auth metadata, mock-data behavior, domain lock, and app-scoped integration grant lookup. They are for deterministic app code, not AI agent reasoning:
When an app action and an agent tool use the same provider credential, they share the same grant by using the same domain and keySlug. The builder must write integration-setup.json with the complete union of permissions, scopes, and named secrets required by both. OAuth custom tools are still custom HTTP tools. They declare integration.auth and omit Authorization headers:
OAuth tools must not include {{oauth.access_token}}, {{access_token}}, {{token}}, {{secrets.*}}, or an explicit Authorization header. The broker injects the bearer token after resolving the triggering user from runId for agent tools, or the current app viewer for app-callable actions.

Setup state

A grant needs setup when:
  • no credential is bound to the current app grant
  • the bound credential is not configured
  • a requested permission/scope is not present in configured snapshots
  • a requested required secret name is not present in configured snapshots
  • an OAuth provider config shell exists but has no client ID/secret
  • the current user has not connected the required OAuth account
  • the connected OAuth account is revoked or missing required scopes
Review approval and direct publish check only the current app’s grants. A different app’s configured provider key does not clear the gate.

Secret management

Secrets are injected by replacing named placeholders such as {{secrets.SLACK_BOT_TOKEN}} in URL, headers, query params, and request body. Tool input fields can also be used as placeholders, such as {{query}}. OAuth client secrets and refresh tokens use apps/web/src/lib/oauth/secret-store.ts. When WorkOS Vault is configured, values go to Vault. In local development, the adapter encrypts values with SECOND_TOKEN_ENCRYPTION_KEY or a generated gitignored key under .second-dev/. In production without WorkOS Vault, the local adapter fails closed unless SECOND_TOKEN_ENCRYPTION_KEY is configured. If the agent provides tool input but the endpoint spec does not use any non-secret placeholders, execution fails. This prevents custom tools from accidentally turning a lookup into a broad static API call.

Tool execution constraints

  • HTTPS only, except localhost during development
  • final URL hostname must match integration.domain or one of its subdomains
  • requested tool must be present in the approved app agents.json payload
  • app-callable actions must be present in top-level approved appTools[]; the browser cannot provide endpoint specs or credential metadata
  • runtime grant lookup includes workspaceId, appId, domain, and keySlug
  • OAuth runtime additionally requires runId, loads the run by workspaceId + appId + runId, and resolves the triggering user from that server-created row for agent tools; app actions use the current authenticated app viewer
  • OAuth provider config and connected account lookups include workspaceId, and connected account lookup also includes userId
  • OAuth authorization and token URLs must be HTTPS and resolve outside private network ranges before Second redirects or exchanges tokens
  • external requests to private/internal IP ranges are rejected
  • 30-second external request timeout
  • 1MB response limit
  • mock data is returned for missing or unconfigured integrations, including OAuth missing-account, revoked-account, missing-scope, or provider-config cases
  • live failures return structured, redacted diagnostics such as provider status, provider message, errorCategory, resolution, retryable, and whether the failure is reasonable for the builder to repair

API routes

Workspace realtime publishes compact integration.changed invalidation events after successful mutations. Events may include IDs and key slug metadata, but never secrets, prompts, source files, headers, cookies, or full documents.

Key files