langgenius/dify
github.com/langgenius/dify ↗ · 4000 files · 27.1 MB · scan capped
Dify Cloud · Self-hosting · Documentation · Dify edition overview Dify is an open-source LLM app development platform.
Overview
Dify is an open-source platform for building, hosting, and operating LLM applications — chatbots, RAG pipelines, agentic workflows, and MCP services — exposed through four HTTP surfaces (console admin, end-user webapp, service API, and MCP/trigger/webhook ingress). The monorepo contains a Flask/SQLAlchemy/Celery Python backend (`api/`, served via gevent gunicorn at :5001), a Next.js standalone console (`web/`), a generated-contract pnpm workspace (`packages/contracts` feeds an oRPC `consoleClient`), a Go job-sandbox runtime (`dify-agent-runtime/`), a Python agent backend (`dify-agent/`), and a Node/TS CLI (`cli/`, difyctl). Everything is deployed via generated Docker Compose with Postgres+Redis plus a pluggable fleet of ~28 vector stores, object stores, an external plugin daemon, and Squid SSRF proxies. The codebase is heavily productionized: license gating, quota/rate limiting, SSRF protection, human-in-the-loop pause/resume, and edition-aware (community/cloud/enterprise) feature flags are woven into the request path.
System map
The system is a modular monolith API (Flask + flask-restx + SQLAlchemy + Celery) plus a Next.js console, with execution-heavy work pushed to Celery workers and sidecar services (plugin daemon, sandbox, agent runtime, SSRF proxy). `api/app.py` is the process entrypoint; `app_factory.create_app()` builds a `DifyApp`, runs an ordered list of ~30 `extensions/ext_*` initializers, and wraps the result in `socketio.WSGIApp`. HTTP ingress is split across eight Flask blueprints registered in `ext_blueprints.py`, each with its own CORS/auth surface. State lives in Postgres (Alembic, 219 migration files), Redis (broker, cache, rate limits, pub/sub for SSE), pluggable object storage, and pluggable vector DBs loaded via Python entry points.
- `api/app.py` is the API process entrypoint: when run as `python -m app` it gevent-monkey-patches stdlib, grpc, and psycopg2 BEFORE other imports, then serves the socketio-wrapped app on 0.0.0.0:5001 via `gevent.pywsgi.WSGIServer` with `WebSocketHandler`.
- `app_factory.create_app()` returns a `(socketio.WSGIApp, DifyApp)` pair; `initialize_extensions()` iterates a fixed ordered list of ~30 `extensions/ext_*` modules (timezone → logging → database → redis → storage → key_provider → celery → login → blueprints → otel …) calling `init_app(app)` on each, skipping any ext exposing `is_enabled() == False`.
- All HTTP surfaces are Flask blueprints registered in `ext_blueprints.init_app`: `/v1` service_api, `/api` web (split CORS: unauthenticated embed `/chat-messages*` vs authenticated default), `/console/api`, `/files`, `/inner/api`, `/mcp`, `/triggers`, and an `/openapi` blueprint gated behind `dify_config.OPENAPI_ENABLED`.
- `flask db` commands get a deliberately reduced app: `is_db_command()` detects the argv shape and `create_migrations_app()` initializes only `ext_database`, `ext_migrate`, and `ext_commands` — Alembic over `api/migrations` (219 version files).
- The Celery worker entrypoint `celery_entrypoint.py` applies the same gevent/grpc/psycogreen patching then imports `app, celery`; `ext_celery.init_app` creates a Celery app with a `FlaskTask` base class that wraps every task in `app.app_context()` plus request logging context, and stores it in `app.extensions["celery"]`.
- Configuration is a single composed `DifyConfig` pydantic-settings class built from mixins (PackagingInfo, DeploymentConfig, FeatureConfig, MiddlewareConfig, ExtraServiceConfig, ObservabilityConfig, RemoteSettingsSourceConfig, EnterpriseFeature/TelemetryConfig) with source precedence init > env > remote-settings > .env > file secrets > pyproject TOML.
- Remote configuration is an extension point: `RemoteSettingsSourceFactory` reads `REMOTE_SETTINGS_SOURCE_NAME` and delegates to `ApolloSettingsSource` or `NacosSettingsSource` to hydrate every model field.
- The Flask app object is a `DifyApp` subclass carrying typed extension attributes (e.g. `login_manager: DifyLoginManager`); config is pushed into `app.config` from `dify_config.model_dump()` and `RESTX_ERROR_404_HELP` is disabled so flask-restx 404s don't enumerate routes to anonymous callers.
- Object storage is abstracted by `extensions/ext_storage.py`: `Storage.get_storage_factory` matches `STORAGE_TYPE` to per-provider runners (S3, OpenDAL, local fs, Azure Blob, Aliyun OSS, GCS, Tencent COS, Oracle OCI, Huawei OBS, Baidu OBS, Supabase, TOS, …) implemented under `api/extensions/storage/`.
- Vector DBs are hollow workspace packages under `api/providers/vdb/vdb-*` that register `dify.vector_backends` Python entry points (e.g. vdb-pgvector → `PGVectorFactory`); ~28 backends exist and `api/pyproject.toml` defines per-backend uv dependency groups (`vdb-all`, `vdb-qdrant`, …) plus a parallel `providers/trace/*` workspace for tracing backends.
- The graph execution engine is an external pinned dependency `graphon==0.7.0` (not in-repo): `WorkflowEntry` composes `graphon.Graph`, `GraphEngine`, `VariablePool`, `GraphRuntimeState`, command channels, and engine layers; Dify-owned nodes under `core/workflow/nodes/` cover agent, agent_v2, datasource, human_input, knowledge_index/retrieval, and trigger_* types.
- Deployment is a generated `docker/docker-compose.yaml` (regenerated by `docker/generate_docker_compose` from `docker-compose-template.yaml` + envs) running api, worker, worker_beat, scheduler, web, postgres, mysql, redis, `dify-sandbox:0.2.15`, `dify-plugin-daemon:0.6.10-local`, `dify-agent-backend`, `dify-agent-local-sandbox`, two Squid SSRF proxies, certbot and nginx.
Feature catalog
User-facing capability is organized around typed `AppMode`s, each with its own generator under `core/app/apps/*` and matching controller surface. The console blueprint exposes the full authoring/admin surface (apps, datasets/knowledge, plugins, triggers, MCP servers, agent apps, billing/features, auth). The `/api` web blueprint serves published end-user apps; `/v1` service_api serves token-authed programmatic access; `/mcp`, `/triggers`, and `/inner/api` handle protocol ingress. Async work (indexing, mail, cleanup, triggers, telemetry) is a large Celery task fleet under `api/tasks/` and `api/schedule/` with a feature-flagged beat schedule.
- `AppMode` enumerates the product's app types: `completion`, `workflow`, `chat`, `advanced-chat`, `agent-chat` (legacy ReAct), `agent` (new Agent app bound 1:1 to a roster Agent via `Agent.app_id`), `channel`, and `rag-pipeline`.
- Console app authoring APIs live under `controllers/console/app/`: app CRUD/import/export (`app.py`, `app_import.py`), workflow editing/runs/draft variables (`workflow.py`, `workflow_run.py`, `workflow_draft_variable.py`), messages/conversations, annotations, site publish settings, model config, ops tracing, statistics, MCP server exposure (`mcp_server.py`), and agent-app feature/access/sandbox controllers.
- Published-app runtime APIs live under `controllers/web/`: `/chat-messages` (+ `/stop`), `/completion-messages` (+ `/stop`), conversations, messages, saved messages, audio (TTS/STT), file upload, passport issuance, site info, forgot-password, human-input forms, and workflow event streams — all behind `WebApiResource`'s JWT passport decorator.
- The service API (`/v1`, `controllers/service_api/`) exposes app execution, dataset management, end-user, and workspace resources authenticated by `ApiToken` bearer tokens with token-usage recording and single-flight token caching (`fetch_token_with_single_flight`).
- Knowledge/dataset features are under `controllers/console/datasets/`: datasets, documents, segments, hit-testing, metadata, website crawl, external knowledge bases, and a `rag_pipeline` subpackage; indexing runs through Celery tasks like `document_indexing_task` and `batch_create_segment_to_index_task`.
- Plugin lifecycle is a first-class feature: `services/plugin/` provides endpoint management, OAuth, auto-upgrade (`check_upgradable_plugin_task` beat), permission grants, dependency analysis, and data migration; marketplace integration is configured via `MARKETPLACE_ENABLED`/`MARKETPLACE_API_URL`.
- Trigger ingress exists at two layers: `controllers/trigger/` (webhook + schedule triggers) enqueue async work via `tasks/async_workflow_tasks.py`/`tasks/trigger_processing_tasks.py`, and workflow graphs can contain `trigger_plugin`, `trigger_schedule`, `trigger_webhook` node types gated by `is_trigger_node_type` — service/manual invokes of trigger-containing workflows are rejected by `TriggerWorkflowServiceModeUnavailableError`.
- Apps can be exposed as MCP servers: `controllers/mcp/mcp.py` is a dedicated blueprint surface that is license-gated like `/v1` and gets `Forbidden(license_required)` on invalid enterprise licenses.
- The Agent v2 surface spans repos: `dify-agent` (Python package, editable dep of api) provides runtime/protocol/adapters; `dify-agent-runtime` (Go) provides shellctl sandboxed job execution; console controllers `agent_app_access`/`agent_app_feature`/`agent_app_sandbox` plus `services/agent` manage roster agents and Composer bindings.
- Tooling subsystem (`core/tools/`) supports builtin tools, custom (OpenAPI) tools, plugin tools, MCP tools, and workflow-as-tool wrapping, plus tool file manager and label manager.
- Content moderation is pluggable with three backends — `keywords`, `openai_moderation`, `api` — selected through `core/moderation/factory.py` for input and output moderation.
- Console auth covers password login, OAuth providers, email registration, forgot/reset password, activation invites, and data-source OAuth/bearer flows under `controllers/console/auth/`; sessions use flask-login via `ext_login`/`libs/login.py`.
Data flow
Every generate request follows the same spine: blueprint → flask-restx Resource → auth decorator (JWT passport, session, API token, or inner-api key) → Pydantic payload validation → `services/` orchestration → `core/app/apps/*` generator → `graphon` GraphEngine events → Redis pub/sub → SSE stream to the caller. Writes go through SQLAlchemy sessions (Postgres) with explicit tenant scoping; large payloads and files go through `ext_storage` backends; model/tool/plugin invocations egress through the plugin daemon over HTTP; arbitrary outbound HTTP from nodes/tools is forced through the Squid SSRF proxy. Config flows from pydantic-settings (env/.env/TOML/remote) into `app.config` at boot.
- Web-app requests authenticate via `WebApiResource.method_decorators = [validate_jwt_token]` → `decode_jwt_token()` verifies a `PassportService` JWT carrying `app_code`/`app_id`/`end_user_id`, then loads `App`, `Site`, and `EndUser` rows and rejects disabled sites or missing entities.
- `ChatApi.post` (POST `/api/chat-messages`) gates on app mode (`CHAT|AGENT_CHAT|ADVANCED_CHAT|AGENT`), validates `ChatMessagePayload` (inputs/query/files/response_mode/conversation_id/parent_message_id), eagerly resolves the conversation, then calls `AppGenerateService.generate` and streams via `helper.compact_generate_response`.
- `AppGenerateService.generate` wraps dispatch in `_run_with_guardrails`: on CLOUD edition it reserves `QuotaService.reserve(QuotaType.WORKFLOW)` with commit/refund semantics, then `RateLimit(app_model.id, max_active_requests)` bounds concurrent executions where the cap is `min(app.max_active_requests|APP_DEFAULT_ACTIVE_REQUESTS, APP_MAX_ACTIVE_REQUESTS)`.
- `_dispatch_generate` is a `match` on effective `AppMode` routing to `CompletionAppGenerator`, `AgentChatAppGenerator`, `AgentAppGenerator`, `ChatAppGenerator`, `AdvancedChatAppGenerator`, or `WorkflowAppGenerator` — each returning an event stream wrapped by `rate_limit.generate`.
- Streaming workflow/advanced-chat runs cross a process boundary: the request path serializes `AppExecutionParams` (discriminated `Account`/`EndUser` union, app_id, workflow_id, tenant_id, args, call_depth, workflow_run_id) to JSON and enqueues `workflow_based_app_execution_task.delay(payload_json)` on the `workflow_based_app_execution` queue only when the first SSE subscriber attaches (`on_subscribe` + 200ms fallback timer; streams channel type skips the timer).
- Inside the worker, `workflow_execute_task` rehydrates trusted state — reloads `App`, `Workflow`, and the user from Postgres via fresh sessions (never trusting the payload for identity beyond ids) — then drives `WorkflowAppGenerator`/`AdvancedChatAppGenerator` → `WorkflowEntry` → `graphon.GraphEngine.run()`, persisting `WorkflowRun`/`WorkflowNodeExecution` via `DifyCoreRepositoryFactory`/`DifyAPIRepositoryFactory`.
- Graph events are filtered before reaching the client: `iter_dify_graph_engine_events` pipes `engine.run()` through `filter_graph_events` with `HumanInputFormEventFilter` (backed by `HumanInputFormSubmissionRepository`) and `ResponseStreamFilter` which is deliberately shared with the pause-persistence layer so resumed runs keep consistent stream state.
- Streaming responses are plain SSE: `compact_generate_response` returns a `Response` with `mimetype="text/event-stream"` wrapping `_stream_with_request_context(generate())` — blocking mode returns a JSON `Mapping` response instead.
- A separate `length_prefixed_response` binary framing (1-byte magic + header/data length fields, little-endian) exists specifically for plugin-daemon compatibility, avoiding line-based streaming memory issues.
- Model/tool invocations egress to the external plugin daemon via `BasePluginClient._request_with_plugin_daemon_response` (e.g. `PluginAgentClient.fetch_agent_strategy_providers` GETs `plugin/{tenant_id}/management/agent_strategies`); streamed blobs are reassembled by `merge_blob_chunks`.
- Untrusted outbound HTTP (HTTP Request nodes, provider integrations, auth discovery, custom tools) is forced through `core/helper/ssrf_proxy.py`: pooled httpx clients mounted per-scheme on `SSRF_PROXY_HTTP(S)_URL` (Squid), with retry-on-[429,5xx] backoff and response-size guards (`ResponseTooLargeError`, `UnsupportedResponseEncodingError`).
- Enterprise license enforcement is a `before_request` concern: for `DEPLOYMENT_EDITION == ENTERPRISE`, `_match_license_gated_surface` gates `/console/api`, `/api`, `/v1`, `/mcp`, `/triggers` with surface-specific errors (session surfaces force logout 401, bearer surfaces 403, webhook surfaces 503 so senders retry); `/files`, `/inner/api`, `/health` are deliberately ungated, and bootstrap endpoints (system-features, setup, init, login, features, profile, workspaces/current, version, activate/check) are exempted to avoid signin reload loops.
Behavior traces
Three flows traced end-to-end: (1) API server startup, (2) end-user streaming chat message across the request→Celery→SSE boundary, and (3) the `flask db` migration path plus Celery worker boot. The dominant pattern is 'authenticate at the edge, rehydrate trusted state inside the worker, stream events back through Redis'.
- STARTUP (gunicorn/prod): `api/docker/entrypoint.sh` → gunicorn gevent workers (`gunicorn.conf.py`) → import `app.py` → `app_factory.create_app()` → `create_flask_app_with_configs()` builds `DifyApp` + pushes `dify_config.model_dump()` into `app.config` + registers `before_request` (request-context init, enterprise license gate) and `after_request` (OTel trace headers) → `initialize_extensions()` runs the ordered ext list → `socketio.WSGIApp(sio, app)` → served on :5001.
- STARTUP (bare `python -m app`): monkey.patch_all() → grpc_gevent.init_gevent() → psycogreen.patch_psycopg() → create_app() → `log_startup_banner` → `pywsgi.WSGIServer((0.0.0.0,5001), socketio_app, WebSocketHandler).serve_forever()`.
- MIGRATION PATH: `flask db upgrade` → `is_db_command()` detects `sys.argv` → `create_migrations_app()` → `create_flask_app_with_configs()` → only `ext_database`, `ext_migrate`, `ext_commands` initialized → Flask-Migrate/Alembic runs `api/migrations/env.py` against the 219-version history.
- WORKER BOOT: celery invokes `celery_entrypoint.py` → gevent/grpc/psycopg patch → `from app import app, celery` (full create_app ran) → `ext_celery.init_app` built the Celery app earlier with `FlaskTask` (app_context + `init_request_context` per task) → `setup_workflow_warm_shutdown_handler()` → beat schedule assembled conditionally from ~20 `ENABLE_*`/edition flags (cleanup crons, tidb serverless, human-input timeouts, plugin upgrade checks, telemetry heartbeats).
- CHAT MESSAGE (request side): POST `/api/chat-messages` → `validate_jwt_token` → `decode_jwt_token` → `extract_webapp_passport` header → `PassportService().verify` → DB loads `App`/`Site`/`EndUser` → webapp-auth mode checks (`_validate_webapp_token`, `_validate_user_accessibility` incl. SSO `granted_at` staleness) → `ChatApi.post` → mode gate → payload validate → eager `ConversationService.get_conversation` → `AppGenerateService.generate`.
- CHAT MESSAGE (guardrails+dispatch): `generate` → `_run_with_guardrails` → `QuotaService.reserve` (CLOUD only) → `RateLimit.enter(request_id)` → `_dispatch_generate` → for `ADVANCED_CHAT`/`WORKFLOW` streaming: `_get_workflow` (published vs draft by invoke_from) → `_ensure_workflow_service_mode_available` (reject trigger nodes for manual invokes) → `rate_limit_context` → `AppExecutionParams.new(...).model_dump_json()` → `_build_streaming_task_on_subscribe` wraps `workflow_based_app_execution_task.delay` → `retrieve_events(app_mode, workflow_run_id, on_subscribe)` → `rate_limit.generate(convert_to_event_stream(...))` → `compact_generate_response` SSE.
- CHAT MESSAGE (worker side): `workflow_based_app_execution_task` deserializes `AppExecutionParams` → opens fresh `Session` → reloads `App`/`Workflow`/user (`set_login_user` for flask-login context) → `WorkflowAppGenerator().generate` → `WorkflowAppConfigManager` + `WorkflowAppRunner` → `WorkflowEntry.__init__` (call-depth check, `InMemoryChannel` default, `capture_current_context`) → `GraphEngine.run()` → `iter_dify_graph_engine_events` filters → queue manager publishes to Redis → client SSE relay → finish persists `WorkflowRun`/node executions; `PauseStatePersistenceLayer` handles human-input pauses.
- DEBUG SINGLE-NODE: `AppGenerateService.generate_single_iteration`/`generate_single_loop` bypass the Celery hop entirely — they resolve the draft workflow via `_get_workflow(..., InvokeFrom.DEBUGGER)` and call `single_iteration_generate`/`single_loop_generate` on the advanced-chat/workflow generators synchronously in the request process.
- INNER API AUTH: plugin-daemon/enterprise calls hit `/inner/api/*` → `inner_api_only` checks `dify_config.INNER_API` (404 if disabled) then constant-compares `X-Inner-Api-Key` against `INNER_API_KEY`; `enterprise_inner_api_user_auth` additionally accepts `Authorization: <user_id>:<hmac>` and injects an `EndUser`.
- SERVICE API AUTH: `/v1/*` resources use flask-restx wrappers that extract the `Authorization` bearer `ApiToken`, cache via `ApiTokenCache`/`fetch_token_with_single_flight`, record token usage, and resolve the `EndUser`-equivalent caller from `user` query/json/form params per `FetchUserArg` policy. (inferred)
- STOP FLOW: `POST /chat-messages/<task_id>/stop` → `AppTaskService.stop_task(task_id, invoke_from, user_id, app_mode)` writes a Redis cancellation key → running task pipeline notices and raises `GenerateTaskStoppedError` → worker cleanup calls `clear_app_task_cancellation_signals`. (inferred)
- EXTENSION INIT ORDER is load-bearing: `ext_key_provider` runs after `ext_storage` because `RSAKeyProvider` reads private keys from storage; `ext_logstore` runs after storage and before celery; `ext_import_modules` runs early so controller modules' route decorators evaluate before blueprint registration.
Testing signals
Testing is layered and unusually heavy: ~1655 Python test files split into `unit_tests`, `integration_tests` (Docker-backed, CI-only), and `test_containers_integration_tests`, plus per-provider `vdb/*/tests` and `trace/*/tests` in the uv workspace. Web uses Vitest (+browser mode), e2e uses Cucumber+Playwright with accessibility (axe/WCAG) lanes, cli uses Vitest against a real Hono mock server, and the Go runtime has colocated `*_test.go`. CI shards API unit tests by historical durations and gates on codecov.
- `api/pytest.ini` configures coverage (`--cov=./api`, branch, json+xml reports), `--import-mode=importlib`, and `pytest-env` seeds dozens of fake provider keys plus `PLUGIN_DAEMON_URL`, `PLUGIN_DAEMON_KEY`, `INNER_API_KEY_FOR_PLUGIN`, and `MOCK_SWITCH=true` for hermetic unit runs.
- Backend tests live in `api/tests/` (1655 py files): `unit_tests/` mirrors the package tree (core, controllers, services, tasks, schedule, factories, migrations, tools, datasource, trigger, extension, app/layers/task_pipeline), `integration_tests/` covers controllers/services/workflow/model_runtime/tools/vdb/storage/tasks against real middleware, and `test_containers_integration_tests/` uses testcontainers.
- `make test` runs unit tests with `pytest -p no:benchmark --timeout 20 -n auto` over `api/tests/unit_tests` + all `api/providers/*/tests/unit_tests`, running `unit_tests/controllers` in a second pass with `--cov-append`; `make test-all` adds `--start-middleware` integration runs and `--start-vdb` VDB smoke tests (chroma, pgvector, qdrant, weaviate).
- CI `.github/workflows/api-tests.yml` downloads a shared timing baseline (`test-times.json` from a dify-test-infra stats repo) and runs `api/dev/pytest_sharding.py` to freeze per-shard file assignments, uploading the plan as an artifact for the shard jobs.
- A dedicated `.github/workflows/db-migration-test.yml` exists for migration correctness; codecov.yml at root configures coverage reporting (CODECOV_TOKEN secret plumbed through the reusable workflow).
- Web tests are Vitest with a browser project (`vitest.browser.setup.ts`, `vitest.setup.ts`, `vitest-browser-react`, `vitest-canvas-mock` deps); specs are colocated (`web/service/base.spec.ts`, `web/service/common.spec.ts`, `web/service/__tests__/`) and `pnpm test` = `vp test --project unit`.
- E2E is Cucumber+Playwright in `e2e/` (`cucumber.config.ts`, `features/{apps,auth,smoke,agent-v2,accessibility}`) with tag-selectable lanes including `@axe`/`@wcag-aa` accessibility scans and `@browser-smoke` Chromium+WebKit lanes; scripts orchestrate seed/reset/middleware/web-build lifecycle.
- CLI tests are Vitest colocated with commands (`cli/src/commands/**/*.test.ts`, `coverage.test.ts`, `agent-guides.test.ts` for meta-coverage) and run against a real Hono mock server at `cli/test/fixtures/dify-mock` — AGENTS.md explicitly forbids nock/msw/fetchMock.
- Go runtime tests are colocated (`internal/server/*_test.go`, `cmd/runner/main_test.go`, `packaging_test.go`) covering api/config/db/output/runtime/snapshot/tmux/service; landlock has build-split linux vs stub files suggesting sandbox behavior is only tested on Linux.
- Contract testing exists: `packages/contracts` holds `openapi-yaml.test.ts`, `service-api-contract.test.ts`, `knowledge-fs-contract.test.mjs`, `sandbox-contract.smoke.test.ts` validating generated TS clients against backend OpenAPI output; `api/dev/lint_response_contracts.py` (`make api-contract-lint`) lints Flask response docs against returned schemas.
- Static gates beyond unit tests: `make lint` runs ruff format+check --fix + response-contract lint + `lint-imports` (import-linter module boundaries) + `dotenv-linter` over `api/.env.example`/`web/.env.example`; `make type-check` runs pyrefly (`python-platform=linux`, `infer-with-first-use`) + mypy excluding tests/migrations.
- Integration tests are CI-owned per repo policy — `api/AGENTS.md` states docker-backed suites are not expected to run locally and agents must not start long-running services — so local verification is unit-test-only for most developers.
Risk map
The highest-risk surface is execution of untrusted work: SSRF-bearing HTTP egress, sandboxed code execution, shell job runtime with Landlock, and plugin-daemon RPC — all defended in depth but dependent on every caller routing through the right proxy. Auth is layered (session JWT, passport JWT, API tokens, shared inner-api key) with enterprise license gating able to fail closed across nearly every surface. Fragile spots: committed static test secrets, `ignoreBuildErrors` on the web build, a giant ordered extension-init list, and tenant-scoping discipline enforced by convention.
- SSRF is a designed-in risk domain: generic outbound HTTP must route through `core/helper/ssrf_proxy.py` (Squid at `SSRF_PROXY_*_URL`, `docker/ssrf_proxy/squid.conf.template`); remote file retrieval is split into `core/file/remote_fetcher` — any future code path using raw httpx bypasses egress policy.
- Sandboxed execution: `dify-sandbox` is a separate container image for code nodes; `dify-agent-runtime` executes user shell jobs via tmux with Landlock filesystem restrictions — but `landlock_stub.go` implies no sandbox on non-Linux kernels, so dev/other-OS runs are unsandboxed.
- The entire `/inner/api` enterprise/plugin control plane hangs on one shared secret: `X-Inner-Api-Key` string-equality against `dify_config.INNER_API_KEY`, gated by the `INNER_API` flag (404 when off) — key rotation/leak blast radius covers workspace, agent, knowledge, and runtime-credentials endpoints.
- Enterprise license failure is fail-closed across `/console/api`, `/api`, `/v1`, `/mcp`, `/triggers`: unverifiable license (`None` on exception) produces the same denial as INACTIVE/EXPIRED/LOST — a license-service outage can take down the whole product except `/files`, `/inner/api`, `/health`.
- Committed secret-shaped values in `api/pytest.ini` (PLUGIN_DAEMON_KEY, INNER_API_KEY_FOR_PLUGIN, ANTHROPIC/OPENAI keys) are test fakes but are static, long-lived, and grep-visible — risk of pattern-copy into real envs. (inferred)
- Web production builds cannot fail on type errors: `next.config.ts` sets `typescript.ignoreBuildErrors: true`; correctness relies on separate lint/type-check CI steps staying green.
- Tenant isolation is enforced by convention, not framework: AGENTS.md mandates propagating `tenant_id` across every layer and re-scoping reads by 'the complete owner chain', and `decode_jwt_token`/`AppExecutionParams` rehydrate entities from DB after trust boundaries — a missed `tenant_id` filter in any new service method is a cross-tenant data leak.
- Celery payloads carry user ids and raw `args` through the Redis broker (`AppExecutionParams.model_dump_json()`); broker access reveals tenant inputs, and non-idempotent retried tasks would double side effects — AGENTS.md requires idempotency and bounded transactions (no external I/O inside open DB transactions).
- Route enumeration is a known concern: `RESTX_ERROR_404_HELP` is explicitly disabled because flask-restx appends URL-map suggestions to 404 bodies; `controllers/console/spec.py` and the `/openapi` blueprint expose generated specs (OPENAPI_ENABLED gated).
- The ordered `initialize_extensions` list is fragile coupling: init order is load-bearing (key_provider after storage, logstore before celery, blueprints after everything) and an `is_enabled()` skip can silently drop a dependency of a later ext. (inferred)
- Blocking-vs-streaming asymmetry: Agent App (`AppMode.AGENT`) is SSE-only — `response_mode='blocking'` raises 400 — and debug single-node/loop runs execute in the request process, so a runaway node can pin a web worker greenlet.
- Webapp auth has layered token semantics (`token_source`, `auth_type`, `granted_at` SSO-staleness checks against external/workspace SSO update timestamps); regressions in `_validate_user_accessibility` silently lock out or over-admit end users.
Extension points
Dify is built for extension at every layer: app modes via generator classes + a match-dispatch, workflow nodes via `DifyNodeFactory`/`resolve_workflow_node_class` (with most builtins now in the external `graphon` package), vector/trace backends via Python entry-point packages, storage via a `StorageType` match, remote config via named sources, HTTP routes via flask-restx Namespaces + module import lists, Celery work via the imports+beat_schedule assembly, moderation/tool/providers via factories and the external plugin daemon, CLI verbs via a generated command tree, and frontend calls via regenerated oRPC contracts.
- New app types plug in at three coordinated points: extend `AppMode` in `models/model.py`, add a generator under `core/app/apps/<mode>/` subclassing `BaseAppGenerator`, and add a `case` in `AppGenerateService._dispatch_generate`.
- Workflow node types resolve through `core/workflow/node_factory.py` (`DifyNodeFactory`, `resolve_workflow_node_class`, `is_start_node_type`); Dify-owned node dirs (`agent`, `agent_v2`, `datasource`, `human_input`, `knowledge_index`, `knowledge_retrieval`, `trigger_*`) coexist with `graphon.nodes.BuiltinNodeTypes` from the external engine package.
- Vector backends are true plugins: each `api/providers/vdb/vdb-*` package declares `[project.entry-points."dify.vector_backends"]` (e.g. `pgvector = "dify_vdb_pgvector.pgvector:PGVectorFactory"`) and joins the uv workspace via `tool.uv.workspace.members`.
- Tracing providers mirror the vdb pattern: `api/providers/trace/*` workspace packages (aliyun, arize-phoenix, langfuse, langsmith, mlflow, opik, tencent, weave) selected via `trace-*` dependency groups.
- Storage backends extend `StorageType` + a `case` in `Storage.get_storage_factory` returning a `BaseStorage` runner; implementations live in `api/extensions/storage/` (s3, opendal, azure_blob, gcs, oss, cos, oci, obs, baidu, supabase, tos, clickzetta_volume).
- Config additions go into the right `configs/` mixin group (deploy/enterprise/extra/feature/middleware/observability/packaging/remote_settings_sources) and are picked up by pydantic-settings; remote sources extend `RemoteSettingsSourceName` + a `case` in `RemoteSettingsSourceFactory`.
- New HTTP routes: add a flask-restx `Namespace` resource module under the right `controllers/` blueprint package; console modules must be imported in `controllers/console/__init__.py` (`RESOURCE_MODULES` tuple or the `from . import …` blocks) so route decorators evaluate.
- New Celery work: add module path to `ext_celery.init_app`'s `imports` list, gate it with an `ENABLE_*` flag in `CeleryScheduleTasksConfig`, and add a `beat_schedule` entry for periodic jobs — the file explicitly asks contributors to add the switch.
- Model/tool/agent-strategy providers live in the external plugin daemon, not this repo: new capabilities surface to the API through `core/plugin/impl/*` clients and are installed via marketplace; local extension is via `ext_code_based_extension` + `api_based_extension` external-data-tool hooks.
- Moderation backends plug into `core/moderation/factory.py` with existing `keywords`, `openai_moderation`, and `api` implementations as templates.
- CLI verbs: create `cli/src/commands/<topic>/<verb>/index.ts` extending `DifyCommand` (sibling `run.ts`/`handlers.ts`/`guide.ts` optional; `_shared/` excluded from discovery), then `pnpm tree:gen` regenerates `tree.generated.ts` — CI gates drift with `tree:check`.
- Frontend↔backend contract extension is generated, not handwritten: new console endpoints flow through `api/controllers` schemas → `api/dev/generate_swagger_specs.py`/`generate_fastopenapi_specs.py` → `packages/contracts` `gen-api-contract` (openapi-ts) → consumed via `consoleClient`/`consoleQuery`; AGENTS.md forbids handwritten REST helpers or direct edits to generated contracts.
Contributor handoff
This repo is agent-aware: layered AGENTS.md files (root, api/, web/, cli/, e2e/, dify-agent/) define per-area rules that must be read before editing. Backend work runs through `uv run --project api` and root `make` targets; frontend through pnpm/vp; CLI through pnpm with generated artifacts. Generated files (docker-compose.yaml, tree.generated.ts, contracts/generated, openapi specs) must never be hand-edited. The dominant correctness disciplines are tenant scoping, explicit bounded transactions, SSRF-routed egress, Pydantic v2 contracts at controller boundaries, and idempotent Celery tasks.
- Read the nearest AGENTS.md before editing: root `AGENTS.md` (repo gotchas: `uv run --project api`, integration tests CI-only, docker env layering), `api/AGENTS.md` (layer boundaries + typing rules), `web/AGENTS.md` (contracts/i18n/dify-ui + 'this is NOT the Next.js you know' warning), `cli/AGENTS.md` + `cli/ARD.md` (command structure), `e2e/AGENTS.md` (runtime ownership).
- Backend commands: `make lint` (ruff format+check --fix + `api-contract-lint` + `lint-imports` + `dotenv-linter`), `make type-check` (pyrefly + mypy), `make test` / `make test TARGET_TESTS=./api/tests/<path>`; direct Python via `uv run --project api`.
- Layer rule: transport parsing/serialization stays in controllers, orchestration in `services/`, domain policy in `core/` or its domain owner; `libs/` must stay business-agnostic. Pydantic v2 models for req/resp; domain exceptions are translated at the controller boundary (see `ChatApi.post`'s long except-chain).
- Never hand-edit generated artifacts: `docker/docker-compose.yaml` (edit `docker-compose-template.yaml`/`envs` + regenerate via `docker/generate_docker_compose`), `cli/src/commands/tree.generated.ts` (`pnpm tree:gen`), `packages/contracts/generated/*` (regen via `gen-api-contract`), and generated API specs.
- Tenant safety is the #1 trap: scope every tenant-owned read/write by the complete owner chain, propagate `tenant_id` across layers, and rehydrate trusted entities from DB state after payload/async boundaries (pattern: `workflow_execute_task` reloads everything from `AppExecutionParams` ids).
- Egress/storage/config discipline: read config only via `configs.dify_config`, storage only via `extensions.ext_storage.storage`, and outbound HTTP only via `core.helper.ssrf_proxy` (or `core.file.remote_fetcher` for remote files).
- Celery discipline: tasks that may retry must keep side effects idempotent and log affected resource ids; keep write transactions explicit/bounded and never do external I/O inside an open transaction unless a documented consistency contract requires it.
- Monkey-patching order is load-bearing: `app.py`'s `if __name__ == "__main__"` gevent patch must precede socket/lock imports; gunicorn (`gevent` worker class) and Celery (`celery_entrypoint.py`) do their own patching — adding imports above the patch block freezes the process.
- `/system-features` is a minimal unauthenticated bootstrap allowlist, not a config registry — before touching controller schemas or `SystemFeatureModel`, read `controllers/API_SCHEMA_GUIDE.md`.
- Frontend: use generated `consoleQuery`/`consoleClient` from `@/service/console` for backend calls (no handwritten REST/DTO mirrors); user strings must add keys in `web/i18n/locales/en-US/` and update every locale; prefer `@langgenius/dify-ui/*` primitives per the linked contracts in `web/AGENTS.md`.
- Version is unified at 1.17.1 across `api/pyproject.toml`, `web/package.json`, `cli/package.json`, `dify-agent/pyproject.toml` and the compose image tags (`langgenius/dify-api:1.17.1`, `dify-web:1.17.1`, `dify-agent-backend:1.17.1`) — bump coherently.
- Tests an agent can actually run locally: `make test` (unit only), `pnpm -C web test`/`vp test`, `pnpm -C cli test`, `cd dify-agent && make test`, `go test ./...` in dify-agent-runtime. Docker-backed integration, VDB smoke, and e2e are CI-only/opt-in — do not start long-running services during routine work.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 6845 | 61% |
| Python | 4101 | 37% |
| JSON | 1002 | docs/config |
| Markdown | 178 | docs/config |
| JavaScript | 136 | 1% |
| YAML | 128 | docs/config |
| CSS | 68 | docs/config |
| Go | 66 | 1% |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (928 files)
Repository structure
| Top-level path | Files |
|---|---|
api/ | 928 |
packages/ | 928 |
web/ | 927 |
cli/ | 362 |
dify-agent/ | 290 |
e2e/ | 155 |
docker/ | 82 |
dify-agent-runtime/ | 73 |
Docs found: AGENTS.mdCONTRIBUTING.mdREADME.mddocs/ADVANCED_SETUP.mddocs/ar-SA/ADVANCED_SETUP.mddocs/ar-SA/README.mddocs/bn-BD/ADVANCED_SETUP.mddocs/bn-BD/README.mddocs/cross-env-app-migration/README.mddocs/de-DE/ADVANCED_SETUP.md
Package
package.json — dify, 33 declared dependencies.
@eslint-community/eslint-plugin-eslint-comments@eslint-react/eslint-plugin@eslint/markdown@iconify-json/heroicons@iconify-json/ri@shadcn/lint@tanstack/eslint-plugin-query@types/node@typescript-eslint/parser@typescript/nativeconcurrentlyeslint
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructions.github/workflows/accessibility-e2e.yml— GitHub Actions workflow.github/workflows/api-tests.yml— GitHub Actions workflow.github/workflows/autofix.yml— GitHub Actions workflow.github/workflows/build-push.yml— GitHub Actions workflow.github/workflows/cli-e2e.yml— GitHub Actions workflow.github/workflows/cli-edge.yml— GitHub Actions workflow.github/workflows/cli-release.yml— GitHub Actions workflow.github/workflows/cli-smoke.yml— GitHub Actions workflow
Structural health signals
Average score 6.5/10 across 96 analyzed source files; 45 hotspots surfaced.
packages/contracts/generated/api/web/zod.gen.ts— 1695 lines, score 5 (Large file, Deep nesting)lint.config.ts— 1445 lines, score 5 (Large file, Deep nesting)packages/contracts/generated/api/console/installed-apps/zod.gen.ts— 1307 lines, score 5 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
3a017e8cccb32026-09-22 — perf(ci): reuse coverage XML totals for API summary (#42745)1fc040183c622026-09-22 — chore: replace tsx with native Node TypeScript execution (#42742)5b6e850b250e2026-09-22 — refactor(api): thin console app controllers (#42511)ce2eaab9f7322026-09-22 — refactor(api): move app-scoped end users behind application services (#41398)9badde4d655c2026-09-22 — fix(marketplace): support single-page carousel navigation (#42743)