<aside> 📌
Every way to bring an external system into cognee memory. There are four, they share almost nothing, and picking the wrong one is the expensive mistake. Start at §0, then read only your path.
Verified against the code on dev, 2026-08-27. Reference implementations: Slack, GitHub, Linear (Path A), the community connectors and examples/demos/ingestion_and_migration/dlt_ingestion_example.py (Path B), the two examples under examples/demos/ingestion_and_migration/ (Path C).
</aside>
| If the source is… | Path | Mechanism | Ships |
|---|---|---|---|
| A SaaS account the user connects in the UI, with webhooks pushing changes (Slack, GitHub, Linear) | A, §1 | OAuthIntegration subclass registered with use_integration() |
In core, cognee/modules/integrations/<provider>/ |
| A SaaS source you pull on a schedule, no webhooks, no consent screen in cognee (Gmail, Notion, Confluence, Drive, Slack export) | B, §2 | A plain dlt source handed to remember() |
Community package cognee-community-connector-<source> |
| An existing relational database whose schema and rows should become graph structure | C, §3 | migrate_relational_database() over an extracted schema |
In core, cognee/tasks/ingestion/ |
| An export from another memory system (Letta, Zep, mem0) | D, §4 | cognee/modules/migration/ import sources |
In core, plus worked examples |
Four different things in this codebase get called "integrations". Don't use Path A for any of these:
| What | Seam |
|---|---|
| A client that talks to cognee (Claude Code, Codex, OpenCode, MCP, raw SDK) | KNOWN_PLUGINS in plugins.py, provisioned via POST /integrations/plugins/{key}/provision, which mints an agent sub-user plus API key. No OAuth involved |
| A database backend (Qdrant, Milvus, Weaviate, FalkorDB) | VectorDBInterface / GraphDBInterface plus a register.py calling use_vector_adapter / use_graph_adapter, in the cognee-community repo |
| A new file format (PDF flavour, HTML, office docs) | LoaderInterface plus use_loader(). cognee/infrastructure/loaders/external/ holds core's own in-tree loaders, not third-party ones; an out-of-tree loader registers use_loader() from its own package |
| An LLM or embedding provider | Configuration only, litellm makes the call |
| A hub listing for something that already works | Catalog YAML under catalog/entries/integrations/, validated against catalog/schema.json. Today that directory holds only agent-client plugins (claude-code, codex, …); no OAuth data-source provider has an entry there yet |
registry.py (Path A providers) and plugins.py (clients authenticating to cognee) are two different registries that live side by side on purpose. Register in the wrong one and you get a 404 from the router that matters.
One table, one router, one encryption seam, one OAuth state scheme, shared by every provider. Adding provider #4 is a new class, not a new table or route.
| Concern | Handled by | Do you touch it? |
|---|---|---|
| Credential storage, one row per connected account | models/IntegrationCredential.py, table integration_credentials, discriminated by provider |
No. A new provider is a new row value, so it needs no schema change. Changing the table itself needs an Alembic revision, see §5 |
| Token encryption at rest (AES-256-GCM, keyring, rotatable) | crypto.py plus credentials.py |
No, but the deployment needs a key (§1.8) |
OAuth CSRF state: minting, signing, TTL, binding the callback to the initiating user |
oauth_flow.py |
No, you only supply the signing secret |
| exchange code, parse, persist | connect.py complete_installation() |
No, it calls your two methods |
| HTTP surface: authorize, callback, events, connection, disconnect, status | cognee/api/v1/integrations/routers/get_integrations_router.py |
No, it dispatches on {provider} from the URL |
| Cross-owner conflict ("this account is already connected elsewhere") | upsert_credential(), raising CrossUserConflictError |
No |
| Detaching slow work from the request (post-install sync, webhook handling) | The router's _spawn_background() |
No, your hooks are already called off the request path |
sequenceDiagram
participant U as User (browser)
participant F as Frontend
participant R as Generic router
participant A as Your adapter
participant P as Provider
participant DB as integration_credentials
Note over U,DB: Install
U->>F: clicks Connect
F->>R: POST /integrations/{provider}/authorize
R->>R: make_state(user.id, signing_secret)
R->>A: authorize_url(state)
A-->>F: consent URL
F->>P: redirect to consent screen
P->>R: GET /integrations/{provider}/callback?code&state
R->>R: validate_state gives user_id (the only auth this route has)
R->>A: exchange_callback(code, params)
A->>P: token exchange, plus identity lookup if needed
R->>A: parse_installation(token_response)
A-->>R: OAuthInstallation
R->>DB: upsert_credential, token_payload encrypted
R-->>U: redirect to frontend ?{provider}=connected
R->>A: on_installed(credential), detached
A->>A: initial sync into memory
Note over U,DB: Webhook
P->>R: POST /integrations/{provider}/events
R->>A: webhook_verifier().verify(request) over raw bytes
R-->>P: 200 ok, acked before any work
R->>A: handle_webhook(raw_body, headers), detached
A->>DB: get_credential_by_account(provider, account_id)
A->>A: remember() the change
<aside> 🔑
Two facts drive every design decision in this path. The callback is necessarily unauthenticated: the browser arrives from the provider's site with no session, so a valid, unexpired, unforged state is the only thing tying an inbound code to a cognee user. The events route is necessarily unauthenticated too: providers can't send a bearer token, so your WebhookVerifier is the entire auth model.
</aside>
cognee/modules/integrations/base.py. Treat these signatures as a public extension seam: third parties build cognee-community-integration-<x> packages against them, so changing one is a breaking change.