Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Jul 8, 2026

Making your app compatible with read-only mode

Audience — App developers and Marketplace vendors. This guide explains how to detect Jira Data Center read-only mode and make your app respect it.

When a Jira Data Center administrator enables read-only mode, Jira blocks write operations that flow through its project-permission model. Apps are not automatically compatible: if your app writes data through its own code paths — managers, direct SQL, background jobs, event listeners, webhooks, or user impersonation — those writes will continue unless you guard them yourself.

This guide shows you how to detect read-only mode and where to add guards so your app behaves correctly during a customer's maintenance or migration window.

Why your app needs to opt in

Read-only mode is enforced in one place: the project-permission check in DefaultPermissionManager. The rule is simple — for a non-system-administrator, a project permission is granted only if it is marked as available in read-only mode (reads are; writes aren't).

System administrators are exempt from read-only mode. Keep that in mind: the bypass is meant for a human admin deliberately performing maintenance, not for automated code that happens to run as an admin actor.

That means:

  • ✅ If your app writes through a project-permission check (for example, it calls IssueService/PermissionManager and honours EDIT_ISSUES before writing), it is already blocked for non-admins when read-only mode is on. You get compatibility for free.
  • ❌ If your app writes without a project-permission check — background jobs, scheduled tasks, event listeners, incoming webhooks, direct manager or SQL calls, or code that impersonates another user — the read-only gate never sees it. You must add your own guard.

How to detect read-only mode

There are three supported ways to check the current state. Use whichever fits your extension point.

1. REST (front end, remote apps, integrations)

1
2
GET /rest/api/2/readonly-mode
1
2
{
  "enabled": true,
  "message": "Jira is currently in read-only mode for maintenance.",
  "endTime": "2026-06-30T18:00",
  "timeZone": "Asia/Beirut"
}

This endpoint is available to any licensed user, so front-end code and external integrations can call it to adapt their behaviour.

2. Application property (server-side, in-process)

1
2
applicationProperties.getOption(APKeys.JIRA_OPTION_READ_ONLY_MODE); // true when read-only mode is on

The property key is jira.option.readonly.mode. This is the cheapest check for back-end code.

3. React to changes with an event

Jira publishes com.atlassian.jira.event.ReadOnlyModeChangedEvent whenever an admin turns read-only mode on or off. Subscribe if your app needs to react — for example, to pause a scheduler, flush a cache, or update a status indicator.

1
2
@EventListener
public void onReadOnlyModeChanged(ReadOnlyModeChangedEvent event) {
    if (event.isEnabled()) {
        // read-only mode was turned ON — pause writes
    } else {
        // read-only mode was turned OFF — resume normal operation
    }
}

The event is @ExperimentalApi. Guard against API changes and don't rely on ordering relative to other listeners.

Where to add guards

Audit every place your app can write. Typical surfaces that bypass the read-only gate and need an explicit check:

  • REST endpoints that create or modify data (especially any annotated for unrestricted or token-only access).
  • Background jobs and scheduled tasks. Note that scheduled jobs run with no logged-in user — the sysadmin bypass is meaningless there.
  • Event listeners that write in response to Jira events.
  • Incoming webhooks from external systems (no user context, token-authenticated).
  • Code that impersonates a user before writing (see the worked example below).
  • Direct manager / service / SQL calls that skip PermissionManager.
  • Irreversible external side effects — e.g. consuming email from a mailbox, calling out to a third-party system. Guard these first; they can't be rolled back when read-only mode ends.

Two guard patterns

There are exactly two patterns. Never confuse them.

Pattern 1 — Interactive (with bypass)Pattern 2 — Automated (unconditional)
Use forREST write endpoints and admin UI actions triggered by a logged-in humanExecution pipelines, scheduled jobs, incoming webhooks, event listeners
Sysadmin bypass?Yes — a system administrator may proceedNo — block regardless of actor
When to checkBefore any data modification, while the thread user is the logged-in humanBefore any impersonation happens, at the entry point

Pattern 1 — interactive check (with sysadmin bypass)

1
2
boolean isReadOnlyModeActiveForCurrentUser() {
    if (!applicationProperties.getOption(APKeys.JIRA_OPTION_READ_ONLY_MODE)) {
        return false;
    }
    // Check SYSTEM_ADMIN, not ADMINISTER — plain Jira admins must still be blocked.
    return !globalPermissionManager.hasPermission(
            GlobalPermissionKey.SYSTEM_ADMIN,
            authenticationContext.getLoggedInUser());
}

Use SYSTEM_ADMIN, not ADMINISTER. A plain "Jira admin" (ADMINISTER) must be blocked in read-only mode — only system administrators are exempt. Reusing an existing isAdmin()/isJiraAdmin() helper is a common mistake.

Call this at the top of each mutating REST method and return 503 Service Unavailable (or your API's equivalent) when it returns true.

Pattern 2 — unconditional block (automated paths)

1
2
if (applicationProperties.getOption(APKeys.JIRA_OPTION_READ_ONLY_MODE)) {
    // No actor check — automated writes are blocked regardless of who configured them.
    return; // or reject the request (e.g. 503) for an inbound webhook
}

Use this for anything that runs without a deliberate human action: scheduled jobs, event-driven execution, and unauthenticated webhooks.

A worked example: automation-style execution

If your app runs rules or actions as a configured actor (impersonation), the guard placement matters. Consider a pipeline that does:

1
2
event → executeAs(actorAccountId, runnable) → runRule → write to Jira

After executeAs(), the thread's user is the rule's actor — not the human who triggered it. If you check read-only mode inside runRule() and the actor happens to be a sysadmin, the sysadmin bypass would incorrectly let the write through.

Correct approach: apply the unconditional guard (Pattern 2) before impersonation starts — at the top of the execution entry point, before executeAs().

Design principle: the sysadmin bypass is for deliberate, interactive actions. A rule's actor being a sysadmin is configuration set days ago — it does not represent an admin consciously choosing to write during a read-only window. Automated execution must be blocked unconditionally, regardless of the actor's identity.

The same reasoning applies to scheduled jobs (which run with a null user) and to unauthenticated inbound webhooks (which have no user at all): there's no one to "bypass", so the block must be unconditional.

Front-end guidance

A back-end guard that silently returns 503 is confusing UX. If your app has UI, also make it read-only-aware so users understand why an action failed:

  • On page load, call GET /rest/api/2/readonly-mode (or read a context flag you inject) and, when enabled is true:
    • Hide or disable create/edit/save controls.
    • Show a banner or inline message explaining the instance is in read-only mode. You can surface the admin-supplied message and endTime.
  • If your UI is an embedded SPA (for example, in an iframe), pass an isReadOnly flag in your context payload so the front end can render correctly instead of letting a save silently fail at the REST layer.

The REST guards are your actual enforcement; the front-end changes just prevent confusing silent failures.

Testing your app

Cover these scenarios with read-only mode toggled on and off. Impersonation makes the actor identity the tricky part:

  • Read-only ON, automated/background path, actor is a sysadmin → still blocked (Pattern 2 fires before impersonation).
  • Read-only ON, automated path, actor is a non-sysadminblocked.
  • Read-only ON, interactive REST call, logged-in human is a sysadminallowed (Pattern 1 bypass).
  • Read-only ON, interactive REST call, logged-in human is a plain Jira admin (not sysadmin) → blocked.
  • Read-only ON, inbound webhook (no logged-in user) → blocked unconditionally.
  • Read-only OFF → everything proceeds normally.

Checklist

  • Every write path in the app is inventoried (REST, jobs, listeners, webhooks, impersonation, direct DB).
  • Interactive write endpoints use Pattern 1 (sysadmin bypass, SYSTEM_ADMIN not ADMINISTER).
  • Automated/background/webhook paths use Pattern 2 (unconditional block, guard placed before any impersonation).
  • Irreversible external side effects are guarded first.
  • UI hides/disables write controls and explains read-only mode.
  • (Optional) The app listens for ReadOnlyModeChangedEvent to react to state changes.
  • All six test scenarios above pass.

Rate this page: