# Offer Notion, GitHub and Slack in your product (experimental)

Source: https://docs.littlebigbrain.com/guides/developer-owned-integrations/

Your company owns the provider app and its branding. Your backend holds the LBB
stack key, chooses a graph for each customer, and creates a temporary connection
link. Customers consent to your app, select resources, and return to your product
without an LBB account. LBB seals their grants and runs the source readers.

This is an **opt-in beta**. Fixture tests cover the local flow; they do not prove
live provider acceptance, distribution approval, or permission to index a
customer's data. Enable and test your own apps before offering the flow to users.

## Enable and register an app

1. Ask your operator to enable developer integrations for the stack and the
   server's `LBB_PROVIDER_APPS_ENABLED=1` flag. The API and worker require the
   same explicit `LBB_CREDENTIALS_KEY`. Use separate test and production stacks.
2. In the console, select the stack, then open **Integrations → Provider apps**.
   App management requires an account session with stack-management permission.
   Stack API keys can list safe app summaries and create customer links; they
   cannot manage app credentials.
3. Register your app's identifiers and secrets. Copy the callback and events URLs
   displayed by LBB into the provider's settings. If your product already receives
   events, configure forwarding that preserves the original body and signing
   headers; do not replace an existing receiver without arranging delivery.
4. Allow your product's return origin, such as `https://app.example.com`. HTTPS is
   required except for loopback development. A saved **Configured** state records
   configuration; it does not certify provider approval or a successful install.

Keep app credentials in this authenticated setup flow. Customers never paste
provider tokens into the hosted consent page. Direct credential-based connection
creation is refused for hosted `notion`, `github` and `slack` connections.
The target must be a property graph; RDF-native graphs are refused during setup.

Rotate in two stages. Keep the current primary unchanged while adding the future
key to every reader's decryption-only keyring:

| Process | Decryption-only setting | Format |
| --- | --- | --- |
| SaaS API and integration worker | `LBB_CREDENTIALS_PREVIOUS_KEYS` | Comma-separated base64 keys |
| Separate CDC host | `LBB_CDC_OPEN_ONLY_KEYS` | JSON array of base64 key strings |

Both settings accept up to eight keys. Verify every reader has restarted with the
future key before promoting it to `LBB_CREDENTIALS_KEY`. Retain the former primary
in each reader's decryption-only setting during that second rollout. The CDC host
does not read `LBB_CREDENTIALS_PREVIOUS_KEYS`. Deployment preflight validates the
configured target fleet before changing services; the operator must also verify
the separately managed CDC hosts.
Keep former keys while saved grants, registrations or callback URLs still use them;
changing the primary does not rewrite stored records. Lost keys cannot decrypt
their old records.

| Provider | Developer setup | Customer scope and current reader |
| --- | --- | --- |
| Notion | A public connection for **Any workspace**, with Read content; client ID, secret and integration ID. Configure its webhook subscription. Retrieve the candidate verification value from LBB's app-management page, verify it in Notion, then explicitly confirm it in LBB. | Explicit pages and data sources inside the consent grant. Page roots include descendants; data sources include their rows. Pages, bounded block text and source metadata map onto Documents. No attachments are downloaded. |
| GitHub | A public GitHub App for installation by other accounts; App ID, client ID/secret, private key, webhook secret and slug. Request read-only Metadata, Issues and Pull requests; enable expiring user tokens and repository/issue/PR/comment/review events. | Up to 100 repositories in one installation, intersected with the connecting user's access. Repository metadata, issues/PRs, comments and reviews map onto Work. GitHub.com only. |
| Slack | A distributed confidential app; App ID, client ID/secret, signing secret and a one-time app configuration access token. Bot scopes: `channels:read`, `channels:history`, `groups:read`, `groups:history`. Configure message/lifecycle events and the Events URL. | Explicit joined, non-shared channels, with messages and threads. No DMs or Slack Connect. History stays disabled until the owner has the required provider/use-case agreement and explicitly enables the history profile in LBB. |

For GitHub, customers must install the app as well as authorize their user. If the
resource list is empty, follow the app's installation link, complete any required
organization approval, then start a new connection link. Callback installation
IDs alone do not establish access. Slack's history flag records the owner's
configuration; LBB does not verify an agreement merely because it is enabled.

Slack setup requires an **App configuration access token** generated by an app
manager at [Slack's app settings](https://api.slack.com/apps). LBB uses
[`apps.manifest.export`](https://docs.slack.dev/reference/methods/apps.manifest.export)
to verify access to the exact registered app, then discards the token. It is not
stored with app credentials and is not a customer's bot token or app-level token.
The management request uses `secrets.ownershipToken`. For an existing registration
without proof, use **Edit** to supply it before new consent. Existing grants stay
readable and local disconnect stays available. Only proven registrations can
reserve global mint capacity; a public App ID alone is insufficient.

Provider setup references: [Notion public connections](https://developers.notion.com/guides/get-started/public-connections)
and [webhooks](https://developers.notion.com/reference/webhooks),
[GitHub App visibility](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/making-a-github-app-public-or-private)
and [sharing an installation link](https://docs.github.com/en/apps/sharing-github-apps/sharing-your-github-app),
[Slack OAuth](https://docs.slack.dev/authentication/installing-with-oauth/),
[Events API](https://docs.slack.dev/apis/events-api/) and
[commercial distribution terms](https://docs.slack.dev/changelog/2025/05/29/tos-updates/).

## Create the customer link from your backend

Use the client from [Integrations for your customers](https://docs.littlebigbrain.com/guides/integrations-for-your-customers/#create-the-client).
Derive `graph` from your authenticated customer record. A stack key can access
every graph in the stack, so never accept a customer-supplied graph as authority.

```ts
const { apps } = await lbb.integrations.providerApps();
const app = apps.find((app) => app.active && app.kind === "notion");
if (!app) throw new Error("Notion is not configured for this product");

const session = await lbb.integrations.createConnectSession({
  appId: app.id,
  graph: authenticatedCustomer.graph,
  connectionId: "notion-main",
  returnUrl: "https://app.example.com/settings/connections",
});
// Redirect this customer to session.url. Do not log or cache the bearer link.
```

```python
apps = lbb.integrations.provider_apps()["apps"]
app = next(app for app in apps if app["active"] and app["kind"] == "notion")
session = lbb.integrations.create_connect_session(
    app_id=app["id"],
    graph=authenticated_customer.graph,
    connection_id="notion-main",
    return_url="https://app.example.com/settings/connections",
)
# Redirect this customer to session["url"]. Keep it out of logs and analytics.
```

The browser visits LBB's public connect page, completes provider consent, selects
resources explicitly, then returns to the registered product origin. LBB adds
`lbb_session` to the return URL. Your backend must fetch that session and verify
both the customer's graph and `complete` status. Browser parameters alone do not
prove success.

```ts
const result = await lbb.integrations.getConnectSession(returnedSessionId);
if (result.graph !== authenticatedCustomer.graph || result.status !== "complete") {
  throw new Error("Connection has not completed for this customer");
}
const { connections } = await lbb.integrations.list({ graph: result.graph });
```

Python uses `get_connect_session(id)` and dictionary fields. The async client
offers the same methods. Poll connection status for sync progress: a completed
consent session does not mean its first sync has finished. There is no product
notification webhook in this beta.

Sessions expire after 30 minutes and each creation consumes the creation quota.
The SDK does not automatically retry creation. Request a fresh link after expiry
or an ambiguous code-exchange outcome; an authorization code is single-use. Treat
the link as a bearer secret, including in browser analytics and referrer handling.

The authorization link must finish in the browser that started it. LBB checks
an HttpOnly, SameSite cookie at the provider callback before exchanging the code.
Opening a copied provider authorization URL in another browser fails safely.

## Reconnect, rotate or disconnect

Reauthorize with the existing `connectionId` and the same selected resources.
Resource changes require a **new graph and rebuild** in this beta. A new
connection ID in the same graph retains the old source data. Hosted app
connections reject direct `setCredentials` and `setSettings` changes.

Rotate developer app secrets from **Provider apps**; blank secret fields preserve
the saved values. Keep the same app identity for rotation. A replacement provider
app requires a new registration and customer consent. Disable an app to prevent
further use of its LBB grants and revoke their provider tokens. Failed provider
calls are retried independently. Re-enabling an app does not restore revoked
customer grants; customers must connect again.

`integrations.delete(id, { graph })` in TypeScript, or
`integrations.delete(id, graph=graph)` in Python, removes the local connection
and revokes its provider tokens. **Imported graph data remains.** LBB blocks
new provider reads immediately; if the provider is unavailable, disconnect reports a retryable
error and retains sealed credentials only for recovery. Retries stop after eight
attempts or 24 hours, retaining a sealed diagnostic record and releasing active
capacity. Pending retries do not block unrelated customer connections. Tokens
held by another live grant or consent session for the same app are protected;
Slack token revocation never uninstalls the workspace app. Staging and production
may share a physical Slack app. Shared and legacy registrations require manual
remote revocation: disconnect and erase fence local access and archive the grant
as `shared_token_manual`. Coordinate revocation across environments, then acknowledge
it through the operator route. The console shows a read-only **Revocation policy**. Fresh exclusive
registrations retain automatic revocation after a verified grant. Only a successful
exchange returning the registered Slack app ID establishes membership; a second
verified registration permanently selects manual revocation. Denied consent,
missing codes and wrong-app grants cannot change the policy. Consent attempts
hold only expiring two-minute token-operation reservations. Registrations from
older policy versions remain manual. Safety history survives stack erasure
without locking new registrations out.
Use [customer erasure](https://docs.littlebigbrain.com/guides/integrations-for-your-customers/#erase-a-customer)
when every connection and all data in that customer's graph must be removed.
Erasure proceeds after local access is fenced, even if provider revocation fails.
New connect links do not wait for old provider retries. Retry queues have their own
capacity; overflow or exhausted attempts retain sealed archives for operator action.
Service admins can list, retry or acknowledge them through
`/admin/provider-revocations`. Expired consent records are reclaimed after archiving.
Repeated client retries respect backoff and do not exhaust provider attempts early.
New archive logs carry the stack, app and archive IDs needed by these routes.
The admin-protected `/metrics` endpoint also exposes
`lbb_provider_revocation_archived_total{kind="auth"|"session"}`, a process counter
of newly archived grants, excluding duplicate retries. It resets on restart and
does not represent the current unresolved backlog.
A missing sealing key also preserves the opaque grant after fencing local access;
restore the key or revoke externally and acknowledge.

For an unverified Notion subscription with a stale challenge, use **Discard unconfirmed token**
in app settings and request a fresh challenge from Notion. Discard uses the displayed
candidate version and refuses to overwrite a newer or already trusted verification. Confirmation and discard hold a two-minute claim;
after a crashed request it expires, so retrying setup cannot leave an older
confirmation free to publish a discarded token.

## Data and access limits

These readers use bounded source passes and signed events; they do not promise
complete incremental replication. Missing records in a partial listing do not
prove deletion. Verified deletion markers live in the graph, with no lifetime
256-deletion limit; each turn processes a bounded batch. The reserved
`LbbIntegrationDeletion` bookkeeping class is visible in raw queries and class
inspection and Ask names, and counts toward storage. Select your source classes in app queries.
Markers survive disconnect/full sync. Notion restore events verify the resource's
current state before allowing active content again. A later trash row can subsequently retract that page even if its delete webhook
is lost. When restore and re-trash share Notion's coarse timestamp, LBB checks the
resource's current state rather than treating the equal timestamp as a restore. Other sources require a new
connection identity or rebuilt graph for restore. At the graph storage limit,
marker writes can still block retraction; metadata is not exempt from storage quota. Clearing every old source property and
relationship is not fully reconciled. Explicit entity deletion can also remove
properties your product added to that imported entity, so keep enrichment that
must survive source deletion on separate entities.

Webhook intake captures a routing snapshot and saves the payload in a bounded
durable inbox before replying. Each of 16 hash shards admits at most 64 deliveries
and 48 KiB. A full shard returns 503 with `Retry-After: 30` before acceptance;
unaccepted deliveries require provider or forwarding-service retries. [GitHub does
not automatically redeliver failures](https://docs.github.com/en/webhooks/using-webhooks/handling-failed-webhook-deliveries);
its owner can [redeliver through the UI or API within three days](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/redelivering-webhooks). Oversized deliveries
return 413. Background workers move accepted work into each customer's own paged
storage, even when its 128-delivery working queue is full. Each page holds at most
128 references and 48 KiB. A full customer queue no longer occupies shared
admission capacity; sustained ingress above fanout capacity or a control-plane
outage can still affect the app. Accepted deletion work remains until delivered or
its authorization generation retires, so backlog storage grows with accepted work.
Workers wake on arrivals and retry deadlines.
Slack's three-second reply target still needs live load validation. Notion signed events have a seven-day age limit.
GitHub events without a trustworthy occurrence timestamp cannot all receive the
same check. Small completed and unmatched event fences remain until stack/account erasure to
protect acknowledged deliveries from concurrent cleanup and prevent known delivery
IDs from rebinding after reconnect. GitHub delivery IDs are in unsigned headers; a
captured payload submitted under a new ID still has no universal timestamp check.
An unmatched delivery keeps its fence so replay after a new consent cannot attach
it to that customer. Retired overflow and journal pages retain compact closed-page
fences after their payloads and references are removed; these grow with page
rotations and also remain until stack erasure. Physical reclamation is future work.
Background retries skip unchanged progress/cursor writes and rotate a failed
batch once; a failed promotion does not block its existing working queue.

GitHub quota resets save a durable deadline and finish the turn. GitHub fit
planning and suggestion application use a local schema and run during the pause;
credential checks still need a provider request and wait. A paused Apply for a
connector needing remote schema remains visibly tracked until it finishes. Settings and
mapping saves apply immediately; credential checks and source-dependent fit checks
show that they are waiting, then follow the resumed action's actual result. The
host tick resumes due work, including after a resume turn was skipped.

Coalesced webhook hints preserve the cursor and retain a follow-up request for
resources already read, including hints arriving during the first source turn.
GitHub fresh passes triggered by hints wait at least five minutes after the last
pass finishes. Hints during that interval share one durable request, which also
runs on manual schedules. This interval does not delay explicit user syncs,
configured schedules, continuing the current pass or processing deletion events.
It limits repeated full scans; a large pass can still exhaust GitHub quota. A delayed worker shows
**Resume delayed** in the console. Old unreleased builds may have checkpointed
a timer turn that retains its original wake time until the engine claims it.
Removed repositories, inaccessible pages and unavailable channels produce sync
warnings and allow remaining sources to proceed. Existing imported facts remain
until there is authoritative deletion evidence or explicit erasure.

Losing provider access stops subsequent authorized reads; it does not guarantee
immediate erasure of previously imported facts or cancel an already-issued write.
Stored source ACL fields do not automatically filter SPARQL queries. Your backend
must authenticate users and enforce which customer graphs and results they can
read.

## Run the local product example

The repository includes `sdk/typescript/examples/embedded-integrations/server.mjs`.
It has Connect, backend-verified status and Disconnect, using one fixed demo
customer. From the repository root:

```sh
npm ci
npm run build -w @littlebigbrain/client
export LBB_STACK_URL='https://YOUR-STACK.db.eu.littlebigbrain.com'
export LBB_STACK_API_KEY='YOUR_TEST_STACK_KEY'
node sdk/typescript/examples/embedded-integrations/server.mjs
```

Allow `http://localhost:8080` in the provider app's **return origins**, then open
that exact address. `PORT` changes the demo's port and `DEMO_CUSTOMER_GRAPH`
changes its backend-selected graph. Register a new return origin when changing
the port. The example binds to loopback and rejects cross-origin mutations.

`LBB_INTEGRATIONS_URL` optionally selects another LBB API. Its operator must set
`LBB_SAAS_PUBLIC_URL` for reachable provider callbacks/events and
`LBB_SAAS_WEB_URL` for the console's consent page. The provider callback is an
LBB API URL, **not** the product's `localhost:8080` return URL. Before adapting
this example for production, replace its fixed customer with your authenticated
product session and keep the stack key on your backend.

Expected Notion text, block-request, property and relation bounds remain visible
in each record's `textStatus`, `propertiesStatus` or `relationsStatus` as
`truncated`; they do not mark the whole sync as warning. Skipped or inaccessible
sources remain `incomplete` and still raise warnings.
