n8n Security & Engineering Audit
You shipped it.
Is it safe to operate?
We audited n8n across 9 engineering areas - 60 findings, 11 of them critical or high. Here's the picture.
https://github.com/n8n-io/n8n
Share and export
Send this report to a teammate or turn the findings into implementation work.
9 areas evaluated
Each area scored 0-10 by an AnchorStack engineer.
Code Quality
7/10
n8n's codebase is well-structured and enforces strong quality standards, with a comprehensive CI pipeline gating every PR on lint, typecheck, format, and tests. The main risks are concentrated in the execution engine: workflow-execute.ts is a 2874-line monolith with four broad file-level eslint-disable directives suppressing unsafe-member-access and related rules, making it both hard to change safely and easy to introduce regressions. Type safety is excellent in most of the codebase but degrades in the critical execution layer. No test coverage thresholds are configured, so new code can be merged without adding tests as long as existing gates pass.
Architecture
7.2/10
n8n is a mature, dual-mode workflow automation platform built as a TypeScript monorepo. Its architecture cleanly separates a modular Express API/UI server from an optionally distributed execution engine via a Bull/Redis queue, giving operators a simple regular mode (single process) or a scaled queue mode (main + worker + webhook processes). Core strengths are a clear DI-based separation of concerns, typed FE/BE API contracts, well-defined execution lifecycle hooks, and thoughtful queue recovery for dangling executions. Key risks are in-process ActiveExecutions state that is not globally distributed across multiple mains, manual executions defaulting to run on the main process in queue mode (deprecated but still default unless an env flag is set), local-filesystem binary data breaking multi-process deployments, and WaitTracker timer loss on leader failover creating a latency window for execution resumption.
Data
5.9/10
n8n's data layer is TypeORM-backed and supports SQLite (default, single-process) and PostgreSQL (recommended for production/queue mode). The migration discipline is exemplary — 200+ timestamped, transactional migrations with a purpose-built DSL, most offering reversible `down` methods. Schema design is clean, with consistent string/UUID PKs, composite PKs for join tables, and cascade deletes modeled correctly for the tenant/project ownership hierarchy. The three material risks are: (1) API keys are JWT tokens stored in plaintext — any DB read grants immediate API access; (2) credential encryption defaults to AES-256-CBC with MD5-based key derivation for most deployments (the stronger GCM path is behind a feature flag); (3) no backup/restore tooling or runbooks exist in the repository, leaving recovery entirely to operator initiative. The data layer is safe to evolve under normal conditions but would not withstand a direct database breach without significant credential and API-key exposure.
Security
6/10
n8n is a workflow automation platform with a broadly solid security foundation: JWT-based authentication with token revocation, typed scope-based authorization enforced at API boundaries, credential encryption at rest, and hardened Docker images with Trivy scanning and VEX triage. The most significant risk for self-hosted deployments is that SSRF protection is disabled by default, making every instance an open SSRF proxy until the operator explicitly enables it. The JavaScript Code node (non-task-runner path) relies on vm2, a deprecated sandbox with known escape vulnerabilities, creating code-execution risk in the main process. Vulnerability scanning coverage has meaningful gaps: no SAST, no SCA/dependency audit, and the Trivy image scan does not block releases (exit-code 0), so Critical/High container vulnerabilities can ship without operator intervention.
AI Engineering
6.7/10
n8n has strong AI engineering foundations: the AGENTS.md/CLAUDE.md hierarchy is specific, actionable, and hierarchical, covering TypeScript patterns, migration rules, security fix hygiene, and testing conventions. A rich library of 15+ domain-specific Claude Code skills (developer agent, migration guide, protect-endpoints, mutant-diff, reproduce-bug, create-pr, etc.) provides meaningful development leverage. Validation gates are well-enforced through required CI checks and documented pre-commit requirements. The primary gaps are: spec-driven development is aspirational (the skill exists but no specs are found in .claude/specs/), planning artifacts are gitignored and ephemeral, and there is no documented knowledge lifecycle for compounding learnings from AI-assisted work — leaving the team with strong day-to-day AI leverage but limited institutional memory about where AI approaches succeed or fail.
Infrastructure
6/10
n8n's infrastructure posture is strong where the project has direct control: Docker images are hardened (multi-stage Alpine, non-root, tini, pinned digests), reproducible, and attested with SLSA L3 provenance, SBOM, and VEX triage; CI/CD pipelines are comprehensive with typecheck, lint, unit, E2E, DB, and security gates enforced through a required-checks branch-protection gate. The main risks are in two areas: (1) defaults that leave self-hosted deployments insecure out of the box — health check endpoints are disabled, Redis defaults to no auth and no TLS, and benchmark example Compose files ship hardcoded weak credentials that operators may copy directly; and (2) documentation gaps — no backup/restore tooling or runbooks, no rollback procedures for Docker upgrades, and no disaster recovery documentation exist in the repository. n8n's own cloud infrastructure is not present in this repository, making those controls unauditable from the source.
Observability
3.9/10
n8n ships a mature observability framework — Sentry backend/frontend error tracking, Prometheus metrics with 15+ collectors, OpenTelemetry distributed tracing (including UI configuration), structured Winston logging, an internal event bus with typed audit events, and `/healthz` + `/healthz/readiness` endpoints — but every major tool is disabled or left unconfigured by default. A self-hosted instance in default state produces text-format console logs only: no error tracking, no metrics endpoint, no external alerts, no uptime monitoring. Operators must discover and configure each layer independently, without a production-readiness guide, recommended alert rules, runbooks, or SLO targets in the repository. No request correlation IDs are injected into access logs, making per-request debugging difficult. The highest risk is that most self-hosted operators will run n8n with zero production visibility and learn about failures only from user complaints.
Delivery
7.4/10
n8n has a mature, fully automated CI/CD pipeline built on GitHub Actions with SHA-pinned actions, path-filtered smart gating, Renovate dependency management, SLSA L3 provenance, SBOM, and automated multi-track release scheduling. The primary gap is that LLM evaluation jobs are explicitly non-blocking in required-checks despite AI being a central product capability. Feature flag lifecycle management is PostHog-backed but lacks in-repo cleanup discipline. Release traceability is best-in-class for an open-source project at this scale.
Documentation
6.6/10
n8n's documentation is well-organized and mature for an open-source monorepo, with a clear layered structure: README for end-users, CONTRIBUTING.md for contributors, AGENTS.md as the canonical AI-agent context file, and deep technical docs inside key packages. AI-agent context files are a standout strength — AGENTS.md is comprehensive, CLAUDE.md cascades correctly through sub-packages, and the .claude/plugins/n8n/ plugin layer is carefully namespaced and maintained. The main weaknesses are: absence of operational runbooks (deployment, rollback, DB restore, secret rotation) anywhere in-repo; an env-var surface that is explicitly incomplete with no consolidated reference; stale package-path references in AGENTS.md that point to `packages/editor-ui` instead of the actual `packages/frontend/editor-ui`; no formal decision records; and no in-repo privacy disclosure for the diagnostics and telemetry that are on by default. A new contributor can install and build the project from CONTRIBUTING.md, but cannot respond to a production incident or confidently understand the full configuration surface without external resources.
Where to look first
01
— Code Quality
Execution engine is a 2874-line God object with broad safety bypasses
workflow-execute.ts carries four file-level eslint-disable directives suppressing no-unsafe-member-access, no-unsafe-assignment, prefer-optional-chain, and prefer-nullish-coalescing across its entire 2874-line body.
The file blends orchestration, per-node execution, error handling, lifecycle hook dispatch, and event emission in one class. Any change risks unintended side effects in one of the other responsibilities, and the safety bypasses mean TypeScript cannot catch type errors introduced during edits. This is the single file most likely to be the blast radius of a production regression.
Progressively decompose the class into focused collaborators (e.g., NodeRunner, ExecutionHookDispatcher, ExecutionErrorHandler). Re-enable the suppressed lint rules one at a time, fixing the underlying unsafe accesses rather than suppressing them. Use the existing test file as a safety net during each extraction.
02
— Architecture
Manual executions run on main process in queue mode by default
In queue mode, manual and evaluation executions bypass the Bull queue and run synchronously on the main process unless OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true is set; the deprecation service warns but does not enforce.
Under editorial load, manual executions compete with webhook processing, REST API handling, and event bus work on the main process event loop. The main can saturate, increase webhook response latency, and drop editor-UI push events. Memory pressure from large execution payloads on the main is a secondary risk. The deprecation warning makes this visible but does not eliminate the default risk path.
Remove the OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS env flag guard in workflow-runner.ts (line 273-276) and always enqueue in queue mode. Follow the TODO comment '@TODO: Reduce to true branch once feature is stable'. Ensure worker concurrency limits are set appropriately and document the change in upgrade notes. Also remove the parallel guard in workflow-execution.service.ts line 235 and test-runner.service.ee.ts line 403.
03
— Data
API keys stored as plaintext JWTs — database read grants immediate API access
API keys are JWT tokens generated and stored verbatim in the `user_api_keys.apiKey` column; no hashing or KDF is applied before storage.
Any attacker with read access to the database (misconfigured backup, SQL injection, compromised read replica, or leaked DB credentials) can immediately use all stored API keys without any additional cracking. JWT API keys carry full programmatic access to the n8n instance including credential read, workflow execution, and user management endpoints. Unlike bcrypt password hashes which require brute force, plain JWTs are ready to use instantly.
Store only an HMAC-SHA256 or SHA-256 hash of the API key in the database. Return the plaintext key to the user only at creation time. For lookup during authentication, hash the incoming key and compare against the stored hash. A prefix-index scheme (store first 8 chars plaintext for fast lookup, full hash for verification) avoids full-table scans. A migration can hash existing stored keys, but users must be notified to re-create keys since the originals are irrecoverable.
04
— Data
Default credential encryption uses MD5-based key derivation (AES-256-CBC legacy path)
The `Cipher.encrypt()` and `encryptV2()` fallback paths use AES-256-CBC with MD5-based EVP_BytesToKey derivation; the secure GCM path is only active when `N8N_ENV_FEAT_ENCRYPTION_KEY_ROTATION=true` AND the encryption-key proxy is configured.
For the vast majority of self-hosted deployments operating without the feature flag, all credential data at rest uses AES-256-CBC with MD5 key derivation. MD5 is computationally trivial, and the key derivation lacks a proper KDF (no PBKDF2, HKDF, or scrypt), making it vulnerable to brute-force attacks given the ciphertext and a weak or guessable instance encryption key. n8n credentials routinely contain OAuth tokens, database passwords, and third-party API keys. Additionally, CBC mode without an authentication tag cannot detect ciphertext tampering (padding oracle attacks are theoretically possible).
Remove the double gate (`N8N_ENV_FEAT_ENCRYPTION_KEY_ROTATION` env var AND proxy configured check) and make `encryptV2` the default for all new writes. The AES-256-GCM implementation with HKDF-SHA256 derivation is already present and excellent. Provide an automatic migration that re-encrypts all CBC-encrypted credential rows to GCM on next read (lazy migration) or as a one-time admin operation. Update the instance encryption key derivation to use a proper KDF (PBKDF2 or HKDF) rather than raw SHA-256 for the DEK wrapping key.
05
— Security
SSRF protection disabled by default on every deployment
The N8N_SSRF_PROTECTION_ENABLED flag defaults to false in SsrfProtectionConfig, leaving all HTTP-capable nodes able to reach internal network addresses, cloud metadata endpoints, and loopback interfaces without restriction.
Any workflow author (or attacker who triggers a workflow via public webhook) can make the n8n instance send requests to AWS/GCP/Azure metadata endpoints (169.254.169.254), internal services, or other hosts on the deployment's VPC. In cloud environments this routinely leads to credential theft and lateral movement.
Change the default value of `enabled` to `true` in packages/@n8n/config/src/configs/ssrf-protection.config.ts. Provide a migration notice so self-hosted operators with intentional internal-network workflows can opt out via an explicit allow-list (N8N_SSRF_ALLOWED_IP_RANGES / N8N_SSRF_ALLOWED_HOSTNAMES) rather than the current insecure default.
06
— Security
Deprecated vm2 sandbox used for in-process JavaScript Code node execution
When the task runner is not configured, the JavaScript Code node falls back to JavaScriptSandbox, which wraps user-supplied code in vm2's NodeVM — a library that has been officially abandoned and has multiple publicly disclosed sandbox-escape CVEs.
A workflow author (or attacker who can create/modify workflows) can execute arbitrary code outside the sandbox in the main n8n process, gaining full access to the filesystem, environment variables, credentials, and the database connection. This is particularly severe because it runs in the same process context as the credential cipher.
Make the task runner the required execution path for JavaScript code (remove or disable the vm2 fallback). If a fallback must be preserved, replace vm2 with isolated-vm (already used in the agent runtime) to maintain consistent security properties. At minimum, add a startup warning when vm2 fallback is active and document the risk prominently.
07
— Infrastructure
No backup, restore, or disaster-recovery documentation for self-hosted deployments
No backup scripts, restore procedures, or DR runbooks exist anywhere in the repository for either SQLite or PostgreSQL deployments; operators have no recovery path documented.
A self-hosted operator who loses their database host, suffers disk failure, or needs to restore after a bad migration has no project-provided guidance. SQLite users have no guidance on WAL checkpointing or file snapshot frequency; PostgreSQL users have no guidance on pg_dump schedules, point-in-time recovery, or managed-DB snapshot policies. Data loss becomes the likely outcome of any infrastructure incident without operator-initiated external tooling. This is a material risk for any production deployment storing real user workflows and credentials.
Add a docs/operations/backup-restore.md covering: (1) SQLite WAL backup strategy (copy .n8n/database.sqlite with WAL file, recommended backup frequency, restore procedure); (2) PostgreSQL backup via pg_dump or managed snapshot, point-in-time recovery steps; (3) encryption key backup separately from the database (loss of N8N_ENCRYPTION_KEY renders all credentials unrecoverable even with a good DB backup); (4) how to test a restore in a staging environment. A minimal Docker Compose restore example would be highly actionable.
08
— Observability
All production observability tools disabled by default
Sentry DSNs, Prometheus metrics, and OpenTelemetry are all opt-in with empty/false defaults; a default self-hosted deployment has no error tracking, no metrics scraping, and no external alerting.
Self-hosted operators running the default configuration have zero visibility into production failures, error rates, or performance regressions. Incidents will be discovered through user complaints rather than automated detection. This affects the majority of community self-hosted deployments.
Add a production observability quickstart guide (docs/operations/observability.md) listing minimum recommended configuration: (1) set N8N_SENTRY_DSN for error tracking, (2) set N8N_METRICS=true and point a Prometheus scraper at /metrics, (3) configure N8N_LOG_FORMAT=json for structured logs. Provide a docker-compose.observability.yml example bundling n8n + Prometheus + Grafana with pre-configured datasources. At startup, emit a structured WARNING log when the instance appears to be in production mode (non-localhost host, non-development NODE_ENV) but all three tools are unconfigured.
09
— Observability
No alerting rules, runbooks, or incident response documentation
No Prometheus alert rules, Grafana alert examples, runbooks, escalation paths, or documented alert conditions exist anywhere in the repository for common production failure scenarios.
Even when Prometheus is enabled by an operator, there are no recommended alert thresholds for execution failure rate spikes, queue saturation, worker offline, database connection failures, or instance downtime. Operators must design their entire alerting strategy from scratch. Production incidents for self-hosted users will consistently go undetected or be detected late.
Add docs/operations/alerts.md with recommended Prometheus AlertManager rules covering: (1) n8n instance down (up == 0), (2) execution failure rate > 10% over 5m, (3) queue depth > N for > 5m, (4) last_activity gauge not updated in > 10m. Add a minimal runbook template (docs/operations/runbook-template.md) with sections for detection, diagnosis (what to check in logs/metrics), mitigation, and post-incident review. Reference these from the main README.
10
— Observability
Worker/queue health check endpoints disabled by default
QUEUE_HEALTH_CHECK_ACTIVE defaults to false in queue mode, disabling the /healthz endpoint on worker and webhook-processor processes and blocking Kubernetes liveness/readiness probe configuration.
Kubernetes deployments using workers cannot configure liveness or readiness probes without explicitly enabling the health check. Stuck or deadlocked workers will not be automatically restarted, causing silent execution failures and queue backlogs. This is a common deployment pattern for production n8n installations.
Change QUEUE_HEALTH_CHECK_ACTIVE default to true, or document clearly in the worker startup documentation that Kubernetes operators must set QUEUE_HEALTH_CHECK_ACTIVE=true and QUEUE_HEALTH_CHECK_PORT. Add the health check configuration to the Helm chart values with the port and liveness probe pre-configured. Consider binding the health port to localhost-only by default to avoid unintended exposure.
11
— Documentation
No operational runbooks for deployment, rollback, or incident response
The repository contains no runbooks for deploying, rolling back, restoring the database, rotating secrets, or responding to production incidents — operators must find docs.n8n.io during an incident.
Self-hosted operators and contributors who run production instances have no in-repo reference during failures. A database corruption, failed migration, or credential rotation under pressure would require external docs lookup at exactly the moment reliable in-repo context matters most.
Add a `docs/operations.md` file with concise instructions for the most critical operational tasks: Docker deploy and rollback, database backup/restore commands, secret rotation steps (N8N_ENCRYPTION_KEY rotation procedure), and cross-links to docs.n8n.io for full detail. Even a one-page pointer guide reduces operational risk significantly.
12
— Code Quality
No test coverage thresholds — new code can ship without tests
The root jest.config.js and all package-level configs collect coverage when COVERAGE_ENABLED=true but define no coverageThreshold block, so CI never rejects a PR for insufficient coverage.
Contributors can add new features or change critical paths without adding tests. Coverage metrics improve only as a side effect of voluntary effort, not enforcement. This is especially concerning for sensitive paths (auth, credentials, webhook execution) where a single missed branch can expose a vulnerability.
Add a coverageThreshold to jest.config.js (e.g., global branches: 60, functions: 70, lines: 70) and set COVERAGE_ENABLED=true unconditionally in CI. Start with a threshold matching the current measured coverage and ratchet it upward quarterly.
13
— Code Quality
Broad file-level eslint-disable blocks in the request execution layer
routing-node.ts (15 eslint-disable directives), node-execution-context.ts (11 directives), and related request-helper files carry blanket suppression of no-unsafe-member-access, no-unsafe-assignment, and prefer-nullish-coalescing.
TypeScript's type safety is effectively switched off in the code paths that execute node HTTP requests, handle credentials, and process response data. Regressions here are invisible to the type checker and only caught by tests — which lack coverage thresholds (see COD-002).
Convert blanket file-level disables to narrower line-level suppressions with explanation comments, or better, fix the root cause by adding proper type assertions and null guards. Prioritise the request-helpers directory since it sits on the credential and OAuth execution path.
14
— Code Quality
Critical helpers are oversized with suppressed complexity violations
node-helpers.ts (2059 lines, 2 complexity suppressions) and interfaces.ts (3933 lines as a single barrel file) are foundational to workflow execution but have known, un-addressed complexity.
node-helpers.ts mixes parameter resolution, node type introspection, and display logic. The complexity suppressions mean ESLint has explicitly acknowledged that specific functions exceed safe cognitive limits. Developers modifying these files face a high cognitive load and a high risk of unintended breakage across many callsites.
Split node-helpers.ts by responsibility domain (parameter utilities, display utilities, node type utilities) and distribute them to narrower modules. Extract sub-interfaces from interfaces.ts into co-located domain files (e.g., execution interfaces near the execution engine, credential interfaces near credentials).
15
— Architecture
ActiveExecutions in-memory state is per-process and not globally consistent
ActiveExecutions tracks running executions as an in-memory map local to each process; in queue mode with multiple mains, each main only knows about executions it enqueued, and a main restart loses all response promises and streaming contexts.
If the main that enqueued a webhook execution restarts or is replaced by a load balancer, the webhook caller's response promise is orphaned. Streaming responses (SSE) and synchronous webhook responses (responseMode=lastNode) will time out or return a generic error rather than the actual workflow result. In multi-main deployments behind a load balancer without sticky routing, any main restart corrupts in-flight synchronous webhook responses.
For synchronous webhook responses, implement sticky routing at the load balancer (route by execution ID or session) so the same main handles both enqueue and response delivery. Alternatively, for the webhook response path, store pending response metadata in Redis so any main can fulfill the response on job completion. Document this limitation prominently in the multi-main deployment guide.
16
— Architecture
Local filesystem execution and binary data storage breaks multi-process deployments
Execution data can be stored on the local filesystem (storedAt='fs' in FsStore) and binary data defaults to local filesystem; in queue mode deployments, workers and mains do not share local storage, making cross-process data reads fail silently.
A worker that receives a job cannot read binary data written by the triggering main to its local filesystem path, and vice versa. Executions that produce binary output may yield corrupted or missing results. This is a silent failure: the execution appears to succeed but data is inaccessible. Affects any queue-mode deployment that does not explicitly configure S3 or database binary data storage.
Add a startup validation check: when executions.mode === 'queue' and binary data mode is 'filesystem' (or execution data storage is 'fs'), log an error and refuse to start (or emit a prominent warning). Document that queue mode requires either database binary storage or a shared storage volume / S3. Update the queue mode setup guide to make this a precondition.
17
— Architecture
WaitTracker timer state is lost on leader failover, delaying execution resumption
WaitTracker uses in-memory setTimeout handles that are cleared when leadership is lost; on leader takeover, it immediately polls the DB for waiting executions, but executions scheduled to fire during the failover window resume up to 60 seconds late.
Workflows using the Wait node or scheduled resumptions that fire during a leader failover event will resume late. The gap can be up to 60 seconds (the mainTimer interval). In time-sensitive automation workflows (SLA-driven integrations, retry loops with tight deadlines), this can violate business constraints. The 60s poll interval is not configurable through the standard config system.
Reduce the WaitTracker poll interval or make it configurable via an environment variable (e.g., N8N_WAIT_TRACKER_POLL_INTERVAL). Alternatively, consider scheduling wait-resumptions as time-delayed Bull jobs on creation, which survive leader failover because they live in Redis. This would eliminate the in-memory timer dependency entirely.
18
— Data
MFA secrets stored as plaintext strings — TOTP bypass on DB compromise
`User.mfaSecret` is a nullable `varchar` column with no encryption; `mfaRecoveryCodes` is stored as a `simple-array` string with no encryption either.
An attacker with database read access can extract TOTP secrets and recovery codes for all MFA-enrolled users. TOTP secrets allow generating valid 2FA codes, enabling account takeover even when the attacker does not have the user's password. Recovery codes, once read, allow irrevocable account access. This is especially serious because MFA-enrolled users are typically the most security-conscious (admins, owners).
Encrypt `mfaSecret` and `mfaRecoveryCodes` before persistence using the instance encryption key (or the GCM key once that path is the default). Decrypt on read in `mfa.service.ts`. A migration should re-encrypt existing plaintext secrets for already-enrolled users at startup.
19
— Data
UserRepository.createUserWithProject has an acknowledged race condition — missing transaction
The method explicitly comments `// TODO: use a transactions / This is blocked by TypeORM having concurrency issues with transactions` and proceeds without wrapping user + personal-project + project-relation creation in a single transaction.
Under concurrent user creation (LDAP sync, mass invitation, automated provisioning), two simultaneous calls can result in: a user with no personal project, a personal project with no project relation, or a second personal project for the same user. Downstream code that assumes every user has exactly one personal project will fail or behave incorrectly, potentially denying the user access to their workflows and credentials.
Resolve the TypeORM concurrency issue (likely related to the SQLite pooled connection implementation) and wrap all three inserts in a single transaction. As an interim measure, add a unique partial index on `project.type = 'personal' AND project.creatorId IS NOT NULL` to prevent duplicate personal projects, and retry on constraint violation. Track the upstream TypeORM issue and remove the workaround once resolved.
20
— Data
Postgres connection pool default of 2 causes exhaustion under modest production load
The default `DB_POSTGRESDB_POOL_SIZE` is 2 connections, shared across all concurrent queries from a given n8n process.
Any burst of concurrency — webhook fanout, execution pruning, active-workflow polling, bulk API requests — exhausts the two-connection pool immediately. TypeORM queues waiting connections with a 20-second acquire timeout, causing execution timeouts and cascading errors under load. Self-hosted users who copy default configuration hit this limit at very modest workflow volumes. Queue-mode deployments with multiple worker processes each holding 2 connections also risk exhausting Postgres's `max_connections`.
Increase the default pool size to at least 10. Add a startup warning when `poolSize <= 2` and `DB_TYPE = postgresdb` outside of test environments. Document in the deployment guide that pool sizing should account for the number of n8n processes sharing the same Postgres server and Postgres's configured `max_connections`.
21
— Data
Variables stored in plaintext — sensitive values exposed on database read
The `variables.value` column is a `TEXT` column with no encryption; there is no `isSecret` or sensitivity flag to selectively protect variable values.
Variables are frequently used to store semi-sensitive configuration values such as environment identifiers, internal base URLs, and occasionally API credentials or tokens. Any direct database read — from a backup file, read replica, SQL injection, or misconfigured admin access — exposes all variable values in plaintext. Users relying on variables as a lightweight secrets store (in the absence of an external secrets provider) are particularly exposed.
Add an optional `isSecret` boolean column (default `false`) to Variables. When `isSecret = true`, encrypt the value before persistence using the same key as credentials. Surface a 'Secret variable' option in the UI with a warning that the value cannot be retrieved after saving (display-masked). Migration should leave existing variables unencrypted by default; users opt in.
22
— Data
No backup/restore tooling or runbooks — recovery depends entirely on operator initiative
The repository contains no backup scripts, restore procedures, disaster recovery runbooks, or any automation for database backup across SQLite and PostgreSQL deployments.
For a self-hosted product, the absence of any backup tooling or documentation means most operators have no verified recovery path. SQLite WAL mode protects against write-crash corruption but does not constitute backup. PostgreSQL users must configure pg_dump, WAL archiving, or managed backup independently. A single misconfigured delete, failed migration with no rollback, or disk failure results in irrecoverable data loss. Given that n8n stores workflows, credentials, and execution history, full data loss would be catastrophic for any production deployment.
Ship a `scripts/backup.sh` and `scripts/restore.sh` for both SQLite (via `.backup` command) and PostgreSQL (pg_dump with recommended options). Add a Docker Compose example with automated daily backups. Document recovery procedures for the most likely failure scenarios: failed migration, accidental deletion, and disk failure. Consider implementing pre-migration backup as a safety net in `base-command.ts` before calling `dbConnection.migrate()`.
23
— Security
OAuth browser binding disabled by default, allowing OAuth token hijacking
N8N_OAUTH_BROWSER_BINDING defaults to false in AuthConfig, meaning the OAuth2 callback is not bound to the browser that initiated the flow, leaving it vulnerable to the OAuth authorization code injection attack.
An attacker who can craft a link (e.g., via phishing or CSRF) can inject their own OAuth authorization URL into a victim admin's browser and steal the resulting access token into the attacker's credential store. This gives the attacker persistent access to the victim's connected third-party services (GitHub, Google, Slack, etc.).
Change the default value of `oauthBrowserBinding` to `true` in packages/@n8n/config/src/configs/auth.config.ts. This is already implemented (OAuthBrowserBindingService exists) — it just needs to be on by default. Provide a migration notice for deployments that may need to disable it temporarily for specific environments.
24
— Security
No SAST, SCA, or secret-detection in CI pipeline
The sec-ci-reusable.yml file explicitly has placeholder comments for 'dependency-scan' and 'secret-detection' that have not been implemented; the only security scanning in PR CI is Poutine (workflow/action security), leaving code vulnerabilities and accidentally committed secrets undetected.
Vulnerable dependency versions, injected malicious packages, and accidentally committed credentials (API keys, tokens) reach the main branch and production releases without automated detection. The lefthook pre-commit configuration contains no secret-scanning step either.
Add CodeQL or Semgrep for SAST, npm audit or Snyk/socket.dev for SCA, and gitleaks or truffleHog for secret detection. These can be added to the sec-ci-reusable.yml placeholder slots. For pre-commit protection, add a gitleaks hook to lefthook.yml targeting staged files.
25
— Security
Trivy container scan does not block releases on Critical/High findings
The Trivy scan workflow runs with exit-code 0, meaning the job succeeds even when CRITICAL or HIGH vulnerabilities are found; findings are sent to Slack only, and release workflows are not gated on scan results.
Container images with known exploitable vulnerabilities can be published to production without automated blocking. An operator relying solely on CI gates would be unaware that a release contains a critical unpatched CVE.
Change exit-code to '1' for CRITICAL severity in the Trivy action configuration, or add a separate step that fails the workflow when critical_count or high_count exceeds a threshold. Add the security scan as a required gate in the release workflow (release-publish.yml or docker-build-push.yml).
26
— Security
Development CORS middleware reflects any origin with credentials, risking misconfigured production exposure
corsMiddleware in packages/cli/src/middlewares/cors.ts echoes any Origin header and sets Access-Control-Allow-Credentials: true with no whitelist; it is conditionally registered only in development, but the condition relies on the NODE_ENV / inDevelopment flag which can be misconfigured.
If n8n is ever started in a non-production classification (e.g., staging, integration environment, or misconfigured NODE_ENV) that is network-accessible, any origin can make credentialed cross-site requests, trivially bypassing the cookie-based CSRF protection and hijacking authenticated sessions.
Replace the blanket reflective CORS with an explicit origin allowlist even in development mode (e.g., localhost on configurable ports). Remove Access-Control-Allow-Credentials from the dev middleware or scope it to the configured front-end URL. Apply the same treatment to the health-check CORS in AbstractServer.
27
— Security
Content Security Policy disabled by default for the frontend application
SecurityConfig sets contentSecurityPolicy to '{}' (empty object), causing server.ts to call helmet with contentSecurityPolicy: false, meaning no CSP header is sent for the main n8n UI by default.
Without a CSP, any XSS vulnerability in the frontend (e.g., in node parameter rendering, workflow name display, or third-party script injection) has full browser-side impact — credential theft, session hijacking, and data exfiltration — with no browser-level mitigation.
Define a restrictive default CSP in SecurityConfig (e.g., default-src 'self'; script-src 'self'; object-src 'none') and enable it by default. Allow operators to extend it via N8N_CONTENT_SECURITY_POLICY for their embed/iframe use cases. The existing CSP infrastructure in helmet is ready; it just needs a default value.
28
— AI Engineering
Spec-driven development infrastructure exists but no specs are in use
The n8n:spec-driven-development skill is documented and ready, but .claude/specs/ is empty — all planning occurs through Linear tickets with gitignored plan files that are lost after each session.
Agents and contributors cannot reference a written spec before implementing non-trivial features or API changes, increasing the risk of scope drift, re-decided architecture choices, and implementation that does not match reviewed intent. Planning decisions in /n8n:plan sessions disappear after the session ends and do not accumulate as institutional context.
Adopt spec files for new modules and API changes: (1) add a '.claude/specs/' subdirectory per feature with a short scope, API contract, and TODO checklist before implementation starts; (2) either stop gitignoring .claude/plans/ so plan files persist in the branch, or move plans into a docs/ or .claude/specs/ location that is committed; (3) update the developer agent and /n8n:plan command to write a starter spec file automatically when no spec exists for the feature.
29
— AI Engineering
Planning artifacts are session-scoped and do not compound as institutional knowledge
The /n8n:plan command saves plans to .claude/plans/ which is gitignored, so implementation rationale, open questions, and design decisions are lost when the Claude Code session ends or the branch is deleted.
Reviewers and future contributors cannot inspect the planning context that drove a PR's design. Repeated similar features must re-derive decisions from scratch. When an AI-assisted implementation diverges from original intent, there is no written record to audit.
Commit plan files alongside the feature branch: remove or narrow the .claude/plans/ gitignore entry to allow committing, or redirect /n8n:plan to write into a committed docs/design/ or .claude/specs/ path. Consider adding an 'Implementation plan' collapsible section to the PR body (the create-pr skill already supports this when the user opts in — make it the default for plans).
30
— Infrastructure
Worker health check endpoints disabled by default, blocking Kubernetes liveness probes
QUEUE_HEALTH_CHECK_ACTIVE defaults to false in ScalingModeConfig, meaning /healthz and /healthz/readiness are unavailable on workers unless explicitly configured, leaving Kubernetes unable to detect unresponsive workers.
Operators deploying n8n workers to Kubernetes cannot configure meaningful liveness or readiness probes without discovering and setting QUEUE_HEALTH_CHECK_ACTIVE=true through documentation. Without probes, Kubernetes will not restart crashed or deadlocked workers, and the service mesh will continue routing traffic to unhealthy pods. The worker health endpoint also binds to all interfaces (QUEUE_HEALTH_CHECK_PORT=5678, address='::' by default), which means that when enabled by an operator who hasn't read the full config, it may inadvertently bind to public-facing interfaces on bare-metal or VM deployments.
Change QUEUE_HEALTH_CHECK_ACTIVE to default true in packages/@n8n/config/src/configs/scaling-mode.config.ts. Change the default bind address from '::' to '127.0.0.1' (or a configurable loopback) so health checks are not unintentionally exposed. Update Kubernetes deployment documentation to include readinessProbe and livenessProbe examples using /healthz/readiness and /healthz respectively.
31
— Infrastructure
Redis queue defaults to unauthenticated plaintext connections in queue mode
QUEUE_BULL_REDIS_TLS defaults to false and QUEUE_BULL_REDIS_PASSWORD defaults to an empty string, meaning any queue-mode deployment using default configuration connects to Redis without authentication or encryption.
In a cloud VM deployment with an improperly scoped security group, or in any Docker network where Redis port 6379 is accessible beyond the local bridge, job payloads (which contain workflow execution data, potentially including credential values passed as inputs), queue metadata, and pub/sub messages are readable and injectable by any network-adjacent party. Redis without authentication is also trivially exploitable via CONFIG SET to write to the filesystem if the Redis server has write permissions.
Document Redis authentication and TLS as required for any non-local-only queue-mode deployment. Provide an example of setting QUEUE_BULL_REDIS_PASSWORD and QUEUE_BULL_REDIS_TLS=true in the deployment documentation and in a canonical production Compose example. Add a startup check: if EXECUTIONS_MODE=queue and Redis password is empty and TLS is off, emit a startup warning.
32
— Infrastructure
Benchmark Compose files ship hardcoded insecure credentials and disabled security controls
Compose files under packages/@n8n/benchmark/scripts/n8n-setups/ use password='password', task runner auth token='test', user: root:root for containers, and explicitly disable N8N_RESTRICT_FILE_ACCESS_TO and N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES; these are discoverable by operators searching the repo for deployment examples.
An operator who finds these Compose files and uses them as a starting point for a production deployment will inadvertently run n8n as root, with unauthenticated task runner connections, an easily guessed Postgres password, and disabled file-access controls. The postgres container also runs as root. These are intended for internal benchmark use but are not labeled as such in a way that discourages production copying. Copying them disables multiple security layers simultaneously.
Add a prominent warning comment at the top of each benchmark Compose file: 'FOR BENCHMARKING ONLY — NOT FOR PRODUCTION USE. Contains hardcoded credentials and disabled security controls.' Change the task runner auth token variable name to something clearly non-default (e.g. BENCHMARK_TASK_RUNNER_TOKEN) so it cannot be silently copied. Consider adding a separate docs/deployment/ directory with a production-safe Compose template that uses environment variable references without defaults.
33
— Observability
Log format defaults to unstructured text instead of JSON
N8N_LOG_FORMAT defaults to 'text', producing human-readable output that cannot be reliably parsed by log aggregation tools such as Datadog, Loki, Splunk, or Elastic.
Operators using centralized log management must manually configure JSON format or accept unparsed log strings. Field extraction, log-based metrics, and alert conditions on log content are not possible with the default text format. This gap is especially significant in container environments where stdout is the only log sink.
Change the default N8N_LOG_FORMAT to 'json'. Add a changelog entry and migration note for operators who rely on the current text format for human reading. Ensure the JSON format includes consistent required fields (timestamp, level, message, scope/component) documented so operators can write log parsers. Consider auto-detecting container environments (TTY absence) to default to JSON.
34
— Observability
No request correlation ID in HTTP access logs
No middleware injects a request ID or trace ID into HTTP access logs or response headers, making per-request debugging in production require fragile timestamp-based log correlation.
When users report an error with a timestamp, operators cannot reliably find the corresponding log entries in multi-instance or high-concurrency deployments. Support incidents take longer to diagnose. Without request IDs, distributed traces cannot be correlated with application logs even when OTel is enabled.
Add a middleware early in the Express pipeline that reads X-Request-ID from the incoming request (or generates a UUID), stores it in AsyncLocalStorage, injects it into all log messages for that request, and echoes it in the response via X-Request-ID header. Wire the request ID into OTel trace context so that logs and traces share the same identifier. This is a 20-30 line change using AsyncLocalStorage.
35
— Observability
Sentry 6-week release expiration silently disables error reporting
ErrorReporter.init() schedules a timer that silently replaces the Sentry captureException handler with a default console logger after the release is 6 weeks old, with only a stdout warning and no metric or alert emitted.
Self-hosted operators running a release older than 6 weeks lose error tracking without a visible indicator. A Prometheus alert or Sentry alert for 'suddenly zero errors' is ambiguous and easy to miss. Operators may believe their instance is error-free when error reporting has silently stopped, missing critical issues that would have been caught by Sentry.
When the expiration fires, emit a structured log at ERROR level (not just warn) with a distinct message that log-based monitors can detect. Additionally, increment a Prometheus gauge (e.g., n8n_error_reporter_active 0) so Prometheus alerts can fire when error reporting is disabled. Consider emitting one final Sentry event via the still-active SDK before disabling it, so a 'last heartbeat' is visible in the Sentry project.
36
— Observability
No LLM/AI token usage or cost metrics
AI and LangChain node executions are captured as EventMessageAiNode events but no Prometheus metrics exist for token consumption, model-specific error rates, or LLM latency, leaving AI-heavy workflows unmonitorable from an operations perspective.
Operators running AI workflows cannot monitor token consumption growth, unexpected cost spikes, LLM provider error rates, or per-model latency without custom instrumentation. A runaway AI workflow consuming millions of tokens will not trigger any metric-based alert. This gap will worsen as AI node usage grows.
Add Prometheus counters/histograms for AI node executions: n8n_ai_node_tokens_total{provider, model, type} (input/output token counts), n8n_ai_node_errors_total{provider, model}, n8n_ai_node_duration_seconds{provider, model} histogram. Wire these to the existing EventMessageAiNode event flow. Add token count attributes to OTel spans for AI node executions. Coordinate with the LangChain nodes package to extract token usage from LLM responses.
37
— Delivery
LLM evaluation jobs are non-blocking required checks for an AI-centric product
The Instance AI workflow eval and discovery eval run in CI but are explicitly excluded from required-checks, meaning PRs touching AI code can merge without passing LLM behavioral tests.
AI capability regressions — broken tool calls, degraded orchestration, prompt failures — can reach release branches undetected. The comment in ci-pull-requests.yml acknowledges this is temporary ('Non-blocking initially; promote to required after stability'), but the promotion has not happened.
Add instance-ai-discovery-evals to the required-checks job needs array. For the heavier workflow evals, set a pass threshold (e.g. ≥N cases must pass) and wire it into required-checks, or gate release merges on a nightly full-eval run result. Stabilize flaky cases in the pr tier first so the gate is reliable.
38
— Delivery
Poutine supply-chain scan only runs on workflow file changes, not on all PRs
The sec-ci-reusable.yml Poutine scan is only triggered when the ci-filter detects changes to .github/**, leaving regular code PRs unscanned for supply-chain issues in workflow configs.
A PR that modifies a workflow indirectly (e.g. by adding a composite action component or updating a script called from a workflow) without touching .github/ directly will bypass the Poutine supply-chain security gate. An attacker or inadvertent contributor could introduce a workflow injection vector that passes all other checks.
Change the security-checks job condition to run unconditionally (remove the `needs.install-and-build.outputs.workflows == 'true'` guard) or expand the workflows path filter to cover .github/actions/** and .github/scripts/** in addition to .github/workflows/**. Poutine runs quickly and the incremental cost is low.
39
— Documentation
AGENTS.md lists stale package filesystem paths for frontend packages
The Package Structure section of AGENTS.md lists `packages/editor-ui` and `@n8n/design-system` as paths, but the actual filesystem locations are `packages/frontend/editor-ui` and `packages/frontend/@n8n/design-system`.
AI agents using AGENTS.md as the canonical context file will search for non-existent paths when navigating to frontend code, producing incorrect file references in suggestions and edits. The discrepancy between AGENTS.md and CONTRIBUTING.md (which uses the correct paths) reduces confidence in the context file's accuracy.
Update the Package Structure section of AGENTS.md to use the correct filesystem paths: change `packages/editor-ui` to `packages/frontend/editor-ui` and `@n8n/design-system` to `packages/frontend/@n8n/design-system`. Audit remaining path references in the same section for similar issues.
40
— Documentation
No consolidated environment variable reference for the full configuration surface
The root `.env.local.example` explicitly acknowledges that 'Many more variables exist — search for @Env() decorators in the codebase,' with no consolidated reference and 50+ config files spread across packages/@n8n/config/src/configs/.
New engineers configuring a development or production environment cannot discover the full set of available options without manually grepping the codebase. AI agents generating configuration guidance will produce incomplete answers. Operators may miss security-relevant variables (SSRF protection, auth settings) that have defaults but should be explicitly reviewed.
Create a `docs/configuration.md` reference that lists all major configuration categories with links to the corresponding config files in packages/@n8n/config/src/configs/ and points to the official docs.n8n.io/hosting/configuration page. Alternatively, expand .env.local.example into a comprehensive template with all production-relevant variables grouped by category.
41
— Documentation
No in-repo privacy disclosure for built-in diagnostics and telemetry
n8n ships with Posthog analytics and an opt-out diagnostics system (N8N_DIAGNOSTICS_ENABLED) enabled by default, but the README and in-repo docs contain no disclosure of what data is collected or how to opt out.
Self-hosted operators may unknowingly send instance metadata and workflow telemetry without understanding the data collection scope or knowing the opt-out mechanism. This creates a trust gap and may create compliance concerns for operators in regulated industries who deploy n8n without reading the external privacy policy.
Add a brief 'Data collection' section to the README that explains what diagnostics n8n collects by default, references N8N_DIAGNOSTICS_ENABLED=false as the opt-out mechanism, and links to the full privacy policy at n8n.io. Alternatively, document this in docs/configuration.md.
42
— Code Quality
Dependency version fragmentation tracked but not yet resolved
The .code-health-baseline.json records 148 catalog violations: change-case in 5 packages across 3 versions, jsdom in 4 packages across 2 versions, tmp-promise in 4 packages across 2 versions, and similar fragmentation for 15+ other dependencies.
Multiple versions of the same package inflate the install footprint, can produce subtle behavioural differences between packages that use different versions, and increase the surface area for supply-chain compromises. The stale-overrides violations on the root package.json compound this by partially duplicating catalog entries.
Work through the catalog violations in .code-health-baseline.json systematically: pin each flagged dependency to catalog: in pnpm-workspace.yaml, remove the stale overrides from the root package.json, then zero out the baseline entry. The baseline mechanism already enforces no new violations, so this is a cleanup-only effort.
43
— Code Quality
ts-jest isolatedModules disables cross-file type-checking in tests
The root jest.config.js sets isolatedModules: true in tsJestOptions.tsconfig to speed up test runs, which prevents ts-jest from catching cross-module type errors that the standalone pnpm typecheck step would catch.
Tests can pass while containing type errors that would fail the separate typecheck CI step, creating a false sense of type safety during local test runs. Developers who skip pnpm typecheck before committing are unprotected.
This trade-off is well-documented in the config and acceptable given that typecheck is enforced in CI. No immediate action required, but document explicitly in AGENTS.md that local `pnpm test` does not substitute for `pnpm typecheck`.
44
— Architecture
ScalingService couples core queue infrastructure to a node implementation package
ScalingService.setupQueue() imports McpServer, QueuedExecutionStrategy, and RedisSessionStore from @n8n/n8n-nodes-langchain/mcp/core, creating a hard dependency between execution infrastructure and a specific node-type package.
Any change to the MCP node package's public API can silently break the queue setup path. The dependency direction violates the clean architecture principle where infrastructure packages should not depend on feature packages. It also prevents deploying a queue-mode worker without including the langchain node package, increasing worker Docker image size and attack surface unnecessarily.
Introduce an MCP-specific infrastructure service (e.g., McpSessionStoreSetup) in the CLI package that owns session store initialization and is conditionally registered. Inject it into ScalingService via an interface or leave the wiring in a dedicated MCP module initializer. Keep ScalingService.setupQueue() free of node-type imports.
45
— Architecture
Inter-process job message protocol versioning is managed ad-hoc
The job-finished Bull progress message has an inline version===2 check in the main listener; backward compatibility between mains and workers is managed through scattered conditionals rather than a formal compatibility contract.
During a rolling deploy where mains and workers run different versions simultaneously, mismatched message formats can cause silently dropped or misrouted completion events. If a worker sends a version 2 message to an older main (or vice versa), the result is not stored in jobResults and the execution may get fetched from the DB unnecessarily or, in edge cases, finalization may be skipped.
Define an explicit versioned message schema for the job completion protocol (e.g., using Zod). Add a parse-and-validate step at the message receiver boundary, and document the minimum compatible worker/main version combination. Consider a compatibility matrix in the deployment docs.
46
— Data
Execution data stored unencrypted — workflow outputs may contain sensitive information
`ExecutionData.data` is a `TEXT` column storing the full serialized execution run data including all node inputs and outputs in plaintext.
Workflow executions frequently process sensitive data: OAuth token responses, PII from CRM or marketing integrations, API responses containing secrets, and database query results. This data is stored in plaintext and appears in database backups, read replicas, and any direct DB access. For compliance-sensitive deployments (GDPR, HIPAA, SOC 2), unencrypted execution data is a significant liability.
Document this limitation clearly in the security documentation. Add an opt-in execution data encryption setting (`N8N_ENCRYPT_EXECUTION_DATA`) that encrypts `ExecutionData.data` using the instance key before storage. Provide a data sanitization/redaction option for known sensitive field patterns. At minimum, ensure the existing redaction service (`execution-redaction.service.ts`) is applied before persistence for sensitive node types.
47
— Data
Credentials list eagerly loads three JOIN levels by default — amplifies query cost
The default `defaultRelations` in `CredentialsRepository.toFindManyOptions` always fetches `shared → shared.project → shared.project.projectRelations` for every credential list call.
On instances with many credentials and large team projects, each credential list request triggers a multi-level JOIN that grows with the number of project members. This can result in excessive memory allocation in the Node.js process for large result sets, and puts unnecessary load on the database. The pattern also prevents TypeORM from generating an efficient single query, often resulting in N+1 sub-queries for deeply nested relations.
Load `projectRelations` lazily (only when needed for permission checks) rather than defaulting all list calls to include them. Provide a separate method or option that includes `projectRelations` only for ownership-resolution paths. Consider caching project membership lookups at the service layer.
48
— Data
Variables entity lacks unique constraint on key per project — duplicate variable keys possible
`Variables.key` is an unindexed `TEXT` column with no uniqueness constraint at the DB level, neither globally nor per-project.
Concurrent or sequential inserts can create multiple variables with the same key within the same project scope. Application code reading variables by key will receive an arbitrary row, leading to non-deterministic behavior in workflow execution. The absence of a DB-level unique index means the constraint can only be enforced in application code, which is not atomic under concurrent writes.
Add a unique partial index on `(projectId, key)` WHERE `projectId IS NOT NULL`, and a separate unique partial index on `(key)` WHERE `projectId IS NULL` for global variables. This ensures uniqueness at the DB level and makes the enforcement atomic.
49
— Security
No pre-commit secret-scanning hook to prevent credential commits
lefthook.yml configures pre-commit hooks for formatting, styles, actionlint, workspace deps, and migration timestamps, but contains no secret-scanning step; credentials accidentally staged for commit are not caught before they enter git history.
API keys, database passwords, or JWT secrets accidentally pasted into configuration files or test fixtures will be committed and pushed to the public repository, requiring rotation even if removed in a later commit (git history retains them permanently).
Add a gitleaks or truffleHog pre-commit hook to lefthook.yml targeting staged files. Example: run: gitleaks protect --staged --redact with the glob: '*'. This is cheap to run and catches the most damaging class of supply-chain secrets exposure.
50
— Security
SOC2 control documentation, incident response, and vendor risk management absent from repository
No security policy documents, incident response playbooks, vendor security review evidence, or SOC2 control mappings are present in the repository; change management relies entirely on CODEOWNERS and PR process without documented control objectives.
For operators seeking SOC2 Type II certification or enterprise compliance audits, the lack of documented controls, incident procedures, and evidence capture creates a significant audit gap. Security incidents also lack a defined response playbook, potentially extending mean-time-to-containment.
Create a security/ or docs/security/ directory containing: (1) incident response playbook with escalation paths, (2) vulnerability disclosure and patching SLA, (3) access control and least-privilege policy, (4) data classification and retention policy, (5) vendor/third-party security review checklist. Even a lean version substantially improves SOC2 readiness.
51
— AI Engineering
Skill telemetry hook sends developer fingerprints without explicit opt-out documentation
track-skill-usage.mjs runs as a PostToolUse hook on every n8n-namespaced skill invocation and sends a SHA-256 fingerprint of (username+hostname+platform+arch+release) to telemetry.n8n.io silently in the background.
Developers who have not read the telemetry script may not be aware their machine identity is being hashed and transmitted. On contractor machines, air-gapped environments, or strict data-residency setups, silent telemetry from a developer toolchain hook is unexpected and may violate policy.
Add a one-time disclosure in .claude/README.md or the onboarding docs stating that skill usage (skill name + anonymized device fingerprint) is sent to n8n telemetry. Optionally add an N8N_CLAUDE_TELEMETRY_DISABLED env-var check in track-skill-usage.mjs that allows engineers to opt out without modifying the shared settings file.
52
— AI Engineering
No knowledge lifecycle for documenting and compounding AI workflow learnings
The team has no retrospective practice, decision record format, or mechanism for recording when AI approaches work well or poorly — learnings from completed AI-assisted work are not fed back into AGENTS.md or skills.
Effective prompting patterns, recurring misunderstandings, and known AI failure modes in this codebase are held in individual heads rather than in the context files agents and new contributors will read. Over time, the gap between documented guidance and actual effective practice widens.
Introduce a lightweight knowledge loop: (1) add a brief 'AI notes' section to the PR template for capturing surprising AI behaviors, agent misunderstandings, or prompting techniques that worked; (2) schedule a periodic review (quarterly or per release) of AGENTS.md to incorporate recurring corrections; (3) consider adding a .claude/retrospectives/ directory (committed) for freeform session notes.
53
— AI Engineering
AI delivery documentation lacks structured fields for AI-specific risks and limitations
The PR template and create-pr skill produce well-structured PRs with human accountability checkboxes and AI authorship labels, but no section captures AI-specific concerns such as confidence level, known edge-case gaps, or whether generated code was validated manually.
Reviewers have no signal about which parts of a PR were AI-generated versus hand-written, whether the AI's assumptions were validated, or what known limitations the author is aware of. This makes it harder to calibrate review depth for AI-assisted PRs.
Add an optional 'AI assistance notes' section to .github/pull_request_template.md covering: (1) which parts were AI-generated, (2) how the output was validated (ran tests, reviewed manually, etc.), (3) any known limitations or assumptions not yet verified. The create-pr skill can prompt the author to fill this in when AI involvement is detected.
54
— Infrastructure
Custom DHI base image (dhi.io/node) used in n8n-base is not publicly auditable
docker/images/n8n-base/Dockerfile builds FROM dhi.io/node:24.16.0-alpine3.22-dev, a proprietary internal image distribution; this base is not available on Docker Hub and cannot be independently inspected by operators or security researchers.
Third parties auditing the n8n Docker image supply chain cannot verify the contents of the DHI base layer independently. While the final published n8n image is scanned with Trivy and attested with SLSA L3 provenance, the DHI base layer is a trust boundary that requires operators to trust DHI without a public audit path. This complicates enterprise procurement security reviews and SOC2 vendor assessments.
Document what DHI is (internal n8n image distribution) and why it is used instead of the public node:alpine image in the n8n-base Dockerfile or a linked document. Provide a verification path: the SBOM and Trivy scan results cover the final image layers and should include any DHI-sourced packages. If operators need a fully auditable build path, document how to rebuild n8n-base from the public node:24.16.0-alpine3.22 image.
55
— Observability
No SLO/SLA definitions or measurement infrastructure
No SLO targets, uptime commitments, error budgets, or measurement sources are defined in the repository; Prometheus metrics infrastructure exists to measure SLIs but nothing is wired up.
Without defined SLIs/SLOs, there is no objective basis for evaluating whether the service meets reliability expectations. Customer-facing commitments (if any exist for n8n Cloud) cannot be verified programmatically and have no alerting backstop. This makes proactive reliability engineering impossible.
Define minimal SLIs using existing Prometheus metrics as measurement sources: availability (up == 1 ratio), success rate (workflow executions succeeded / total), and P95 request latency. Add Prometheus recording rules to precompute these. Document targets (e.g., 99.5% availability, <1% execution failure rate) in docs/operations/slo.md. For n8n Cloud, ensure a status page references these targets.
56
— Observability
PostHog analytics API key hardcoded in default configuration
DiagnosticsConfig has a hardcoded PostHog project API key and telemetry endpoint as default values, meaning all self-hosted instances send product analytics by default unless explicitly disabled.
Self-hosted operators who have not read the privacy documentation will unknowingly send product telemetry to n8n's PostHog instance. While N8N_DIAGNOSTICS_ENABLED=false can disable this, it requires explicit operator action and the hardcoded key is visible in the open-source repository, which may raise compliance concerns in regulated environments.
Move the PostHog key and telemetry endpoint to environment variables with empty-string defaults. Require operators to explicitly opt-in to diagnostics by setting N8N_DIAGNOSTICS_ENABLED=true and providing their own or n8n's PostHog key. Add a clear notice in the startup logs when diagnostics are enabled, stating what data is collected and where it is sent. This aligns with privacy-by-default principles.
57
— Delivery
No operator-facing rollback runbook for self-hosted deployments
n8n ships as Docker images with immutable SHA tags and stable/beta/v1 track tags, but there is no documented rollback procedure for operators in the repo (e.g. how to pin to a previous image, how to handle migration rollback).
Operators who upgrade to a broken release may not know they can roll back by pinning the previous SHA-tagged image. If a migration has run, the absence of guidance on forward-fix vs schema rollback increases recovery time during incidents.
Add a ROLLBACK.md or section in the existing docs describing: (1) how to identify the previous SHA-tagged image for each release, (2) the safe window before which rolling back is migration-safe, (3) the forward-fix path when a migration has already run, and (4) how to use the stable/beta track pointers for controlled upgrades. Link from the main README and CONTRIBUTING.md.
58
— Delivery
Feature flag system lacks in-repo lifecycle management and cleanup discipline
PostHog is the declared feature flag system (AGENTS.md), but there is no in-repo flag registry, no naming convention enforcement, no documented removal timelines, and no CI check for stale flags.
Flags accumulate without cleanup, making it harder to reason about which code paths are active. Dead flags guarded by stale PostHog checks become invisible technical debt, and operators relying on N8N_EXPERIMENT_OVERRIDES have no authoritative list of valid flag names.
Create a feature-flags.md or add a flags section to AGENTS.md that lists active flags, their purpose, and expected removal milestone. Add a lint rule or CI check (e.g. a simple script) that fails if a flag name referenced in code has no corresponding entry in the registry. Establish a convention (e.g. flags older than two minor releases with no removal PR are flagged in code review).
59
— Documentation
No architecture decision records for major system choices
No ADR directory exists; decisions such as the SQLite/PostgreSQL dual-database strategy, fair-code license model, DI framework choice, and Bull/Redis queue architecture are not documented with rationale or tradeoff notes.
Contributors evaluating architectural changes cannot understand why existing approaches were chosen, making it harder to assess whether a proposed change challenges a deliberate decision or corrects an oversight. The agent-runtime-architecture.md pattern of inline design decisions is good but not systematically applied.
Create a `docs/decisions/` directory and write ADRs for the highest-impact architectural choices. Start with: database strategy (SQLite default + PostgreSQL for production), queue architecture (Bull/Redis), the fair-code license, and the DI framework. The agent-runtime-architecture.md design-decisions section is a good template to follow.
60
— Documentation
README is user/marketing-focused with no contributor orientation
The root README.md contains product marketing copy, a quick-start via npx, and resource links, but no guidance on how to contribute, understand, or modify the codebase — contributors must discover CONTRIBUTING.md and AGENTS.md independently.
First-time contributors who land on the README have no immediate path to the contributor documentation. AI tools that consume the README as primary context will miss the architectural and development patterns documented in CONTRIBUTING.md and AGENTS.md.
Add a 'Contributing / Development' section to README.md with direct links to CONTRIBUTING.md (setup), AGENTS.md (AI-agent context), docs.n8n.io (full docs), and a one-line summary of the monorepo structure. This requires minimal changes and significantly improves discoverability.
You've seen the problems.
Let's build a fix plan together.