Skip to content

Connecting Jira Cloud ​

Moʻolelo indexes issues (and their comments) from the Jira Cloud projects you list. It uses Jira's v3 REST API for backfill and Jira's webhook system for steady-state updates.

If you already have Confluence connected on the same Atlassian site, the OAuth app you registered for Confluence works for Jira too — you only need to add the Jira scopes during the consent flow.

Prerequisites ​

  • An Atlassian Cloud instance with at least one Jira project.
  • An Atlassian admin who can create an OAuth 2.0 (3LO) app and install it (or extend an existing one).
  • Network reachability: Moʻolelo's webhook endpoint must be reachable from Atlassian's outbound webhook IP ranges.

Step 1 — create (or extend) an OAuth 2.0 (3LO) app ​

In the Atlassian developer console, open your existing app (or create a new one) and add these Jira scopes. Atlassian splits its scope catalog into Granular and Classic tabs; both flavors are needed because Moʻolelo calls a mix of v3 (granular) and classic endpoints.

Granular scopes (under "Permissions → Jira API"):

  • read:issue:jira — issue reads.
  • read:project:jira — project metadata + role lookups.
  • read:comment:jira — comment surfaces.
  • read:user:jira — assignee + reporter principal lookups.
  • read:group:jira — group lookups.
  • read:issue-meta:jira — field metadata.
  • read:issue-security-level:jira — issue-security-scheme reads.

Classic scopes (under "Classic scopes → Jira platform REST API"):

  • read:jira-work — broad read on the classic Jira REST API. Required for endpoints whose granular equivalents Atlassian hasn't shipped yet.
  • read:jira-user — required. The classic /rest/api/3/users/search endpoint Moʻolelo calls during identity backfill only accepts this name; the similarly-named granular read:user:jira does NOT cover it. Without it consent succeeds but every user-catalog call 401s with "scope does not match".

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 app's "Settings" tab.
  • The redirect URL configured for the OAuth flow.

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 Atlassian 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 Jira Cloud you're connecting and copy its id field. If you already grabbed this for Confluence on the same Atlassian site, you can re-use it.

Step 3 — register the connector ​

From the admin UI, open Connectors → New connector, pick Jira Cloud, and fill in the form. Behind the scenes that writes a connector_configs row equivalent to:

sql
INSERT INTO connector_configs (
  tenant_id, source, instance, mode, resources, raw_config, updated_at
) VALUES (
  'default',
  'jira',
  'acme',
  'backfill',
  '["ENG", "OPS"]'::jsonb,
  jsonb_build_object(
    'cloud_id', '<your cloud id from step 2>',
    'site_url', 'https://acme.atlassian.net',
    'oauth', jsonb_build_object(
      'client_id',     '<from step 1>',
      'client_secret', '<from step 1>',
      'refresh_token', '<from step 2>'
    )
  ),
  now()
);
  • resources is the list of Jira project keys to index. Leave it empty to ingest every project the OAuth app can see — useful for tenants that want a single connector to cover their entire Jira.
  • mode starts as backfill. Once the first walk completes, flip the row to webhook to receive real-time updates (see Step 5).
  • instance is a free-text tag; use your Atlassian site slug (e.g. acme). The webhook URL uses it (/webhooks/jira/<instance>).

The connector picks the change up via Postgres LISTEN/NOTIFY within seconds.

Step 4 — watch the backfill ​

Backfill walks POST /rest/api/3/search/jql (the token-paginated successor to /search) one project at a time, ordered by updated ASC. Progress shows up in:

  • The connector's logs (backfill project complete per project).
  • The connector_cursors table — each row's updated_at advances as issues land.

A medium-sized Jira (≤100 projects, ~50k issues) typically backfills in under an hour. Very large tenants (>1M issues) need an overnight pass.

Step 5 — register the webhook ​

Jira Cloud's webhook administration lives at Settings → System → WebHooks in the Atlassian admin UI (admin-only; not configurable via the OAuth app).

Create a webhook with:

  • URL: https://<your-moolelo-host>/webhooks/jira/<instance> — the same <instance> value you used in the connector_configs row.
  • Secret: a random 32-byte string. Save it; you'll need it in Step 6.
  • Events: at minimum, Issue created, Issue updated, Issue deleted. Optionally Comment created/updated/deleted to ingest comments separately from issues.
  • JQL filter: leave blank to match all projects, or scope it to the projects you listed in resources.

Atlassian signs each delivery with HMAC-SHA256 over the raw body, in the X-Hub-Signature header (same scheme as Confluence; note this is X-Hub-Signature, not X-Hub-Signature-256).

Step 6 — wire the signing secret ​

The webhook-ingress service validates every incoming webhook against the secret you set above. Mount it via the environment variable on the webhook-ingress deployment:

yaml
env:
  WEBHOOK_INGRESS_JIRA_SECRET: "<the secret from step 5>"

The signature lookup is per-provider; if you run multiple Atlassian sites you can extend the SecretLookup callback to return different secrets per <instance>.

Once the secret is in place, flip the connector_configs.mode column to webhook:

sql
UPDATE connector_configs SET mode = 'webhook', updated_at = now()
WHERE tenant_id = 'default' AND source = 'jira';

The connector switches modes within seconds and starts publishing webhook-driven records.

What gets indexed ​

Record typeSourceNotes
issue/rest/api/3/search/jql (backfill) + jira:issue_* webhooksFull issue JSON including embedded comments. Indexed under jira:issue:<KEY>.
commentcomment_* webhooksComments arrive both inside the issue payload (during backfill) and as standalone webhook events. The connector keeps both paths idempotent.

The connector ignores worklogs, attachments, sprints, and dashboards in v1. If you need them, raise the gap — adding them is a small follow-on rather than an architectural change.

ACL ​

Per-issue read permissions emit one ACL change event with entity_id = "jira:issue:<KEY>". The principal set is the union of every actor in every project role on the issue's project — i.e., "anyone with a role on the project". Issue-security-scheme overrides (the per-issue restriction that overlays project roles) are not enforced in v1; if your projects rely heavily on issue-security schemes you should hold off until that lands.

The downstream acl-projector resolves user accountId values to canonical identities via the identity_aliases table populated by identity-collector. Wire up the Atlassian identity source (via the Identity sources admin page) so user lookups resolve cleanly.

Disabling or removing ​

To pause Jira ingestion, set mode = 'disabled'. To remove it entirely, delete the row. Already-indexed records remain in Iceberg and Vespa until you explicitly purge them — Moʻolelo doesn't auto-delete data when you change connector configuration.

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