Cross-Site Issue Sync for Jira
Jira v1.0.0 · Release candidate
Keeps issues synchronised between two independent Jira Cloud sites. The same app is installed on both; the two installations pair with each other and exchange changes directly. There is no hosted backend.
Overview
Two Jira Cloud sites, one app, no broker. Each installation pairs with the other over its own Forge web trigger and they exchange changes directly — everything runs on Forge.
Field mapping is admin-configurable, because two independent sites never share custom field ids, workflows or issue types. Mapping goes through a canonical intermediate form rather than site-to-site directly, so each side points a shared canonical key at its own local field.
Conflict resolution is explicit and visible. The merge is three-way and per field: the ancestor both sites last agreed on, the receiver’s current value, and the sender’s proposed value. Editing different fields of the same issue on both sites merges cleanly; only genuinely contested fields go through the strategy.
Highlights
Pairing without shared credentials
A mutual token exchange over each installation’s web trigger URL. No Jira credentials are shared — each app only ever writes to its own site, using the permissions its own admin granted at install time.
Three independent loop guards
Forge’s ignoreSelf trigger filter; no-op suppression, because after applying a peer’s change the local state matches the agreed base so the next diff is empty; and foreign-origin marks on comments and attachments so they are never sent back where they came from.
Nothing is silently overwritten
The default strategy is manual: neither side changes, and the field is flagged on both issue panels with both values and a Keep-this / Use-theirs choice. Every conflict is logged with both values, whichever strategy applies.
One retry authority
Inbound work is handled synchronously because the sender needs the merge result in the reply. Retries live entirely on the sending side, and the consumer returns an InvocationError so Forge owns backoff and attempt counting.
Terminal failures stay terminal
Retries happen only for plausibly transient failures — network errors, HTTP 429 and 5xx. A rejected token or a field the destination will never accept is written to the pair’s log where an admin can see it, rather than being retried forever.
Testable on a single site
Pair two projects on the same site. The installation holds both ends and talks to its own web trigger, exercising auth, merge and loop prevention without a second site.
How a change travels
Someone edits an issue on Site A. The issue-events trigger (with ignoreSelf) resolves the project’s pairs and pushes a job to a queue — it does no network work, because it has a 25-second limit.
The outbound-sync consumer reads the issue, builds a canonical snapshot, diffs it against the last agreed base, and POSTs the delta to Site B’s web trigger over HTTPS with a bearer token.
Site B’s sync-inbound web trigger verifies the token in constant time, three-way merges the delta, writes to Jira, and returns the new agreed base. Site A stores it and applies any corrections.
Latency: expect 3–15 seconds end to end in normal conditions. This is near-real-time, not real-time. Under retry — peer down or rate limited — it backs off exponentially from 15 seconds up to the platform maximum of 15 minutes.
User guide
Who this is forA Jira administrator on each site. Pairing is a two-person, two-site operation — one creates the invite, the other accepts it.
Install the app on both sites, then pair one project on each. Pairing is a mutual token exchange over Forge web triggers: no Jira credentials are shared, and each app only ever writes to its own site using the permissions its own admin granted at install time.
| Surface | Where to find it |
|---|---|
| The console | Jira → Settings → Apps → Cross-Site Issue Sync, on each site |
| Creating and accepting invites | The Connect a site screen |
| Field mapping, scope filter, token rotation | Sync pairs → Configure |
-
Install on both sites
Install the same version on each site that will form a pair. For a single-site test, install it once and pair two projects within that installation.
-
Create an invite on the first site
Open Connect a site, choose the project, name the site, and click Create invite. The app generates a pair id and a token, reads its own web trigger URL, and encodes them into an invite code.
-
Carry the code over a channel you trust
The invite code contains a live token, so it must travel out of band — email, Slack, a ticket. Three things bound the damage if it leaks: it expires after 24 hours, it is single use (a second handshake is refused once the pair is active, so a leaked code cannot re-point an established pair), and it grants access to one project on one site, nothing else.
-
Accept it on the second site
Open Connect a site, paste the code, choose the local project, and click Accept invite and connect. The second site stores the first site’s URL and token, generates its own token, and posts back. The first site verifies in constant time, records the peer, and marks the pair active.
Afterwards each side holds two tokens: an inbound token it demands of the peer, and an outbound token it presents to the peer.
-
Set the field mapping on each site
Open Sync pairs → Configure on each side. Custom fields need the same canonical key on both sides.
Optionally set a scope filter — JQL narrowing which of the project’s items the pair covers. It is set independently on each site, and each one governs what leaves that site.
-
Test the connection, then backfill
Test connection sends a ping over the real path — DNS, egress, the peer’s web trigger, the token — and reports the round trip plus whether the far side is paused. A pair that has not moved for a day is otherwise indistinguishable from a quiet project.
Sync existing work items starts a backfill of what already exists.
Good to know
- Expect 3–15 seconds end to end in normal conditions: Jira product events reach a Forge trigger in roughly 1–3s, queue dispatch adds 1–5s, and the round trip plus destination writes add another 1–5s. This is near-real-time, not real-time. Under retry it backs off exponentially from 15s to the platform maximum of 15 minutes.
- Retries happen only for plausibly transient failures — network errors, HTTP 429 and 5xx. A rejected token or a field the destination will never accept is terminal and is written to the pair’s log where an admin can see it, rather than being retried forever.
- Revocation is local. Deleting a pair deletes its secrets, so the peer’s token stops verifying immediately — even though their installation still holds it.
- Rotate token issues a fresh token for the peer to present and tells it the new value. The peer is told first, so a failure leaves both sides on the old token with the pair still working, and the old token keeps verifying for 24 hours so nothing in flight is lost.
- Recount work items recomputes link and conflict counts exactly. They are otherwise maintained incrementally and can drift slightly under concurrent syncs.
- The web trigger is public by Forge’s design, so everything protecting it is in the handler: constant-time token comparison, byte-identical answers for an unknown pair and a wrong token (so pair ids cannot be discovered by probing), a 30-minute envelope freshness window, a 5 MB body cap rejected before parsing, and attachments re-checked against this site’s size limit using the measured byte count rather than the sender’s declared size.
- To test without a second site, pair two different projects on one site. The installation holds both ends and talks to its own web trigger, which exercises the whole path — auth, merge, loop prevention. Two projects, not two issues in one project.
If something looks wrong
An invite code will not be accepted.
It expires after 24 hours and is single use. Once a pair is active a second handshake is refused by design — create a fresh invite.
A pair has not moved anything for a while.
Use Test connection. It reports the real round trip and whether the far side is paused, which a quiet project otherwise looks exactly like.
Link or conflict counts look slightly off.
They are maintained incrementally and can drift under concurrent syncs. Recount work items recomputes them exactly.
One item repeatedly fails to sync.
Check the pair’s log. Terminal failures — a rejected token, or a field the destination will never accept — are written there rather than retried forever.
What it does not do
- Share any Jira credential between sites. Each installation writes only to its own site, with the permissions its own admin granted.
- Use OAuth 2.0 (3LO). It authorises a user to an app, which is the wrong shape: sync must keep running when nobody is logged in, and a user’s token would carry that user’s permissions rather than the pair’s.
- Reach beyond one project per pair. An invite grants access to exactly one project on one site.
- Guarantee real-time delivery. It is near-real-time, and the latency budget above is the honest one.
Permissions & scopes
| Scope | Why it's needed |
|---|---|
read:jira-work | Reading the issue, its comments and its attachments to build a canonical snapshot |
write:jira-work | Applying a merged change to the local issue, and creating the counterpart issue |
read:jira-user | Resolving the caller for the admin and panel permission checks |
storage:app | Pair configuration, issue links, the operational log, pairing tokens, and event de-duplication |
External access
| Host | Why |
|---|---|
*.webtrigger.atlassian.app | The paired installation’s Forge web trigger. Declared with inScopeEUD: true — deliberately, because synchronising issues across two independent Jira sites means summaries, descriptions, comments and attachments cross an organisational boundary. |
Traffic goes over HTTPS to *.webtrigger.atlassian.app, which is Atlassian infrastructure; it does not traverse a third-party host. There is no vendor-operated server anywhere in the path.
What data leaves each site
This applies to the paired project only, and only for fields mapped with a direction that permits sending. It is relevant to data residency and is why the manifest declares inScopeEUD: true.
Leaves the site
- Issue summary, description, and any mapped fields (labels, priority, custom fields)
- Status name, as a canonical name rather than an id
- Comment bodies, comment author display names, and creation timestamps
- Attachment file names, MIME types and file contents
- Assignee email address — but only if that field is explicitly mapped, which it is not by default
- Issue keys and issue URLs, so each side can link back
Never leaves the site
- Account ids
- Project or workflow configuration
- Permissions
- Any other project
- Unmapped fields
- Comment visibility restrictions
- Pairing tokens
Data stored
| Record | Key | Contents | Retention |
|---|---|---|---|
| Pair config + field mapping | custom entity | Which projects are paired, the field mapping, direction and conflict strategy | Until the pair is deleted |
| Issue links | custom entity | Local issue key ↔ remote issue key and URL, conflict flag, last synced timestamp | Until the pair is deleted |
| Operational log | custom entity | Level, issue key, message and detail for each sync event | 30-day TTL, so the installation stays inside quota with no cleanup job |
| Pairing tokens | KVS secret store | The inbound token we demand of the peer and the outbound token we present | Until the pair is deleted. Encrypted at rest and not readable through an entity listing, so tokens cannot leak into the admin UI |
| Event de-duplication | plain KVS | Seen-event markers | 7-day TTL — a retry arriving a week later is not a retry |
| Comment/attachment identity | plain KVS, independent keys | Which remote comment or attachment a local one corresponds to | Permanent. Independent keys mean concurrent comment syncs cannot clobber each other the way one JSON blob on the link record would |
Security notes
The pairing handshake
- An invite code carries a live token, so it must travel over a channel you trust. Three things bound the damage if it leaks: it expires after 24 hours; it is single use — once the pair is active a second handshake is refused, so a leaked code cannot re-point an established pair at a different peer; and it grants access to one project on one site, nothing else.
- Tokens are verified in constant time.
- Revocation is local. Deleting a pair deletes its secrets, so the peer’s token stops verifying immediately — even though their installation still holds it.
- Forge does not authenticate web triggers, so the inbound handler verifies the per-pair bearer token itself. That is the single inbound door.
Why not OAuth 2.0 (3LO)
- 3LO authorises a user to an app, which is the wrong shape here: sync must keep running when nobody is logged in, and a user’s token would carry that user’s permissions rather than the pair’s.
- A per-pair token bound to a web trigger is narrower, needs no consent screen, and is revocable from either end.
Why the app acts as itself, and what stops that being a hole
- Sync is driven by product events and peer requests, neither of which carries a user context, so writes use
asApp(). - Because the app’s own permissions are used, resolver entry points check the caller’s permissions explicitly:
ADMINISTERfor configuration andEDIT_ISSUESfor panel actions. Without that, any logged-in user could pair a project with an external site.
Routing correctness
- A single installation can hold both ends of a pair — two projects on one site — so every message names the pair and the role it is addressed to, and routes correctly even when sender and receiver are the same installation.
- Both sites must be on the same app major version for the wire format to line up. The envelope carries
v: 1and mismatches are refused rather than mis-parsed.
Testability of the merge
- The
domain/layer — canonical field conversion and the three-way merge — is pure and has no Forge imports, which is what makes the merge algorithm directly testable. - The receiver computes the merge because it is the only place all three versions of a field exist at once. The reply carries the new agreed ancestor so both sides stay identical without a second round trip.
Known limitations
- Attachments are capped at 3 MB by default. A Forge invocation payload tops out at 5 MB and base64 inflates by a third. Larger files are skipped and logged, not silently dropped. Configurable per pair.
- Product events over 200 kB are not delivered by Forge, so a very large issue update may not fire a trigger. The next smaller update reconciles it, because the diff is computed from full current state rather than from the event payload.
- Custom entity indexes block installation for several minutes after a deploy — they are built asynchronously and
forge deployreturns while indexing continues. - Assignee sync is off by default. Account ids are site-scoped, so matching is by email, which fails when the two sites have different user directories or when profile visibility hides addresses. Unresolved assignees are logged and left unchanged.
- Status mapping needs a reachable transition. If the destination workflow has none from the current status to the target, the move is logged and skipped; the rest of the update still applies.
- Issue type is off by default, because changing type after creation is frequently blocked by workflow or screen configuration.
- Rich text media is stripped. Inline images in a description reference the source site’s media store and would render broken. Real attachments sync as files instead.
- Comment authorship cannot be preserved — the original author has no account on the destination. Comments are posted by the app with an attribution line.
- Deletes do not propagate. Deleting an issue on one site leaves the other in place; the stale link is cleaned up on next contact.
- The issue panel shows the first pair when a project belongs to several.
- Field mapping is not exchanged automatically after pairing. The acceptor inherits the initiator’s canonical keys at pairing time; later changes must be mirrored by hand on both sites. Divergence shows up as
skippedfields in the logs.
Release notes
Version history for Cross-Site Issue Sync for Jira. The most recent release is listed first.
-
v1.0.0 Current
Initial release.
What it does
- Two independent Jira Cloud sites pair over their own Forge web triggers via a mutual token exchange, with no hosted backend and no shared Jira credentials.
- Admin-configurable field mapping through a canonical intermediate form, with absolute direction values (
aToB,bToA,both,none) so both installations read the same stored value identically. - Three-way, per-field conflict resolution with
manualas the default: nothing is silently overwritten, and contested fields are surfaced on both issue panels with a Keep-this / Use-theirs choice. - Issue links stored on both sides and surfaced on an issue panel; comments and attachments sync with foreign-origin marks.
- A per-pair operational log with a 30-day TTL, surfaced in the admin UI.
Security
- Invite codes expire after 24 hours, are single use, and grant access to one project on one site.
- Tokens live in the KVS secret store and are verified in constant time. Deleting a pair revokes the peer immediately.
- Resolver entry points check
ADMINISTERfor configuration andEDIT_ISSUESfor panel actions, because sync itself runs as the app. - The wire envelope is versioned and mismatches are refused rather than mis-parsed.
Loop prevention
- Forge’s
filter.ignoreSelfstops the platform invoking the trigger for the app’s own writes. - No-op suppression catches loops
ignoreSelfcannot — for example a Jira automation rule reacting to the app’s write, which is a different actor. - Comments and attachments created from a peer are recorded as foreign and never sent back to the site they came from.
Get it & contact
Vendor
AryonForge
Support
Security
App ID
ari:cloud:ecosystem::app/44872b46-a8af-4be4-a072-ecab99019dbd