Connecting Confluence Cloud
Moʻolelo indexes pages and blog posts from the Confluence Cloud spaces you list. It uses the Confluence v2 REST API for backfill and Confluence's webhook system for steady-state updates.
Prerequisites
- An Atlassian Cloud instance with at least one Confluence space.
- An Atlassian admin who can create an OAuth 2.0 (3LO) app and install it.
- Network reachability: Moʻolelo's webhook endpoint must be reachable from Atlassian's outbound webhook IP ranges.
Step 1 — create an OAuth 2.0 (3LO) app
In the Atlassian developer console, create an OAuth 2.0 (3LO) app under Permissions → Confluence API and enable every scope below. Atlassian splits its scope catalog into Granular and Classic tabs; some endpoints Moʻolelo calls accept only the granular form, others only the classic. Enabling all of them is the only configuration that works end-to-end — requesting a scope the app doesn't have causes the consent screen to silently drop it, and the connector then 401s on the affected endpoints with "scope does not match".
Granular scopes:
read:page:confluence— page reads.read:space:confluence— space + page-tree walk.read:content:confluence— content surfaces beyond the page object.read:content-details:confluence— body, version, ancestors.read:hierarchical-content:confluence— child/sibling traversal.read:space-details:confluence— space metadata.read:space.permission:confluence— required for space-level ACL extraction. Without it OpenFGA never learns who can read the space and the People inspector stays empty.read:content.restriction:confluence— per-page restriction reads (granular surface).read:content.permission:confluence— per-page restriction reads (v2 surface).read:user:confluence— principal lookups on restrictions.read:group:confluence— group lookups on restrictions.read:attachment:confluence— attachment listing/metadata.read:content.attachment:confluence— attachment binary download.readonly:content.attachment:confluence— alternative name some Atlassian apps require for the same download surface. Enable both.read:label:confluence— page labels.
Classic scope (under "Classic scopes → Confluence API"):
read:confluence-content.permission— required. The per-page restrictions endpoint Moʻolelo reads (/wiki/rest/api/content/{id}/restriction/byOperation/read) is on the v1 REST surface and only accepts the classic scope name; the similarly-named granular ones don't cover it. Without it consent succeeds but every page-restriction probe 401s.
Always required:
offline_access— Atlassian only issues a refresh token when this is requested. Without it the connector burns its grant after the first access-token expiry.
Make a note of:
- Client ID and Client Secret from the "Settings" tab.
- The redirect URL configured for the OAuth flow (you'll use this momentarily).
Step 2 — run the consent flow once
Moʻolelo doesn't drive an interactive consent flow; you do it once as an administrator and store the resulting refresh token in Moʻolelo's configuration.
A typical consent flow:
- Construct the consent URL per Atlassian's OAuth 2.0 (3LO) documentation using your Client ID, redirect URL, and the scopes above.
- Open it in a browser, sign in as the Confluence admin, and approve the install.
- Atlassian redirects to your redirect URL with an authorization
codeparameter. - Exchange the
codefor an access token + refresh token viaPOST https://auth.atlassian.com/oauth/token. The response carriesrefresh_token— that's what you store.
Your Atlassian Cloud has a unique Cloud ID that the API uses for routing. Find it via:
curl -H "Authorization: Bearer <access_token_from_the_exchange>" \
https://api.atlassian.com/oauth/token/accessible-resourcesThe response is an array of resources; pick the entry for the Confluence Cloud you're connecting and copy its id field.
Step 3 — register the connector
Insert a row into connector_configs:
INSERT INTO connector_configs
(tenant_id, source, instance, mode, resources, raw_config)
VALUES (
'default',
'confluence',
'acme', -- your "instance" label
'backfill',
'["SPACE_ID_1", "SPACE_ID_2"]'::jsonb, -- Confluence v2 space IDs
'{
"cloud_id": "12345678-aaaa-bbbb-cccc-dddddddddddd",
"site_url": "https://acme.atlassian.net",
"oauth": {
"token_url": "https://auth.atlassian.com/oauth/token",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"refresh_token": "the-refresh-token-from-step-2"
}
}'::jsonb
);Note the resources are space IDs, not space keys. Find them via Confluence's admin UI or the v2 API.
The connector exchanges the refresh token for short-lived access tokens automatically. If Atlassian rotates the refresh token (which it sometimes does), Moʻolelo updates its stored copy on the next exchange.
Step 4 — register the webhook on the Atlassian side
Confluence Cloud webhooks are configured via the Atlassian REST API. Register a webhook for each event type you want:
curl -X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
https://api.atlassian.com/ex/confluence/<cloud_id>/wiki/rest/api/webhooks \
-d '{
"name": "moolelo-confluence",
"url": "https://<your-moolelo-host>/webhooks/confluence/acme",
"events": ["page_created", "page_updated", "page_removed",
"blogpost_created", "blogpost_updated", "blogpost_removed"],
"secret": "<random-secret>"
}'The secret value goes into Moʻolelo's deployment as WEBHOOK_INGRESS_CONFLUENCE_SECRET. Each delivery carries an X-Hub-Signature: sha256=<hex> header that the webhook receiver validates.
Step 5 — flip to webhook mode
After backfill completes:
UPDATE connector_configs
SET mode = 'webhook'
WHERE tenant_id = 'default' AND source = 'confluence';What gets indexed
Per page or blog post, Moʻolelo indexes:
- Title — separate chunk, weighted higher in lexical scoring.
- Body — Confluence's storage-format markup (XHTML augmented with
<ac:*>/<ri:*>macros) is converted to clean Markdown by the connector before publishing: code macros become fenced code blocks, info / warning / note macros become blockquotes, drawio / gliffy / plantuml / mermaid macros are replaced with a_[<name> diagram: <title>]_placeholder, and inter-page links are preserved as plain markdown links. The Markdown is then chunked along its heading hierarchy. Heading paths are preserved as structural metadata. - Author — for residency routing.
- Source URL — direct link back to the Confluence page.
- Space and ancestors — the page tree path.
- Cross-document references — explicit references in the body (
PROJ-123Jira keys,owner/repo#42GitHub refs,*.atlassian.net/wiki/...page URLs,github.com/.../issues|pull/...URLs, and markdown links to other Confluence pages) are extracted intorefers_tograph edges so search can surface them as related entities. See Graph expansion.
What is not indexed:
- Attached files. Confluence's attached-file API is separate; if you need attachment search, the SharePoint connector covers Office documents directly.
- Comments. (Page comments are a separate API surface, not yet covered.)
- Personal spaces. Moʻolelo only indexes spaces explicitly listed in
resources; personal spaces are skipped even if the OAuth token has access.
Permissions visible in search results
Confluence's permission model is hierarchical: a page can inherit from its parent, from its space, or have its own restrictions. Moʻolelo extracts each page's effective permissions at ingestion time and projects them as authority records.
A user sees a page in search results only if:
- Their Confluence identity (matched via the user's email or Atlassian account ID) has read access on the page in Confluence's own permission model.
- And the tenant's Moʻolelo policy doesn't add additional restrictions.
Restricted pages do not leak titles or snippets via the search index — they are filtered out before retrieval.
Troubleshooting
- OAuth refresh failing. Atlassian rotates refresh tokens aggressively if a token is unused for ~90 days. If you see
invalid_granterrors, redo the consent flow (Step 2) and update therefresh_tokenfield inconnector_configs. - No records appearing. Verify the OAuth app has the granular scopes listed in Step 1. The Atlassian developer console shows which scopes the app actually has — the consent screen sometimes omits scopes if the app was configured incorrectly.
Unauthorized; scope does not matchon/restrictioncalls. The classic scoperead:confluence-content.permissionis missing from the app. Add it under "Classic scopes → Confluence API" in the developer console and re-authorize.Unauthorized; scope does not matchon/attachments/{id}/download. This is an open Atlassian-side bug where OAuth 3LO tokens are refused on the attachment download endpoint regardless of granted scopes. Page text still ingests; diagram-attachment rendering (drawio/gliffy preview images) is skipped with a placeholder. There is no client-side workaround today.- People list empty even after backfill. OpenFGA only learns about users when an ACL event is projected against them. Confirm
read:space.permission:confluenceis granted (the space-level ACL fetch needs it), and confirm the Atlassian identity source is also configured (see Identity catalog) —accountIdvalues on ACL events resolve to canonical emails via theidentity_aliasesrows the identity-collector writes. - Webhook deliveries 401-rejected. The
secreton the Confluence webhook registration does not matchWEBHOOK_INGRESS_CONFLUENCE_SECRET. Webhooks created via the API are immutable on the secret field — delete and recreate with the correct secret. - Backfill missing newer pages. The v2 API uses cursor-based pagination; if the cursor token is invalidated mid-walk, Moʻolelo restarts from the last successfully-published record. Wait for the next backfill cycle, or trigger one by toggling mode
webhook→backfill→webhook.

