NangoHQ/nango
Integration platform with a 1,000-provider YAML auth catalog, credential-injecting proxy, hosted TypeScript functions and MCP endpoints.
Overview
Nango is integration infrastructure for SaaS products and AI agents. It does three jobs. First, it owns the auth dance with third-party APIs: OAuth, API keys, basic auth, client credentials, JWT apps and more than a dozen other modes, all driven from one provider catalog. Second, it runs a credential-injecting proxy. Third, it hosts functions, TypeScript actions and syncs that you write against Nango’s SDK and that run on Nango’s infrastructure. Your backend or your agent then calls the Nango API, the Node client, or one of Nango’s MCP endpoints, and never handles a refresh token itself.
The provider catalog is data, not code. packages/providers/providers.yaml is a 29,500-line file with roughly 1,000 entries. Each entry names the auth mode, the authorize and token URLs, the proxy base URL with ${connectionConfig.*} templating, rate-limit headers, a pagination recipe, and optional hook scripts for webhooks and post-connection steps. Adding an API usually means adding YAML. Integration logic (which endpoints to sync, what an action does) is user-authored TypeScript that the CLI compiles and deploys.
The repository is a large Node/TypeScript monorepo with over 40 packages. It is licensed under the Elastic License 2.0, which is source-available but not OSI open source. It contains the whole cloud product, but the self-hosted Docker image is a deliberately reduced edition. That distinction matters more than anything else on this page (see Running it).
Architecture
flowchart LR
APP["Your backend / agent"] --> SRV["server (Express API, MCP, Connect UI)"]
SRV --> PRX["ProxyService"]
PRX --> API["Third-party API"]
SRV --> ORC["orchestrator + scheduler (Postgres queue)"]
JOBS["jobs (task processor)"] -->|"dequeue"| ORC
JOBS --> FLEET["fleet: runner nodes"]
FLEET --> RUN["runner: node:vm exec"]
RUN -->|"nango.get / proxy"| SRV
RUN --> PER["persist"]
PER --> REC["records (Postgres)"]
SRV --> DB["Postgres + Redis"]
| Component | Path | Role |
|---|---|---|
| API server | packages/server |
Public/private REST API, OAuth callbacks, proxy, webhooks ingress, three MCP servers |
| Shared services | packages/shared |
Connections, credential refresh, encryption, orchestrator client |
| Provider catalog | packages/providers |
providers.yaml and providers.scopes.yaml, alias-resolving loader |
| Orchestrator | packages/orchestrator, packages/scheduler |
Task/schedule service over Postgres (FOR UPDATE SKIP LOCKED) |
| Jobs | packages/jobs |
Dequeues tasks and dispatches them to a runtime (runner fleet or Lambda) |
| Fleet | packages/fleet |
Manages runner nodes on local processes, Render, Kubernetes or Lambda |
| Runner | packages/runner, packages/runner-sdk |
Executes deployed function code; the nango object scripts call |
| Persist / records | packages/persist, packages/records |
Writes sync records and logs; serves the record cache |
| CLI and clients | packages/cli, packages/node-client, packages/connect-ui |
Author and deploy functions; call the API; embedded auth UI |
How a request flows
Take an agent or backend calling nango.triggerAction("hubspot", "user-42", "create-contact", input):
- API entry. The public route resolves the environment and calls
executeAction. That function loads the connection and the provider config, resolves the deployed function, refuses disabled ones, opens an activity-log context, and calls the orchestrator client (action.service.ts). - Enqueue.
triggerActionbuilds a task named after the environment, connection and action. Async actions go into a group withmaxConcurrency: 1, so they run one at a time per environment. Sync actions are scheduled and awaited (orchestrator.ts).executeActiongives the task 30 seconds to start and 15 minutes to finish, then long-polls the task output (client.ts). - Schedule. The orchestrator stores tasks and schedules in Postgres. Due recurring syncs are picked with
FOR UPDATE SKIP LOCKED, so several orchestrator instances can share the table (scheduling.ts). - Dequeue. In the
jobsservice,OrchestratorProcessorlong-pollsdequeueinto aPQueuesized to its concurrency and reports failed tasks back (processor.ts).handlerbranches on task type: sync, abort, action, function, webhook or on-event (handler.ts). - Pick a runtime.
startScriptfetches the compiled script from local disk or remote storage and asksgetRuntimeAdapterfor a runner or Lambda adapter (start.ts, runtimes.ts). In production each team gets its own runner id, and if that runner cannot start the call falls back to a shared default runner (runner.ts). - Execute. The runner wraps the CommonJS bundle and runs it in a
node:vmcontext.consoleis a no-op, string and WASM code generation are off, andrequireis limited tourl,crypto,zod,botbuilder,soapandunzipper(exec.ts). - Call the API. Inside the script,
nango.get/post/proxygoes back to the server’sProxyService. That service checks base-URL overrides and plan capping, loads the integration and connection, refreshes credentials if needed, and sends the request with retries (proxy.service.ts). For syncs,batchSavesends records topersist. - Return. The runner reports the output, the task becomes
SUCCEEDED, and the waiting API call returns the action result.
Key components
Provider catalog
loadProvidersYaml reads the YAML once and resolves alias: entries by shallow-merging the parent with overrides. This is how sandbox and regional variants avoid copying whole entries (index.ts). A typical entry, HubSpot, declares OAUTH2, a portalId connection field, three hook scripts (post-connection, pre-deletion, webhook routing), a rate-limit header and a cursor pagination recipe (providers.yaml). The proxy and nango.paginate() read these fields, so most per-API behaviour is configuration.
Credentials and refresh
Connection credentials are encrypted with AES-256-GCM in EncryptionManager.encryptConnection. If no encryption key is configured, shouldEncrypt() is false and credentials are stored as plain JSON (encryption.manager.ts). Refresh is guarded twice. Within one process, an in-flight map shares a single refresh promise. Across processes, a Redis lock is taken per connection with a 10-second TTL. The code’s own comment notes that this lock is not safe across several Redis instances (refresh.ts). Failed refreshes enter a cooldown, and connections are eventually marked refresh_exhausted.
MCP surfaces
The server exposes three MCP endpoints:
/mcpserves one connection’s deployed actions as tools, selected withconnection-idandprovider-config-keyheaders./session/:sessionId/mcpis for agent sessions.- A separate management router exposes
/mcpwith tools for provisioning integrations, functions and syncs (routes.public.ts, L478-L482).
The agent-session server is the agent-oriented one. It registers four meta-tools: nango_tool_search, nango_execute, nango_proxy and nango_create_connection. It also registers every tool the session’s toolset allows, but it pages tools/list at 50 entries. Searchable-but-unlisted tools stay callable by name or through nango_execute (sessionServer.ts, L60-L154).
Webhooks
Incoming provider webhooks hit routeWebhook. It looks up the provider’s webhook_routing_script in a table of dozens of per-provider handler files, lets the handler map the payload to connections and to syncs that have onWebhook logic, and then forwards a signed copy to your own webhook URL if your plan allows it (webhook.manager.ts).
Extending it
- New API. Add a
providers.yamlentry (auth mode, URLs, proxy config, optional pagination and hook script names). There is no per-provider TypeScript unless the API needs a webhook router or a post-connection hook. - Functions.
nango init,nango dev,nango dryrunandnango deploy(inpackages/cli) compile TypeScript syncs and actions written againstrunner-sdkand push them to an environment. “Zero-YAML” functions export a typed object instead of anango.yamldeclaration. Syncs usebatchSave,lastSyncDateandsaveCheckpointfor incremental runs. - Consumption. The Node client covers connections,
proxy,triggerAction/triggerActionAsync,listRecordswith cursor pagination, and connect sessions. Agents can use the MCP endpoints instead.
Running it
- Nango Cloud is the full product.
- Self-hosted Docker (
docker-compose.yaml) runsnango-server, Postgres and Redis, with Elasticsearch optional for logs. It mountsproviders.yamlinto the container. The image deletespackages/jobs,packages/runnerandpackages/persistat build time (Dockerfile.self_hosted). A Docker run counts asisHosted, andflagHasScriptsis true only for local, enterprise, cloud and test (detection.ts). The free self-hosted edition therefore gives you auth, the Connect UI and the proxy, but not syncs, actions or functions. Running those yourself requires the enterprise mode (NANGO_ENTERPRISE). - Local development runs every service from the monorepo with
RUNNER_TYPE=LOCAL, which spawns runner child processes.
Strengths and caveats
- Strength: breadth as data. About 1,000 providers live in one reviewed YAML file. Pagination and rate-limit hints sit beside auth, so the proxy and the SDK get per-API behaviour for free.
- Strength: production-grade auth plumbing. Per-connection refresh locking, refresh backoff and exhaustion tracking, encrypted credentials, and a white-label Connect UI.
- Strength: real job system. A Postgres-backed scheduler with heartbeats, timeouts and retries, per-team runner fleets on Kubernetes, Render or Lambda, and durable records with cursor reads.
- Caveat: self-host is auth + proxy only. The open image strips the function runtime, so the “1,000 APIs for your agent” story needs Cloud or an enterprise deployment.
- Caveat:
node:vmis not a security boundary. The runner context receives host-realmBuffer,ErrorandsetTimeout. Isolation comes from running each team on separate runner nodes in production. Outside production, every team shares thedefaultrunner, and production falls back to it when a team’s runner fails. - Caveat: plaintext fallback. If you forget
NANGO_ENCRYPTION_KEY, credentials are stored unencrypted. - Caveat: licence and size. Elastic License 2.0 forbids offering Nango as a managed service. The codebase is large: dozens of packages, several datastores (Postgres, Redis, optional ClickHouse and Elasticsearch) and cloud-specific services in the same tree.
Sources: code at 8da0001, OpenDeepWiki wiki (36 pages), verified Q&A.
How it answers the API layer & connectors questions
Each answer was drafted by a code-reading agent at commit 8da0001. Its citations were checked mechanically. Compare with the other api layer & connectors →
How is third-party authentication implemented?
answeredNango supports 15 distinct auth_mode values, the most numerous being API_KEY (344 providers), OAUTH2 (304), OAUTH2_CC (106), BASIC (100), and TWO_STEP (68). The providers.yaml at the package root defines each provider's auth mode, credential schema, and token URLs — Zendesk Sell shows a typical OAUTH2 entry with authorization_url, token_url, refresh_params, and proxy config. OAuth2 flows follow the standard authorization-code grant; the server's OAuth controller in oauth.controller.ts handles the callback, stores tokens, and initiates refresh. API keys and Basic auth use dedicated handlers: postApiKey.ts validates an apiKey credential against the provider's schema while postBasic.ts handles username/password pairs. Credential storage encrypts artifacts via node:crypto HKDF + JWE AES-256-GCM in crypto.ts. The Data Encryption Key comes from the DekRegistry in env.ts. Token refresh runs through refreshOrTestCredentials invoked by the proxy service before every request, with a 60-second memoized TTL. Multi-tenant connected accounts are scoped by connection_id + providerConfigKey, resolved via connectionService.getConnection(). The Connect UI in connect-ui/ provides a white-label embedded auth flow launched through connectSession.service.ts with session tokens.
oauth-server/lib/crypto.ts (HKDF + JWE) encrypts the MCP OAuth server's own artifacts, not connections. Connection credentials are encrypted with AES-256-GCM by EncryptionManager.encryptConnection using NANGO_ENCRYPTION_KEY, and are stored in plaintext if no key is configured.How is an integration / connector defined?
answeredIntegrations are defined in packages/providers/providers.yaml, a 29,523-line YAML file declaring 1,034 providers. Each entry includes display_name, categories, auth_mode, OAuth endpoints (authorization_url, token_url, refresh_params), proxy configuration (base_url with ${connectionConfig.*} templating, headers, verification), credential schemas, and connection config schemas. The YAML is loaded at runtime by packages/providers/lib/index.ts via js-yaml, with alias-based inheritance for provider variants. A separate providers.scopes.yaml defines OAuth scopes per provider. Code vs config: provider templates are static YAML; users create integrations ("provider configs") in a given environment, binding custom credentials. The IntegrationService class in integration.service.ts handles get/list/create/update/delete with rich error types. The buildIntegrationConfig helper transforms creation requests into DBCreateIntegration objects. Scripts (syncs/actions) are user-written TypeScript that can be bundled into nango-yaml format. The nango-yaml package's parser.ts parses v1/v2 format sync/action definitions. Versioning of deployed scripts is tracked via DBFunctionConfigVersion in the database. There is no OpenAPI codegen; integrations ship as curated YAML templates.
How are integrations exposed to LLM agents?
answeredIntegrations are exposed to LLM agents via MCP (Model Context Protocol) servers using @modelcontextprotocol/server and @modelcontextprotocol/node. The Agent Session MCP server (sessionServer.ts) registers every tool from the session's compiled toolset. Four meta-tools drive agent interaction: nango_tool_search (fuzzy search via Fuse.js — agentSessionToolSearch.service.ts), nango_execute (execute/execute.ts) to run any tool by name, nango_proxy for direct credential-injected API calls, and nango_create_connection to start auth flows. The Management MCP server (managementServer.ts) exposes 18 tools (list integrations, deploy functions, trigger syncs, proxy requests, etc.) for provisioning, authenticated by API key or OAuth2. Both servers support OAuth2 authentication via the OAuth server in oauth-server/lib/provider.ts using oidc-provider with PKCE required, rotating refresh tokens (rotateRefreshToken), and resourceIndicators. Tool tools/list paginates at 50 tools per page (sessionServer.ts:142-151). Tool search uses Fuse.js fuzzy matching across action name, description, integration, and provider names with stopword removal. The toolset policy (agentSessionToolset.service.ts) controls which integrations and actions are available per session, with * allowing all, explicit allow/deny lists, and pinned vs searchable tools.
How is a tool call executed?
answeredTool calls execute through executeAction in action.service.ts which resolves the connection and function, then delegates to the orchestrator. The runner (packages/runner/lib/exec.ts) executes TypeScript in a sandboxed node:vm context — require() is restricted to only url, crypto, zod, botbuilder, soap, unzipper; console is a no-op proxy; codeGeneration disables strings and wasm. No container isolation is used. The proxy (proxy.service.ts:136-370) is the direct-execution path: it resolves the integration, loads/refreshes credentials, builds an InternalProxyConfiguration from the provider's proxy.base_url and headers template (interpolating ${apiKey}, ${connectionConfig.*}, etc.), then makes the HTTP request via axios. Rate limiting uses ratelimit.middleware.ts and a separate webhook ingress rate limiter. Plan-level capping is checked before every proxy request and webhook forward. Retries are configured per-request via the retries header, with maxWaitMs for backoff in the ProxyRequest class. Error mapping distinguishes upstream errors (returned as upstream_error with the provider's status) from infrastructure errors (ProxyServiceError with typed codes like base_url_override_disabled, connection_not_found, credentials_refresh_failed, plan_limit).
node:vm, functions run on separate runner nodes managed by the fleet package (local child processes, Render, Kubernetes or Lambda), with one runner per team in production and a shared default runner otherwise.How are data sync, webhooks and triggers implemented?
answeredData syncs are built on the Scheduler package at packages/scheduler/lib/scheduler.ts, a cron-like task engine with states (CREATED, STARTED, SUCCEEDED, FAILED, EXPIRED, CANCELLED) and three daemons: SchedulingDaemon (evaluates schedules), ExpiringDaemon (expires stale tasks), CleaningDaemon (cleanup). Scheduled syncs are triggered via postSyncStart.ts (unpause a recurring sync) and postTrigger.ts (one-shot trigger with modes incremental, full_refresh, or full_refresh_and_clear_cache). Both call syncManager.runSyncCommand. Incremental cursors are implemented in packages/records/lib/cursor.ts as base64-encoded last_modified_at||id strings. The records package provides sync state tracking with upsert semantics. Webhook ingestion uses per-provider routing scripts in packages/server/lib/webhook/ — 40+ scripts (HubSpot, Shopify, Salesforce, GitHub, etc.) that parse webhooks, verify signatures (signature.ts), and map to the provider's format. The webhook.manager.ts:25-120 routeWebhook function looks up webhook_routing_script from providers.yaml, dispatches to the matching handler, then fans out via dispatchWebhookExecutions. Large webhook fan-outs use the dispatch queue (dispatch-queue/publisher.ts) backed by AWS SQS with batching (max 10 entries, 1MB).
How is it self-hosted and what is open vs proprietary?
answeredNango is licensed under the Elastic License 2.0 (ELv2) (LICENSE:1-10), which permits use, copy, distribution, and modification but prohibits providing the software as a hosted/managed service to third parties or removing license-key functionality. The entire monorepo is open-source — all packages (server, webapp, runners, oauth-server, providers, records, scheduler, sandbox) are in this repository. Proprietary components like WorkOS-based managed auth are feature-flagged and gated behind the cloud hosted service. Self-hosting uses Docker: Dockerfile.self_hosted starts from the base image and removes jobs, runner, persist packages (production runs them as separate services). The docker-compose.yaml requires PostgreSQL and Redis; Elasticsearch is optional for log storage. Dashboard authentication in self-hosted mode uses basic auth or local username/password (clients/auth.client.ts:43-96) via NANGO_DASHBOARD_USERNAME/NANGO_DASHBOARD_PASSWORD env vars, configurable with FLAG_AUTH_ENABLED and AUTH_ALLOW_SIGNUP. The server entrypoint is packages/server/entrypoint.sh. The providers.yaml must be volume-mounted. What requires the cloud: license key validation, managed auth (WorkOS/SSO/MFA), billing, and the hosted runner fleet — the self-hosted Dockerfile strips runner/jobs packages, implying sync/action execution runs in-process.
flagHasScripts is false unless local, enterprise, cloud or test). Free self-hosting provides auth, Connect UI and proxy only; syncs and actions need Cloud or NANGO_ENTERPRISE.