- 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
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: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:
agents.json boundary
and the same hardened HTTP executor, but they are called by app code through the
iframe bridge:
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:
agents.json and integration-setup.json; runtime safety comes from
admin approval plus URL/scope invariants.
integration-setup.json
The builder createsintegration-setup.json only when this app needs setup.
The same provider can appear in many apps because the grant identity includes
appId and keySlug.
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:
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 withintegration.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:
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.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
Secret management
| Mode | Storage | When |
|---|---|---|
| WorkOS Vault | vaultSecretIds[name] in integration_credentials | Production deployments with WorkOS configured |
| Local secret | localSecrets[name] in integration_credentials | Local development without WorkOS |
| OAuth secret store | vault:<id> or local:v1:<ciphertext> refs in provider/account rows | OAuth client secrets, refresh tokens, and optional short-lived access-token cache |
{{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
localhostduring development - final URL hostname must match
integration.domainor one of its subdomains - requested tool must be present in the approved app
agents.jsonpayload - 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, andkeySlug - OAuth runtime additionally requires
runId, loads the run byworkspaceId + 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 includesuserId - 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
| Method | Path | Purpose |
|---|---|---|
GET | /api/workspaces/[wId]/integrations | Projected settings read model grouped by app/key. No secret values or Vault IDs |
POST | /api/workspaces/[wId]/integrations | Rejected for app-blind creates; grants come from app setup sync |
PATCH | /api/workspaces/[wId]/integrations/[id] | Configure or rotate the credential for one app grant |
POST | /api/workspaces/[wId]/integrations/[id] | Reset saved credential state for one app grant |
DELETE | /api/workspaces/[wId]/integrations/[id] | Delete one app grant and its dedicated credential |
POST | /api/internal/integration-requirements | Sync app-scoped grant requirements from integration-setup.json |
POST | /api/internal/workspace-integrations | Return current app grant metadata to the builder, without secret values |
POST | /api/internal/tool-execute | Execute a custom HTTP tool with app-grant credential resolution |
POST | /api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/execute | Execute an approved app-callable integration action for the current app viewer |
POST | /api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/report-failure | Report a repairable draft app backend function failure to the builder |
PATCH | /api/workspaces/[wId]/oauth-provider-configs/[providerConfigId] | Configure or rotate a workspace OAuth client |
GET | /api/workspaces/[wId]/oauth/[providerConfigId]/start | Start current-user OAuth consent for one app grant |
GET | /api/oauth/callback | Generic OAuth callback that exchanges code, stores tokens, and redirects back |
DELETE | /api/workspaces/[wId]/connected-accounts/[accountId] | Revoke the current user’s connected account |
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
| File | Role |
|---|---|
apps/web/src/lib/db/types.ts | Grant and credential document types |
apps/web/src/lib/db/repositories/integrations.ts | Grant sync, credential configure/reset/delete, setup checks |
apps/web/src/lib/db/repositories/oauth-provider-configs.ts | OAuth provider config shell/configure helpers |
apps/web/src/lib/db/repositories/connected-accounts.ts | Connected-account lookup, scope checks, token cache, revoke state |
apps/web/src/lib/oauth/secret-store.ts | WorkOS/local encrypted OAuth secret references |
apps/web/src/lib/oauth/token-exchange.ts | Code exchange and refresh-token request helper |
apps/web/src/lib/oauth/token-broker.ts | On-demand access-token cache/refresh broker |
apps/web/src/lib/integrations/execute-http-action.ts | Shared runtime secret/OAuth injection, mock fallback, response bounds, and domain/IP guards |
apps/web/src/app/api/internal/tool-execute/route.ts | Internal app-agent tool approval check and audit wrapper |
apps/web/src/app/api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/execute/route.ts | Browser-authenticated app action route |
apps/web/src/app/api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/report-failure/route.ts | Draft-only builder recovery reports from generated app code |
apps/web/src/components/app-integration-bridge.tsx | Iframe parent bridge for callIntegrationTool |
apps/web/src/app/api/internal/integration-requirements/route.ts | Worker-to-web setup sync |
apps/web/src/app/api/workspaces/[wId]/integrations/[id]/route.ts | Configure, reset, and delete one app grant |
apps/web/src/app/api/workspaces/[wId]/oauth/[providerConfigId]/start/route.ts | OAuth consent start |
apps/web/src/app/api/oauth/callback/route.ts | OAuth callback |
apps/web/src/app/w/[wId]/settings/integrations/integrations-client.tsx | App/key settings UI |
apps/worker/src/runner.ts | Builder integration tools, present_agents, and custom app tool bridge |
apps/worker/src/workspace-template.ts | Generated app SDK, including callIntegrationTool |