Building a cognee Integration

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


0. Pick your path

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.


Path A: in-tree OAuth connector

1.1 What you get for free

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

1.2 The two flows

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>

1.3 The contract

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.

Required