LLMs Technical Reviews

NangoHQ/nango

Integration platform with a 1,000-provider YAML auth catalog, credential-injecting proxy, hosted TypeScript functions and MCP endpoints.

GitHub ↗★ 13kTypeScriptElastic-2.0commit 8da0001 · 2026-10-06homepage ↗

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):

  1. 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).
  2. Enqueue. triggerAction builds a task named after the environment, connection and action. Async actions go into a group with maxConcurrency: 1, so they run one at a time per environment. Sync actions are scheduled and awaited (orchestrator.ts). executeAction gives the task 30 seconds to start and 15 minutes to finish, then long-polls the task output (client.ts).
  3. 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).
  4. Dequeue. In the jobs service, OrchestratorProcessor long-polls dequeue into a PQueue sized to its concurrency and reports failed tasks back (processor.ts). handler branches on task type: sync, abort, action, function, webhook or on-event (handler.ts).
  5. Pick a runtime. startScript fetches the compiled script from local disk or remote storage and asks getRuntimeAdapter for 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).
  6. Execute. The runner wraps the CommonJS bundle and runs it in a node:vm context. console is a no-op, string and WASM code generation are off, and require is limited to url, crypto, zod, botbuilder, soap and unzipper (exec.ts).
  7. Call the API. Inside the script, nango.get/post/proxy goes back to the server’s ProxyService. 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, batchSave sends records to persist.
  8. 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:

  • /mcp serves one connection’s deployed actions as tools, selected with connection-id and provider-config-key headers.
  • /session/:sessionId/mcp is for agent sessions.
  • A separate management router exposes /mcp with 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.yaml entry (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 dryrun and nango deploy (in packages/cli) compile TypeScript syncs and actions written against runner-sdk and push them to an environment. “Zero-YAML” functions export a typed object instead of a nango.yaml declaration. Syncs use batchSave, lastSyncDate and saveCheckpoint for incremental runs.
  • Consumption. The Node client covers connections, proxy, triggerAction/triggerActionAsync, listRecords with 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) runs nango-server, Postgres and Redis, with Elasticsearch optional for logs. It mounts providers.yaml into the container. The image deletes packages/jobs, packages/runner and packages/persist at build time (Dockerfile.self_hosted). A Docker run counts as isHosted, and flagHasScripts is 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:vm is not a security boundary. The runner context receives host-realm Buffer, Error and setTimeout. Isolation comes from running each team on separate runner nodes in production. Outside production, every team shares the default runner, 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?

answered

Nango 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.

Editor's note. Correction: 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?

answered

Integrations 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?

answered

Integrations 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?

answered

Tool 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).

Editor's note. Correction: beyond 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?

answered

Data 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?

answered

Nango 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.

Editor's note. Correction: the self-hosted image removes jobs/runner/persist because Docker mode disables scripts (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.