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/searchendpoint Moʻolelo calls during identity backfill only accepts this name; the similarly-named granularread:user:jiradoes 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.
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 Atlassian 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 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:
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()
);resourcesis 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.modestarts asbackfill. Once the first walk completes, flip the row towebhookto receive real-time updates (see Step 5).instanceis 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 completeper project). - The
connector_cursorstable — each row'supdated_atadvances 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 theconnector_configsrow. - Secret: a random 32-byte string. Save it; you'll need it in Step 6.
- Events: at minimum,
Issue created,Issue updated,Issue deleted. OptionallyComment created/updated/deletedto 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:
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:
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 type | Source | Notes |
|---|---|---|
issue | /rest/api/3/search/jql (backfill) + jira:issue_* webhooks | Full issue JSON including embedded comments. Indexed under jira:issue:<KEY>. |
comment | comment_* webhooks | Comments 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.

