AryonForge
v1.0.0 · Release candidate

User License Cleanup & Dashboard for Bitbucket

Bitbucket   v1.0.0 · Release candidate

Finds inactive Bitbucket seats, explains exactly which grant keeps each one billable, and refuses to bulk-action anything that looks like CI.

Bitbucket · Forge app 1 external host No sale of customer data Workspace settings
01

Overview

Most user-cleanup apps stop at “here is a list of people who have not logged in — tick the boxes to deactivate them”. That list is the easy half of the problem, and acting on it blindly is how you break a production pipeline.

Billable-path clarity. For every user the app shows which access path keeps the seat billable — a direct repository grant, a project grant, an inferred group grant, or plain workspace membership — and, critically, whether the app can remove it or whether you have to do something in Atlassian admin. A seat only frees up when every path is gone, so a list of names without paths tells you nothing actionable.

Bitbucket-specific service-account safety. In Bitbucket, a large share of the busiest accounts are CI identities authenticating with app passwords or API tokens. Those never register as interactive product activity, so by the standard “last login” measure they look completely dead. This app subscribes to push and pull-request events so it can tell “nobody is using this seat” apart from “a robot is using this seat hard”.

02

Highlights

Six tabs, one honest story

Overview (including which data sources actually worked), Users with full billable-path drill-down, Excluded showing which rule or heuristic spared each user, Cleanup with a Safe/Live toggle and typed confirmation, Runs & audit, and Settings.

Safe mode produces the same plan object Live executes

There is no separate code path. Read the plan, then read the Skipped by safety rails list underneath it — that list is the app telling you what it refuses to touch, and why.

Git activity from events, not history

Push and pull-request product events cost nothing and are recorded from installation onwards. No commit or pull-request history is ever read — that approach cost two API calls per repository per run and could not complete inside Bitbucket’s hourly rate limit on a large workspace.

Scans that do not scale with repository count

Inactivity comes from the workspace permissions endpoint, which paginates by user — one call per hundred members whether the workspace holds ten repositories or a hundred thousand. Repository detail is a background index a scan never waits on.

Exact restore

Every applied action records the permission level it removed, so “Restore all applied changes” is an exact replay. The app never deletes an account and never performs an irreversible operation.

Quarantine, not silent exclusion

A likely service account is quarantined from bulk actions rather than excluded silently, because a wrong auto-exclusion is as bad as a wrong revocation. Confirm ownership, then add an explicit exclusion — explicit exclusions survive threshold changes; heuristics do not.

03

How to use it without breaking CI/CD

Bitbucket is not Jira. Revoking the wrong seat here does not inconvenience someone — it fails builds, blocks deployments and can take a release train down. The intended order:

1. Never run Live before a Safe-mode pass. Read the plan, then read the skipped-by-safety-rails list underneath it.

2. Treat every “likely service account” as a real finding. Confirm ownership, then add an explicit exclusion.

3. Give the git-activity signal time to accumulate. On a fresh install that signal is empty, so lean on the quarantine list rather than on the safety window.

4. Keep the safety window at 30 days or more. Any push or PR inside the window makes a user un-flaggable regardless of login history. A nightly job that only runs monthly needs a window longer than its cadence.

5. Leave scheduled scans on “report only”. Auto-apply exists, is opt-in, and still honours every rail — but nobody reviews the plan first.

6. Know the restore path before you need it. Use it the moment a build starts failing; investigate afterwards.

04

User guide

Who this is forBitbucket workspace administrators. The organization API key that unlocks inactivity detection has to be created by an organization admin, who may be someone else.

Bitbucket is not Jira. Revoking the wrong seat here does not inconvenience someone — it fails builds, blocks deployments and can take a release train down. The setup order below exists for that reason, and step three is the one that protects your pipelines.

SurfaceWhere to find it
The dashboardWorkspace settings → Forge Apps → User License Cleanup
The organization API keyThe Settings tab. Create the key at admin.atlassian.comSettings → API keys as an organization admin
ExclusionsThe Excluded tab
Plans, apply and restoreThe Cleanup tab
  1. Create the organization API key

    Last-login data does not come from Bitbucket’s API. It comes from the Atlassian Admin (Organizations) API, which uses a credential Forge cannot mint for itself.

    At admin.atlassian.com → Settings → API keys → Create API key, give it a name and an expiry, then copy the key and the organization id — the key is shown once.

  2. Store it in the app

    Workspace settings → Forge Apps → User License Cleanup → Settings. Paste the key (the org id is optional — leave it blank to auto-detect) and choose Verify and save.

    The key is validated against the API before it is stored, is written with Forge’s encrypted secret store, is never returned to the browser, and is only ever sent to api.atlassian.com. Only the last four characters are shown back to you, so you can confirm which key is installed.

    The key is optional to install but required for inactivity detection. Without it the app still maps billable paths and flags service accounts; it just cannot tell you who is idle.

  3. Configure exclusions before your first live run

    This is the step that protects your pipelines. On the Excluded tab, add a rule for every CI, bot and integration account you know about.

    Prefer a user exclusion (exact account id) or a pattern exclusion for anything you cannot afford to break. Explicit exclusions survive threshold changes; heuristics do not.

  4. Run a scan and read the data-source notes first

    Run scan now on the toolbar. The scan runs in the background and results appear as batches complete.

    Read the Overview tab’s honest report on which data sources actually worked before trusting any number on the page.

  5. Do a Safe-mode pass, then read what was skipped

    Safe mode produces the exact same plan object that Live mode executes — there is no separate code path. Read the plan, then read the Skipped by safety rails list underneath it. That list is the app telling you what it refuses to touch, and why.

    Never run Live mode before a Safe-mode pass.

  6. Treat every “likely service account” as a real finding

    The heuristic quarantines them from bulk actions rather than excluding them silently, because a wrong auto-exclusion is as bad as a wrong revocation. Confirm ownership for each, then add an explicit exclusion.

  7. Apply, and know the restore path before you need it

    Live apply requires a typed confirmation. Every applied action records the permission level it removed, so Restore all applied changes on the Cleanup tab is an exact replay.

    Use it the moment a build starts failing; investigate afterwards. The app never deletes an account and never performs an irreversible operation — the worst case is a revoked permission grant, one button away from being restored.

Good to know

  • Give the git-activity signal time to accumulate before your first cleanup. Push and pull-request events are the only signal separating a token-driven CI account from an abandoned human account, and they are collected from installation onwards — no history is read. On a fresh install that signal is empty, so lean on the quarantine list rather than on the safety window. Do not act on the first scan of a fresh install.
  • Keep the safety window at 30 days or more. Any push or PR inside the window makes a user un-flaggable regardless of login history. A nightly job that only runs monthly needs a window longer than its cadence.
  • Leave scheduled scans on “report only”. Auto-apply exists, is opt-in, and still honours every rail — but nobody reviews the plan first, and a pipeline identity that starts being used between scans will already have been revoked.
  • The six tabs: Overview (summary, cost impact, inactivity trend, where waste concentrates, and which data sources worked), Users (drill-down with the full billable-path breakdown), Excluded (every rule, plus every user spared and which rule or heuristic spared them), Cleanup (Safe/Live, plan preview, bulk apply, restore), Runs & audit, Settings.
  • Defaults, and why: inactivity threshold 90 days (survives parental leave and sabbaticals; 30 days produces too many false positives in engineering orgs), never-logged-in grace 14 days (a pending invite is not waste), recent-activity safety window 30 days, service-account confidence threshold 50%, notification lead time 7 days, weekly report-only scans (licence waste changes slowly; daily scans burn invocations for no benefit).
  • CSV export is copy-paste, not a download. Forge UI Kit runs sandboxed with no filesystem or Blob access, so the CSV is rendered in a modal with the suggested filename above it. The content is byte-identical to what a download would produce.
  • Notifications are recorded, not emailed — Forge has no first-party outbound email API. Enabling them still changes behaviour end to end: the first plan for a flagged user records a notice with a deadline instead of revoking anything, and every later plan refuses to touch that user until the lead time elapses. You send the message, then use They replied “still active” in the user detail modal to block the pending cleanup.

If something looks wrong

A user shows “No data” and is never auto-actioned.

The Admin API only covers managed accounts. Users outside your verified domains — external collaborators, contractors on personal accounts — return no record, are classified unknown, and are never auto-actioned.

Some billable paths are missing from a user’s breakdown.

The repository access index is eventually consistent. It builds in the background in bounded batches and parks itself when the workspace nears its hourly API limit, resuming on the next daily trigger. Inactivity results are unaffected, and the Overview tab states where the index has got to.

A path is labelled “Group grant (inferred)” and cannot be removed.

Bitbucket exposes user groups only through the deprecated 1.0 API, which Forge apps cannot call. Group-derived paths are labelled, marked non-removable, and carry instructions pointing at workspace settings. For a hard guarantee, use a user or pattern exclusion rather than a group one.

A project-grant revocation failed with 403.

Expected, and deliberate. Project-grant revocation may additionally require admin:project:bitbucket, which also confers project deletion — a scope a licence-cleanup app has no business holding, so it is not requested. The action is marked failed with the API’s message and written to the audit log; it fails loudly, not silently.

A workspace membership path cannot be removed.

Bitbucket does not support it via the API — membership is managed in Atlassian admin or via SCIM. The app reports these paths and tells you where to go.

What it does not do

  • Delete an account, or perform any irreversible operation. The worst case is a revoked permission grant.
  • Read commit or pull-request history. Inactivity comes from the workspace permissions endpoint, which paginates by user; git activity comes from product events, which cost nothing.
  • Remove a user from a group, or remove workspace membership — neither is exposed to Forge apps.
  • Auto-action an account it has no data for.
  • Run on Data Center. This is a Forge Cloud app.
05

Permissions & scopes

ScopeWhy it's needed
storage:appSettings, exclusions, scans, plans and the audit log
read:workspace:bitbucketWorkspace membership and workspace-level permission grants
read:project:bitbucketProject listing and project-level permission grants
read:repository:bitbucketRepository listing and repository permission grants, for the background access index. No commit history is read.
read:pullrequest:bitbucketRequired to receive pull-request events. Not used for any REST call; the app never reads pull request history.
read:permission:bitbucket, write:permission:bitbucket, delete:permission:bitbucketReading and revoking explicit user permission grants — the only cleanup actions this app can perform natively
admin:repository:bitbucketRequired by PUT/DELETE on the repository permissions-config endpoint. The scope docs note that admin:repository alone does not allow modification of repository permissions, which is why the write and delete permission scopes are still needed alongside it.

admin:project:bitbucket is deliberately not requested, even though project-grant revocation may require it. That scope also confers project deletion, which a licence-cleanup app has no business holding. If a project-grant revocation returns 403, the action is marked failed with the API’s message and written to the audit log — it fails loudly, not silently.

06

External access

HostWhy
api.atlassian.comThe Atlassian Admin (Organizations) REST API — the only first-party source of org-wide last-active data. Called with an organization API key the administrator supplies in-app, not with Forge’s own credentials.
07

Data stored

RecordKeyContentsRetention
Organization API keyForge encrypted secret storeThe credential, validated against the API before it is storedUntil replaced. Never returned to the browser; only the last four characters are shown back
Scans and plansForge KVSPer-user classification, billable paths, activity dates, service-account signals, and the actions a plan proposesWhile the app is installed
Git activityone storage write per eventThe date of the most recent push or pull-request event per accountFrom installation onwards; no history is backfilled
Exclusions and settingsForge KVSExclusion rules, thresholds, schedule, notification settings and per-seat costWhile the app is installed
Audit logappend-onlyEvery scan, plan, applied action, restore and failure, with the permission level that was removedWhile the app is installed
08

Security notes

Credential handling

  • The organization API key is validated against the API before it is stored, written with Forge’s encrypted secret store, never returned to the browser, and only ever sent to api.atlassian.com.
  • Only the last four characters are shown back, so an administrator can confirm which key is installed without being able to read it.

Irreversibility is designed out

  • The app never deletes an account and never performs an irreversible operation. The worst case is a revoked permission grant, which is one button away from being restored.
  • Every applied action records the permission level it removed, so restore is an exact replay rather than a best guess.
  • Safe mode produces the identical plan object Live executes — there is no separate code path that could diverge.

Safety rails

  • A recent-activity safety window (default 30 days): any push or pull request inside the window makes a user un-flaggable regardless of login history.
  • Service-account confidence threshold (default 50%), calibrated so no single weak signal can quarantine a human, but the strong “writes with no login” signal can on its own.
  • Accounts the Admin API cannot measure — anyone outside a verified domain — are classified unknown, shown as “No data”, and are never auto-actioned.
  • The plan surfaces a “Skipped by safety rails” list alongside itself, so what the app refused to touch is as visible as what it proposes.

Scope restraint

  • admin:project:bitbucket is not requested, because it also confers project deletion. A project-grant revocation that needs it fails loudly with the API’s own message rather than the app quietly holding a scope it should not.
  • read:pullrequest:bitbucket is held only to receive events. It backs no REST call in this app.

Testing

  • Tests cover the logic that carries the safety guarantees: classification, service-account detection, billable-path derivation, exclusion evaluation, the plan safety rails, schedule due-calculation and CSV escaping.
  • They run without a Forge runtime; the platform packages are stubbed.
09

Known limitations

  • Group membership is not readable. Bitbucket exposes user groups only through the deprecated 1.0 API, which Forge apps cannot call. The app therefore cannot remove a user from a group: group-derived paths are labelled “Group grant (inferred)”, marked non-removable, and carry instructions pointing at workspace settings. Membership is inferred from effect. For a hard guarantee, use a user or pattern exclusion.
  • Workspace membership cannot be removed via the API. Bitbucket does not support it; membership is managed in Atlassian admin or via SCIM. The app reports these paths and says where to go.
  • Project-grant revocation is unverified against a live workspace, because the scope it may additionally require also confers project deletion and is deliberately not requested. A 403 is recorded as a failed action with the API’s message.
  • The Admin API only covers managed accounts. External collaborators and contractors on personal accounts return no record, are classified unknown, and are never auto-actioned.
  • Last-active data lags up to 24 hours and counts only interactive product use — which is exactly why the git-activity event triggers exist.
  • Git activity starts at installation. An identity that pushed last month but not since installation has no write date yet. Do not act on the first scan of a fresh install.
  • The repository access index is eventually consistent. It builds in bounded background batches and parks itself when the workspace nears its hourly API limit. Inactivity results are unaffected, and the Overview tab states where the index has got to.
  • Notifications are recorded, not emailed. Forge has no first-party outbound email API. Enabling notifications still changes behaviour end to end — the first plan for a flagged user records a notice with a deadline instead of revoking anything, and every later plan refuses to touch that user until the lead time elapses. Only delivery is missing.
  • CSV export renders for copy, not download. UI Kit is sandboxed with no filesystem or Blob access. The content is byte-identical to a download.
  • Data Center is out of scope. This is a Forge Cloud app.
10

Release notes

Version history for User License Cleanup & Dashboard for Bitbucket. The most recent release is listed first.

  1. v1.0.0 Current

    Initial release.

    What it does

    • Inactive and never-logged-in seat detection for a Bitbucket Cloud workspace, with a per-user billable-path breakdown that says which grant to remove and whether the app can remove it.
    • Service-account detection built for Bitbucket specifically: push and pull-request product events distinguish a token-driven CI identity from an abandoned human account.
    • Safe/Live cleanup with a shared plan object, typed confirmation, and an exact restore of every applied change.
    • Exclusion rules by user, pattern and synthesised group label, with the Excluded tab showing which rule or heuristic spared each user.
    • Scheduled scans (default weekly, report only), an append-only audit log, and CSV export from any view.

    Defaults, and the reasoning

    • Inactivity threshold 90 days — long enough to survive parental leave, sabbaticals and long holidays; short enough that a quarterly audit is meaningful. 30 days produces too many false positives in engineering organisations.
    • Never-logged-in grace 14 days — an invited user who has not accepted yet is a pending invite, not waste.
    • Recent-activity safety window 30 days — longer than the slowest common CI cadence plus margin.
    • Service-account confidence threshold 50%, notification lead time 7 days, scan cadence weekly, report only.

    Deliberate omissions

    • admin:project:bitbucket is not requested, because it also confers project deletion.
    • No commit or pull-request history is read; git activity comes from product events, which cost nothing and cannot exhaust a rate limit.
    • No account is ever deleted, and no irreversible operation exists in the codebase.
11

Get it & contact

Marketplace link coming soon Contact support

Vendor

AryonForge