Skip to content

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

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:

  1. Construct the consent URL per Atlassian's OAuth 2.0 (3LO) documentation using your Client ID, redirect URL, and the scopes above.
  2. Open it in a browser, sign in as the Confluence admin, and approve the install.
  3. Atlassian redirects to your redirect URL with an authorization code parameter.
  4. Exchange the code for an access token + refresh token via POST https://auth.atlassian.com/oauth/token. The response carries refresh_token — that's what you store.

Your Atlassian Cloud has a unique Cloud ID that the API uses for routing. Find it via:

sh
curl -H "Authorization: Bearer <access_token_from_the_exchange>" \
  https://api.atlassian.com/oauth/token/accessible-resources

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

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

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

sql
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-123 Jira keys, owner/repo#42 GitHub refs, *.atlassian.net/wiki/... page URLs, github.com/.../issues|pull/... URLs, and markdown links to other Confluence pages) are extracted into refers_to graph 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:

  1. 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.
  2. 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_grant errors, redo the consent flow (Step 2) and update the refresh_token field in connector_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 match on /restriction calls. The classic scope read:confluence-content.permission is missing from the app. Add it under "Classic scopes → Confluence API" in the developer console and re-authorize.
  • Unauthorized; scope does not match on /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:confluence is granted (the space-level ACL fetch needs it), and confirm the Atlassian identity source is also configured (see Identity catalog) — accountId values on ACL events resolve to canonical emails via the identity_aliases rows the identity-collector writes.
  • Webhook deliveries 401-rejected. The secret on the Confluence webhook registration does not match WEBHOOK_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.

Moʻolelo — self-hosted federated enterprise knowledge platform.