Jira User License Optimizer
Jira v1.0.0 · Release candidate
Finds inactive Jira seats, applies configurable deactivation policy, governs its own exception list, and produces enterprise-grade audit evidence — without ever deleting a user.
Overview
Built around five guarantees, each of which is a property of the code rather than a promise in a document.
1. Users are never deleted. There is no delete action type, no code path that calls the account-deletion endpoint, and no setting that enables one. The app can only suspend directory access, remove a group membership, or revoke a product role — all reversible.
2. Every change is reversible. A pre-change snapshot is captured before each action, and execution refuses to proceed for any account whose snapshot could not be written.
3. Every change is auditable. The audit log is append-only; the repository exposes append, page and listAll and nothing else.
4. Safe mode is the default. Every plan starts as a dry run. Live execution requires an explicit submit, an approval from a different person, and an explicit execute.
5. The ignore list is governed. Entries have an owner, an approver, a reason, an expiry and a review cadence, and move through an enforced lifecycle.
Highlights
Approval is a genuine second pair of eyes
requireDifferentApprover rejects self-approval of an execution plan, and ignore entries cannot be approved by their creator.
Checkpointed, queue-driven scanning
An organization of 100,000 users cannot be processed in one invocation. Each page persists a checkpoint before enqueuing the next, so an interrupted run resumes rather than restarting. A page that fails is recorded, audited, skipped, and the scan continues — losing 50 accounts must not cost the other 99,950.
Rules with versioning and priority
A conservative default rule set is seeded on first use. Conditions, priority ordering, disabled rules and validation are all covered by the test matrix.
Custom cron on a platform without it
Forge scheduled triggers offer only five-minute, hourly, daily and weekly intervals. An hourly heartbeat evaluates stored schedules against a five-field cron parser instead, so administrators get cron semantics at hourly resolution.
Eleven normalised entities, indexed for real queries
Indexes are chosen from the actual query patterns. Because a query can range-scan only one index, the query planners pick the most selective partition for the supplied filters and apply the rest as post-index filters.
Recommendation engine with confidence and savings
Service-account scoring, a confidence value, and the seat cost each recommendation would recover.
Two storage constraints that shaped the schema
The Forge entity store types integer as 32-bit signed, so every timestamp in this app is epoch seconds, never milliseconds. A single module centralises that so it cannot drift.
A query can range-scan only one index. The scan-user and audit query planners therefore pick the most selective partition available for the supplied filters and apply everything else as post-index filters — and the Users tab states plainly which filters are server-side and which are not.
Scan history is pruned to the most recent N scans (default 12) because each scan stores one row per user.
User guide
Who this is forJira administrators for setup, plus whoever they grant a role to. Executing a plan needs an approval from a different person, so it is never a one-person job.
Setup is five steps, and the first one is a credential the app cannot mint for itself: last-active data lives only in the Atlassian Organization API. Until it is supplied, there is nothing to scan.
| Surface | Where to find it |
|---|---|
| The dashboard | Jira → Settings → Apps → Jira User License Optimizer |
| Credential and roles | The Setup tab |
| Scans | The Overview tab |
| Plans, approval, execution, rollback | The Cleanup tab |
| The API key itself | admin.atlassian.com → Settings → API keys; the organization id is in the admin URL (admin.atlassian.com/o/<org-id>) |
-
Connect the organization
On the Setup tab, enter your organization id and an organization API key. The key is validated against the live API before it is stored, and is written to Forge’s encrypted secret store.
It is never returned to the browser: only
OrgApiClientreads it, and the UI only ever sees a masked hint such as****6789. -
Review the rules
A conservative default rule set is seeded on first use. Rules carry conditions, a priority ordering and versioning; disabled rules and validation are covered by the test matrix.
-
Run a scan
From the Overview tab. Progress is polled and the UI never blocks — scanning is checkpointed and queue-driven, so an organization of 100,000 users is processed a page at a time rather than in one invocation.
A page that fails is recorded, audited, skipped, and the scan continues. Losing 50 accounts must not cost the other 99,950.
-
Generate a dry run and read the plan
On the Cleanup tab. Every plan starts as a dry run — that is the default, not a mode you have to remember to choose.
A pre-change snapshot is captured before each action, and execution refuses to proceed for any account whose snapshot could not be written. No snapshot, no change.
-
Grant roles, so approval is a genuine second pair of eyes
On the Setup tab. Live execution requires an explicit submit, an approval from a different person, and then an explicit execute.
requireDifferentApproverrejects self-approval of an execution plan, and an ignore entry cannot be approved by whoever created it. -
Govern the ignore list rather than accumulating a skip list
Entries carry an owner, an approver, a reason, an expiry and a review cadence, and move through an enforced lifecycle. Matching, per-status enforcement, the full lifecycle and the self-approval refusal are all covered by tests.
Good to know
- Users are never deleted. There is no
deleteaction type, no code path that calls the account-deletion endpoint, and no setting that enables one. The app can only suspend directory access, remove a group membership, or revoke a product role — all reversible. - The audit log is append-only by construction: the repository exposes
append,pageandlistAll, and nothing else. - Execution and rollback are batched at 25 items, using the same checkpointed queue pattern as scanning.
- Scheduling has hourly resolution. Forge scheduled triggers offer only five-minute, hourly, daily and weekly intervals, so an hourly heartbeat evaluates your stored schedules against a five-field cron parser. You get cron semantics; you do not get sub-hour precision.
- Some filters are applied after the query, not by it. Free-text, product and group filters are not indexable, so they are applied to the returned page — and the Users tab says so rather than implying a server-side filter.
- Exports are capped at 25,000 rows and report
truncatedwhen they hit it, because an unbounded export would fail slowly. CSV exports neutralise formula-injection prefixes (=,+,-,@). - Scan history is pruned to the most recent 12 scans by default, because each scan stores one row per user.
- Per-user enrichment is budgeted to the first 40 accounts per page; the rest use list-response summary data. Project-role enumeration costs projects × roles requests, so it is built once per scan, shared, and bounded to 100 projects.
If something looks wrong
The app cannot tell you who is inactive.
Check the organization connection on the Setup tab. No Jira product scope exposes last-active data — it comes only from the Organization API, using a key an organization admin creates at admin.atlassian.com.
A “no comments” signal is never populated on a service account.
Jira Cloud JQL has no comment-author predicate, so that signal cannot be collected. It is reported as unknown rather than zero, and the scorer uses issue authorship as the nearest supported proxy.
A plan cannot be executed.
Live execution needs three separate acts: submit, approval by a different person, then execute. Self-approval is refused server-side, not just hidden in the UI.
An export looks incomplete.
Exports cap at 25,000 rows and set a truncated flag when they do. Narrow the filters and export again.
What it does not do
- Delete a user account, under any setting.
- Act without a reversible snapshot — execution refuses to proceed for any account whose snapshot could not be written.
- Let one person both propose and approve a live plan.
- Return the organization API key to the browser. The UI only ever sees a masked hint.
Permissions & scopes
| Scope | Why it's needed |
|---|---|
storage:app | Eleven custom entities: scans, users, plans, actions, rules, ignore entries, schedules, audit and settings |
read:jira-user | Resolving the calling user and the accounts under review |
read:jira-work | Issue authorship, used as the nearest supported proxy for the activity signals Jira does not expose directly |
manage:jira-configuration | Reading project role membership, which is one of the billable paths a seat can hold |
External access
| Host | Why |
|---|---|
api.atlassian.com | The Atlassian Organization API (/admin/**) and User Management API (/users/**) share this host. Declared as a Forge remote rather than a bare egress string because the plain-string form is deprecated. |
Requests carry Atlassian account IDs, display names and email addresses. The destination is Atlassian’s own admin API, so that data does not leave the Atlassian trust boundary. The credential used is an organization API key the administrator supplies in-app, not Forge’s own.
Data stored
| Record | Key | Contents | Retention |
|---|---|---|---|
| Organization API key | Forge encrypted secret store | The credential itself, validated against the live API before it is stored | Until replaced. Only the API client reads it; the UI sees a masked hint such as ****6789 |
| Scan and scan-user entities | custom entities | Per-user classification, activity signals, billable paths and recommendation output | Pruned to the most recent 12 scans by default |
| Plan and action entities | custom entities | Proposed and executed actions, their pre-change snapshots and their outcomes | While the app is installed |
| Audit log | append-only entity | Every scan, plan, approval, execution, rollback and governance event | Append-only; the repository has no update or delete method |
| Rules, ignore entries, schedules, settings | custom entities | Governance configuration and its lifecycle state | While the app is installed |
Security notes
Credential handling
- The organization API key lives in Forge’s encrypted secret store (
kvs.setSecret). Only the Organization API client reads it. No resolver returns it; the UI only ever sees a masked hint. - The logger redacts any field whose key matches
api_key|secret|token|password|authorization|bearer|credentialbefore writing. - Non-
AppErrorthrowables are reported to the browser as an opaqueINTERNALerror, so upstream messages cannot leak account data or credentials.
Authorization
- RBAC is enforced at the resolver boundary. The UI filters tabs and buttons purely for usability; every mutating resolver re-checks independently.
- Approval enforces separation of duties: an execution plan cannot be self-approved, and an ignore entry cannot be approved by its creator.
- No business logic lives in UI components or in resolvers. Resolvers resolve identity, check one permission, and delegate.
Destructive-action safety
- There is no delete action type and no code path that calls the account-deletion endpoint. Every action the app can take — suspend directory access, remove a group membership, revoke a product role — is reversible.
- A pre-change snapshot is captured before each action, and execution refuses to proceed for any account whose snapshot could not be written.
- Execution and rollback are queue-driven in batches of 25, with the same checkpoint semantics as scanning.
Output safety
- CSV exports neutralise spreadsheet formula-injection prefixes (
=,+,-,@). - Exports are capped at 25,000 rows and report
truncatedrather than failing slowly on an unbounded set.
Testing
- 86 tests across four suites: the rule engine’s condition matrix and priority ordering, the ignore engine’s matching and full lifecycle including self-approval refusal, the recommendation engine’s scoring and savings, and an end-to-end lifecycle test covering scan → plan → approve → execute → rollback with RBAC and audit immutability.
- The Forge SDK is replaced by in-memory mocks. The KVS mock implements real index semantics from a catalogue mirroring the manifest, so a test exercises the same partition and range behaviour the platform enforces rather than passing on a query the platform would reject.
Known limitations
- Forge scheduled triggers have no cron. Custom schedules are evaluated by an hourly heartbeat against a five-field cron parser, so effective resolution is one hour.
- Jira Cloud JQL has no comment-author predicate, so the “no comments” service-account signal cannot be collected. The scorer treats it as unknown rather than zero, and uses issue authorship as the nearest supported proxy.
- Free-text, product and group filters are not indexable and are applied to the returned page. The Users tab states this rather than implying a server-side filter.
- Exports are capped at 25,000 rows and report when they were truncated.
- Project-role enumeration costs projects × roles requests, so it is built once per scan and shared, bounded to 100 projects.
- Per-user enrichment is budgeted to the first 40 accounts per page; the rest use list-response summary data.
Release notes
Version history for Jira User License Optimizer. The most recent release is listed first.
-
v1.0.0 Current
Initial release.
What it does
- Inactive-seat detection across the organization, with a billable-path breakdown per user.
- A configurable, versioned rule engine with priority ordering and a conservative seeded default set.
- A governed ignore list with owner, approver, reason, expiry and review cadence, moving through an enforced lifecycle.
- Dry-run plans, a separate-person approval step, live execution, and rollback — all queue-driven and checkpointed.
- An append-only audit log, CSV export with formula-injection neutralisation, and a recommendation engine that reports confidence and expected savings.
Safety
- No deletion is possible: there is no delete action type and no code path to the account-deletion endpoint.
- Execution refuses to proceed for any account whose pre-change snapshot could not be written.
- Self-approval is rejected for both execution plans and ignore entries.
- The organization API key is validated before storage, written to the encrypted secret store, redacted from logs, and never returned to the browser.
Platform work
- Epoch seconds throughout, because the entity store’s
integeris 32-bit signed. - Query planners that select one range index and apply remaining filters after it, because the platform allows only one.
- An hourly heartbeat plus an in-app cron parser, because Forge scheduled triggers have no cron interval.
- Two scheduled triggers used of the platform’s five; the weekly digest self-paces inside the daily one.
Get it & contact
Vendor
AryonForge
Support
Security
App ID
ari:cloud:ecosystem::app/40570503-d6b6-4883-a116-f5bfcc1a7400