# Security — Crosstab

A security policy for an app with no servers is a short document, and
pretending otherwise would be the first thing wrong with it. Crosstab runs
entirely inside Atlassian's platform. There is no backend, database or network
of ours. There is nothing to breach that is not Atlassian's own infrastructure.

Last reviewed: 26 October 2026. This is **version 3**, in force since the app
was listed on the Atlassian Marketplace. A change to how the app is built or
operated takes the next version number; a correction that touches none of that
moves this date and the digest at the foot of the page instead.

This page is about how the app is built and operated. The one source for what
is read, stored and kept, and for how long, is [the privacy policy](/privacy).
The whole arithmetic is on [the method page](/method).

## At a glance

⟨flow⟩

- **Your browser** — the gadget's interface, in an iframe Jira draws on the
  dashboard.
- **A Forge function, inside Atlassian** — reads Jira as the person looking, or
  as whoever switched a schedule on, and hands back totals and labels.
- **Your Jira site and the app's storage** — it reads Jira and writes only to its
  own storage, and Jira keeps a gadget's settings; counts and a line of sizes go
  to Atlassian's console, and once a week the account ids it stores go to
  Atlassian's privacy reporting.
- **Anywhere else** — out of reach. The manifest declares no external host, and
  the platform refuses one that is not declared.

| The question         | The short answer                                                                                                                                                                                                                   |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Where it runs        | Inside Atlassian's platform. No backend, database or network of ours                                                                                                                                                               |
| Whose permissions    | The person looking at the dashboard; for a schedule, whoever switched it on; for a published grid, whoever published it                                                                                                            |
| What it may ask for  | Twenty-four read scopes, one storage scope, one that checks a search and one that reports account ids, [printed below](#the-twenty-seven-scopes-printed)                                                                           |
| A person as an axis  | Refused, against the schema of every field your site returns                                                                                                                                                                       |
| Data residency       | Stored only in Forge storage, which resides where your site does. The function usually runs there too, but Forge does not promise it; nothing leaves Atlassian either way                                                          |
| Encryption           | Atlassian's, at rest and in transit; the app adds no cryptography of its own                                                                                                                                                       |
| Checked by Atlassian | Eligible for Runs on Atlassian by Atlassian's own check, run by the Forge CLI after a deploy. It passed on 8 October 2026 for version 2.0.0 as deployed to the production environment. The badge itself comes with the listing     |
| Before it ships      | `npm audit`, lint, a build and the test suite, on the workstation before every push and again in Bitbucket Pipelines on every merge to `main` and every release, and the browser suite on `main` before "Tag a release" is offered |
| A vulnerability      | [support@chmut.com](mailto:support@chmut.com) or Atlassian; a critical one fixed within 10 days                                                                                                                                    |
| Who stands behind it | One person, a sole trader, and no team. [If the one person stops](#if-the-one-person-stops)                                                                                                                                        |

### For whoever approves the app

- **It reads** your Jira, with the permissions the table above names, and
  nothing else. Every item, and when it is read, is on
  [the privacy page](/privacy#what-the-app-reads).
- **It writes** only to its own Forge storage, and a gadget's settings, which
  Jira keeps on the dashboard when somebody saves the form. In the reader's
  browser it leaves two small notes, under keys that carry no search:
  [the privacy page](/privacy#what-the-app-stores) says what they hold. It
  creates, edits or deletes no issue, filter or field.
- **It reaches** your Jira site and Atlassian's platform. The manifest declares
  no external host.
- **Three scopes look broader than they are.** `read:user:jira` and
  `read:group:jira` come in the set Jira asks for to read a saved filter's name;
  no group is read, and of a person the app asks only a display name.
  `read:board-scope.admin:jira-software` is asked only for a board's columns:
  which statuses each holds, and in what order.
- **One neither reads nor writes:** `validate:jql:jira`, to check a search when
  the settings form stores it.
- **One is asked only on the administration page:** `read:permission:jira`, to
  check that whoever uses it holds Administer Jira.
- **One is asked as the app:** `report:personal-data`, to send Atlassian, once
  a week, the account ids the app stores and learn which accounts are closed.
  A closed account's schedules and published grids are deleted, and its id comes off the setups it saved.
- **One hourly allowance serves every site.** Jira meters the app, not your
  site: every installation reads from one pool of 65,000 points an hour. Each
  site's share is capped, but when the pool runs out, every gadget on every
  site stops reading until the next UTC hour.
  [What it spends of Jira's allowance](/getting-started#what-it-spends-of-jira-s-allowance).
- **Publishing is off** from the day it is installed, until a Jira
  administrator turns it on.
- **Keep automatically is switched on for the site** from the day it is installed: nobody's grid is kept on a schedule until they switch one on, and an administrator can turn it off for everybody.
- **Keep a first grid for me when I save** is ticked on the settings form of the Crosstab grid gadget, wherever those settings have no kept grid yet; the other four gadgets do not offer it. Saved without unticking it, the gadget keeps its first grid the first time whoever saved it opens it, if they have room for another; its settings hold that person's Atlassian account id.
- **A grid kept automatically is read as its owner**, with their permissions, on its schedule, whether or not anybody has the dashboard open: that is what `allowImpersonation` on the read scopes allows.
- **After uninstalling**, Atlassian keeps the app's storage for 28 days, then deletes it. Every record's own lifetime is in [Retention](/privacy#retention).

## What you can check yourself

The source is not public, so every claim below is narrower than it sounds.
These you can check yourself, before installing.

| What                             | Where to check it                       | What it shows                                                   |
| -------------------------------- | --------------------------------------- | --------------------------------------------------------------- |
| The manifest                     | The consent screen                      | No external host                                                |
| The scopes                       | The consent screen                      | What the app may ask Jira, line by line                         |
| What runs when nobody is looking | The manifest's triggers                 | One thing, for the people who asked for it                      |
| Runs on Atlassian                | The badge, which comes with the listing | Atlassian's own check of where the code runs and the data stays |

### The manifest

Atlassian shows it to you on the consent screen. A Forge app declares every
external host it may reach. A host it did not declare it cannot reach: the
platform refuses the request. This app declares none.

### The scopes

There are twenty-four read scopes, one storage scope, one that checks a search
and one that reports account ids. They are granular rather than one blanket `read:jira-work`. That is more
lines to read, not fewer, and it is the point. A granular list can be checked
against what the app claims to do, and `read:jira-work` cannot.

Some of the length is Atlassian's bundling rather than ours. Reading a saved
filter's id and name costs nine scopes. Among them are `read:user:jira` and
`read:group:jira`, in a gadget that will not draw an axis by person. The same
four person-shaped scopes are what Jira asks for a display name. That is the one
thing about a person this app asks for. A worklog arrives with its author and
comment, and each change in an issue's history with its author. The app uses
only the time logged and the changes to the sprint, the status and the field
being added up, and stores none of it.

Every scope is [printed below](#the-twenty-seven-scopes-printed), with why it is
asked for.

### What runs when nobody is looking

One thing, and only for the people who asked for it: **Keep automatically**,
beside **Keep this**. It keeps a grid every week on a chosen day, on the 1st of
each month, or whenever a sprint closes. Each is kept then, or as soon after as
Jira allows.

| Entry point in the manifest        | What it does                      |
| ---------------------------------- | --------------------------------- |
| A scheduled trigger, once a day    | Puts a run on the app's own queue |
| An event trigger, a sprint's close | Puts a run on the app's own queue |
| That queue's consumer              | Reads, one run at a time          |

The consumer does one narrow thing for each schedule on the site whose turn it
is. It reads the grid as the person who switched that schedule on, by their
account id, with their permissions and no one else's. Then it does one thing
more: keep it into their own history, exactly as their own press of **Keep
this** would.

- **Nothing is read as the app**, and nothing is published.
- **A schedule stops**, rather than reading as anyone else, once Jira will no
  longer act as its owner.
- **At most twenty schedules run on a site**, and at most three of any one
  person's.
- **The platform skips both triggers** on a site without an active licence.
- **A Jira administrator can turn it off** for the whole site, on Crosstab's administration page, beside the publishing setting. While it is off, no schedule runs and nobody can switch one on. Nothing is deleted. Once it is back on, no week or month whose day came before that day is kept, and no sprint that closed while it was off.
- **A Jira administrator can switch off one person's schedule** on the same page, to free a place on a full site. For each running schedule the page shows them its owner, by the name Jira shows them, the filter's name if they can open that filter, when it keeps, when it was switched on and when it last kept - never its settings, its search or anything it kept. The schedule is stopped, not deleted, and its owner's gadget says a Jira administrator stopped it.
- **A Jira administrator can delete a stopped schedule** on the same page: one they switched off, or one Jira stopped because it would no longer let the app act as its owner. Stopped schedules are listed after the running ones. Deleting one takes its owner's account id and settings out of the app's storage. The grids it already kept stay.

What a scheduled read asks of Jira beyond a render is listed on
[the privacy page](/privacy#what-the-app-reads).

Everything else reads Jira only while somebody is looking at the gadget or
pressing something on it. [The privacy page](/privacy#when-jira-is-read) lists
each moment. One script runs beside the gadgets. It shares a first read between
the ones on a dashboard asking the same question. It is part of that dashboard:
it runs while the dashboard is open and reads as whoever opened it. It keeps
nothing a gadget's own read would not.

That is a statement about the manifest in front of you, not about every release
after it. If a release adds another trigger, this page and the privacy policy
say so before that release ships.

### How much of Jira's allowance it may spend

Jira meters every installation of this app out of one pool of 65,000 points an
hour. So a read made while nobody is looking spends what readers on other sites
need. One site's share is capped: at most three tenths of the pool, schedules
and gadgets together, so three sites at their ceiling still leave a tenth for
every other site.

| Who spends, on one site                                | At most, in any UTC hour                                                                    | Past that                                                                                          |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| The schedules                                          | 6,500 points - a tenth                                                                      | A schedule that does not fit waits for the next hour rather than failing                           |
| The gadgets people open, their settings forms included | 13,000 points - a fifth, give or take 500 points for each gadget reading at the same moment | A gadget asks Jira nothing until the hour turns, and says that the limit is this app's, not Jira's |

- **Both are priced the same way, and counted apart.** A point per issue and a
  point per page, and two points for each count Jira is asked instead. These
  prices were measured on 30.09.2026. The schedules' share keeps a margin for
  the calls around the read.
- **A background read takes at most five thousand issues.**
- **The first refusal from Jira stops every schedule on the site.** That is
  the first `429`. They stop until the hour turns, or for as long as Jira asked
  if that is longer. So does any answer
  saying the pool is running low.
- **A grid Jira cut short that way is not kept.**
- **One run reads at a time**, through one queue.

### Checked by Atlassian

The Runs on Atlassian programme asks three things of an app.

| The programme asks for                                                 | This app                                                                                                                                                                                       |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Atlassian-hosted compute and storage only                              | The manifest declares no external host                                                                                                                                                         |
| The host's own data residency                                          | Stored only in Forge storage, which resides where your site does                                                                                                                               |
| A customer able to control external egress, such as analytics and logs | What leaves the function is counts per minute into Atlassian's own counters and a line in Atlassian's own log. A site administrator can turn off **Custom metrics access** and **Logs access** |

The counters and the log hold sizes, status codes and fixed phrases, never
content. The privacy page
[describes both](/privacy#what-the-vendor-sees).

Atlassian's own check agrees. The Forge CLI runs it after a deploy, and it
passed on 8 October 2026 for version 2.0.0, as deployed to the production
environment. Atlassian applies that label to an eligible app; the vendor does
not claim it. It is not an audit. It certifies nothing about how the code is
written. It says where the code runs and where the data stays. That is what the
manifest says, checked by the platform the app runs on.

### The twenty-seven scopes, printed

Named here rather than left on a consent screen the reader has already clicked
through. The grouping is the manifest's own. The right-hand column is why each
group is asked for. Every read scope but `read:sprint:jira-software` and
`read:permission:jira` carries `allowImpersonation`. That is what lets a grid
kept automatically be read as its owner. The sprint scope reads nothing and is
held for the event alone. The permission scope is asked only as the person at
the administration page.

| Scopes                                                                                                                                                                                                              | Asked for                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read:field:jira` · `read:field-configuration:jira` · `read:project-category:jira`                                                                                                                                  | The field description: what may be an axis, what may be a value, and the names behind saved settings                                                                                                                                                                                                                                                                                           |
| `read:filter:jira` · `read:jql:jira` · `read:application-role:jira` · `read:group:jira` · `read:user:jira` · `read:project:jira` · `read:project-role:jira` · `read:issue-type-hierarchy:jira` · `read:avatar:jira` | Saved filters: the picker, and naming the one already chosen. Nine scopes for two fields, `id` and `name`, because Jira counts them as a set. It reads no group. It reads a person only for a display name: the reader's own, a publisher's by account id, and a schedule owner's by account id for a Jira administrator on the administration page                                            |
| `read:issue-details:jira` · `read:priority:jira` · `read:field.default-value:jira` · `read:field.option:jira`                                                                                                       | The issues themselves. The search is a POST rather than a GET. Atlassian gives the two verbs different sets for the same question, and GET's includes `read:audit-log:jira`                                                                                                                                                                                                                    |
| `read:board-scope:jira-software` · `read:board-scope.admin:jira-software`                                                                                                                                           | The order a board draws its statuses in, which no issue carries and no Jira Platform endpoint has. Asked for only when an axis is status or board column                                                                                                                                                                                                                                       |
| `read:instance-configuration:jira` · `read:issue.time-tracking:jira`                                                                                                                                                | How this site writes a length of time: how long a working day is, and which of Jira's three forms a duration is drawn in. Asked when the settings form opens, and when the metric is a length of time - one of the six fields held in seconds, time between two dates, or time logged                                                                                                          |
| `read:issue-worklog:jira` · `read:issue-worklog.property:jira`                                                                                                                                                      | The worklogs of one issue, for time logged in a month. The search carries at most twenty worklogs an issue, and the rest are asked for issue by issue. Asked for only when the metric is time logged                                                                                                                                                                                           |
| `read:sprint:jira-software`                                                                                                                                                                                         | The event a sprint's close arrives as, for a grid kept when a sprint closes. The platform delivers it only to an app holding this scope. Nothing is read with it. The sprint's issues come through the search above, as the schedule's owner                                                                                                                                                   |
| `read:permission:jira`                                                                                                                                                                                              | Whether whoever uses the administration page holds the Administer Jira global permission. Asked on every change made there and each time it lists the site's schedules, as the person at the page, and on a production site nowhere else. One yes or no crosses into the app                                                                                                                   |
| `validate:jql:jira`                                                                                                                                                                                                 | Checking a search when the settings form stores it. Jira's strict parse is the one question that names a field it does not know. Not a read and not a write. Asked only by the person at the form. A site that refuses it has the search checked as before                                                                                                                                     |
| `storage:app`                                                                                                                                                                                                       | Keeping a grid, by hand or automatically, saving a setup, holding a reader's last grid for five minutes, each reader's finished work as totals, and the other records [the privacy page](/privacy) lists. The only scope here that writes anything                                                                                                                                             |
| `report:personal-data`                                                                                                                                                                                              | Sending Atlassian, once a week, the account ids the app stores - a schedule's owner, a published grid's publisher, a saved setup's author - so it can say which accounts are closed. A closed account's schedules and published grids are deleted, and its id comes off the setups it saved. The only scope asked as the app rather than as a person. Neither a read of Jira nor a write to it |

`validate:jql:jira` exists because a misspelt field in a search answers with no
issues rather than an error. It reads nothing and writes nothing.

Two of these are worth a second look rather than a skim:

- `read:user:jira` and `read:group:jira` are in a gadget that will not draw an
  axis by person.
- `read:board-scope.admin:jira-software` has `admin` in its name and buys a
  column order. A site that refuses the two board scopes gets the statuses in
  the order they were added to the site. That is how Jira itself sorts them.

## Where the data lives

Nowhere it was not already. The code runs in a Forge function inside
Atlassian's infrastructure. The interface runs in an iframe in the reader's own
browser. The function turns the issues it reads into totals and hands them to
that iframe. There is no server or database of ours for anything to be written
to.

What is kept sits in your own site's app storage, inside Atlassian's
infrastructure and under your agreement with them. None of it holds an issue
key, a description or a comment. The one piece of ticket text the app keeps is
the label on the **Parent** axis: a parent's own summary, usually an epic's.

That storage is Forge's own. Atlassian's rule is that an app keeping all
end-user data in Forge hosted storage qualifies for `PINNED` data residency,
with nothing to configure. This app keeps it nowhere else, so the storage sits
where your site's data is pinned. Where the function runs is a separate
question. Forge usually runs it in your site's region, but Atlassian does not
guarantee that, and this page does not either. Either way, the app calls no
host outside Atlassian's: the manifest declares none.

Every record is listed once, on the privacy page:

| Question                    | Where it is answered                                  |
| --------------------------- | ----------------------------------------------------- |
| What is read, when, and why | [What the app reads](/privacy#what-the-app-reads)     |
| What each record holds      | [What the app stores](/privacy#what-the-app-stores)   |
| How long each record lives  | [Retention](/privacy#retention)                       |
| What reaches the vendor     | [What the vendor sees](/privacy#what-the-vendor-sees) |

Uninstalling the app removes all of it, on Atlassian's schedule rather than ours.

## Whose eyes the data is read with

The search runs with the permissions of whoever is looking at the dashboard. It
does not run with an application account, nor with the permissions of whoever
configured the gadget.

| Grid               | Read with the permissions of                                            | Who can open it                                                             |
| ------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| On a dashboard     | Whoever is looking                                                      | That person. Two people opening the same gadget can see two different grids |
| Kept by hand       | Whoever kept it, when they pressed                                      | Only them. The storage key is a hash of their account id and of the choices |
| Kept automatically | The one person who switched that schedule on, named by their account id | Only them. It is kept only into their history                               |
| Published          | Whoever published it, on the press                                      | Everybody who opens that gadget                                             |

- **A grid read for somebody is never wider than their own view of Jira.** A
  reader who cannot see a project does not see its issues in a total.
- **A schedule is never read as the app**, and never as anybody else. It stops,
  rather than reading as anyone else, once Jira will no longer act as its owner.
- **A colleague opening the same gadget** on the same dashboard sees their own
  kept grids, never somebody else's. The interface says so where the button is.
- **A saved setup reaches the whole installation.** It is a name and four ids,
  and no figures at all. Everybody reads it; only whoever saved it can change or
  remove it.
- **A reader with no account**, on a publicly shared dashboard, is handed no kept
  grid and no setup.

### A kept grid is a picture

**A kept grid is a picture, not a query**, and so is a published one. It holds
figures already worked out. So drawing it again does not re-check anybody's
permissions against the issues it counted. That is the exception a reader
should know about.

### A published grid

**A published grid is the one picture meant for other people.** **Publish this
grid** leaves one grid on that gadget for whoever opens it. A colleague then
sees figures from the publisher's view of Jira. That includes issues their own
permissions may not show them.

So it is never silent. The grid is drawn under a banner naming whose view it is.
The gadget asks before each publish, except when the publisher's own published
grid is the one showing on that gadget.

- **Only whoever published it can replace it or take it down.**
- **A grid whose publisher has left stays.** It is hidden only while a site
  administrator has publishing turned off.
- **Its figures are read from Jira on the press**, as the publisher, not taken
  from the page. So a browser cannot put numbers under somebody's name that Jira
  never gave them.
- **A read that stops before the end of the filter, or that Jira refuses,
  publishes nothing.**
- **It holds the publisher's account id**, so only they can take it down. The
  name over it is asked of Jira when it is drawn, not stored.

⟨more: How a published grid is keyed⟩

It is stored against the gadget rather than a person. The key is a hash of the
dashboard and gadget ids. Those ids are taken from the platform, never from the
page. So nobody can ask for the grid of a dashboard they were never shown.

⟨/more⟩

### The publishing switch

A site has to want this before anybody can publish.

- **The setting:** **Let people publish a grid**, on the Crosstab page under
  **Apps** in Jira's administration. It is off until somebody turns it on.
- **Off:** the resolver refuses every publish and offers no published grid. The
  gadget draws no publish button.
- **Who can change it:** site administrators. The change is accepted only from
  that administration page, which Jira shows to site administrators alone.
- **A setting that cannot be read counts as off.**

⟨more: How the switch is enforced⟩

The resolver reads which module called it from the platform's context, not from
the request. So a request from anywhere but the administration page cannot
change the setting. And it asks Jira, as the person at the page, whether they
hold the Administer Jira global permission. A no, or no answer, changes
nothing. The same two checks guard the Keep automatically switch, the list of
the site's schedules, switching one of them off and deleting a stopped one.

⟨/more⟩

## No person as an axis

No axis can be a person. Not assignee, not reporter, not creator, not any user field. This is enforced
against the schema of every field your site returns, not against a list of
names. So a custom user picker called `Reviewers` is refused for the same reason
`Assignee` is. So is one that holds several people at once.

The rule enforces the shape of a field. Three things fall outside a shape: a
label named after a colleague, a custom select list whose options are typed-in
names, and a line of JQL. All three are set out on
[the privacy page](/privacy#personal-data), rather than left for a reviewer to
find.

## Checks before release

The gate is `npm run check`, run on the workstation before every push. Its first
step is `npm audit`. Bitbucket Pipelines runs the same checks on every merge to
`main`, and again on every release before it offers a deploy. A known
vulnerability in a dependency stops the change on the workstation, and a release
that fails the audit offers no deploy. A pull request gets the same checks
without the audit and the test suite. On `main` the browser suite, `npm run
check:ui`, runs too, by its own manual step, and "Tag a release" is offered only
once it has passed on that commit.

| Check                                                | Detail                                                                                                  |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `npm audit` at `moderate` and above                  | A known vulnerability in a dependency stops the change, rather than appearing in a report nobody opens  |
| `npm audit` of the deploy tools' own lockfiles       | Reported, not a gate. What it found, and why it is accepted, is under [who can deploy](#who-can-deploy) |
| Formatting, and lint at zero warnings                | -                                                                                                       |
| Only the four files allowed to reach the platform do | -                                                                                                       |
| A build, before the suite rather than after it       | The bundle assertions have nothing to look at otherwise                                                 |
| The test suite                                       | Over 4,600 tests across more than a hundred files                                                       |

Two of those tests are about this page's claims rather than about behaviour.

- **One reads every built file.** It fails on any host the bundle reaches that is
  not this site's own. The handful it permits are strings inside error messages.
  Each is checked by reading the characters around it, not by trusting the name.
- **The other holds the claims in these documents** against the code that has to
  make them true.

## Who can deploy

A Forge deploy reaches every installation with no action by you. That is the
app's most valuable asset and its largest risk. Every deploy runs in Bitbucket
Pipelines, as `bitbucket-pipelines.yml` declares it, and only from a release
tag.

| What                         | How                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A release                    | A `v` tag, which the "Tag a release" step cuts from `main` after the checks there pass, the browser suite included. The tag's pipeline runs the checks again on its commit before it offers a deploy                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| Development, and these pages | Deployed from the tag, each by its own manual step, or again, after the checks, in the pipeline someone runs to approve a major version                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Production                   | A manual step too, offered only after development and the pages have deployed the same commit                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| A change of scopes           | A new major version: a site keeps the previous version until its admin accepts the new scopes. Development approves it by itself, and its installation is upgraded only when someone runs a custom pipeline for that release. Production approves it only when someone runs a custom pipeline for that release, which deploys development and these pages again first; the usual production step fails on it and deploys nothing. Until production has its first installation, Forge asks for no approval, and the usual step deploys it like any release. Either can be started from any branch or tag, but deploys only a commit on `main` that a release tag points at |
| Any branch or pull request   | Nothing deploys                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| The deploy credentials       | Secured variables in Bitbucket, never in the repository. Each belongs to the deployment that uses it, so only that step can read it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| The check steps              | Hold no credentials at all                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Rolling back                 | Redeploy of the previous release, from Bitbucket's deployment history. The step rebuilds from that release's own commit. A Redeploy of a production deploy that approved a major version approves it again                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

⟨more: What the deploy steps do not run⟩

Each deploy step installs its dependencies with lifecycle scripts disabled, both
the app's and the tools'. The Forge CLI is 832 packages with three install
scripts, one of which downloads a 38 MB executable during install. Running
arbitrary install code in the one environment holding a deploy token is the
risk that flag exists for.

Each deploy step takes its tools from a lockfile of its own, committed beside
the deploy script, and restores no npm cache. The lockfile fixes the Forge CLI,
the tool that deploys these pages and every npm package under them, so a release of
any of them cannot change a deploy on the day it ships. The check steps install
with scripts on and keep an npm cache; nothing they leave in it reaches a step
that holds a credential.

The tools' own trees are audited by `npm run check` too, from their lockfiles,
and it reports what it finds rather than failing on it. On 8 October 2026 that
was 44 known vulnerabilities in the Forge CLI 14.1.0 (3 critical, 29 high, 8
moderate, 4 low) and none in wrangler 4.148.0: the three it had, all in the
image library its local simulator pins, were cleared by taking that library's
next patch release. That override comes off once wrangler pins that release or
a later one itself. The Forge CLI's are accepted rather than fixed: 14.1.0 is
the latest release, and it ships its own `npm-shrinkwrap.json`, so its tree
changes only when Atlassian releases a new one. Overriding packages inside it
would deploy with a tree Atlassian has not tested. The deploy steps move to each
new Forge CLI release, and the count here is measured again when they do.

The build still runs the code of every build dependency, and what it builds is
what is deployed. The step builds with no Forge or Cloudflare variable in the
environment it hands the build, so a dependency that reads its own environment
finds no token there. That is all it covers: code in the same container, run as
the same user, can still read the step's own environment from `/proc`. A
dependency that could do that could as well change the bundle it builds, which
is why the app's dependencies are audited and locked.

The deploy steps run on one Node image, pinned by its digest. A change to the
image's tag reaches them only through a commit that names a new digest.

A deploy runs the pipeline file of the commit it deploys. So all of this holds
for releases tagged since the pipeline has done it, and a Redeploy of an older
release runs that release's own steps.

⟨/more⟩

The repository has one account with write access, the vendor's. On this
Bitbucket plan a deployment cannot be tied to a branch or a tag by a setting.
The pipeline file does that, and only that account can change it. A tag on a
commit that is not on `main` deploys nothing when that commit's pipeline file
carries the check, which each deploy step runs before it installs anything. It
stops a mistaken tag. A commit whose pipeline file lacks the check, an older
branch or one that removed it, is not stopped by it, so what remains is that
only that account pushes tags and a person starts each deploy. That account could still push a release
tag by hand on a commit of `main`, which skips the browser suite: tags are cut
by the "Tag a release" step, not by hand.

The vendor's machine keeps an Atlassian token of its own, because reading the
app's logs needs one. It could deploy, but is not used for that.

## Reporting a vulnerability

Write to [support@chmut.com](mailto:support@chmut.com). Say what you found and
how to reproduce it. You do not need a proof of concept for anybody to take it
seriously.

You will get a human reply within 48 hours, on the same working days as any
other message to support. That is a commitment about acknowledgement rather than
about a fix.

You can also report it to Atlassian. They raise it in their Marketplace Security
project, where it arrives with a remediation due date. Either route reaches the
same person.

The app is held to these timeframes. They are Atlassian's rather than ours.

| Severity | CVSS          | Fixed within |
| -------- | ------------- | ------------ |
| Critical | 9.0 and above | 10 days      |
| High     | 7.0 to 8.9    | 4 weeks      |
| Medium   | 4.0 to 6.9    | 12 weeks     |
| Low      | below 4.0     | 25 weeks     |

## Response times

Two clocks, and it is easy to read one as the other.

| What                                                                                                                     | The clock                                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| An ordinary question: a figure that looks wrong, a setting that will not do what you want, a grid that came back partial | Answered within **48 hours**, between 09:00 and 19:00 Europe/Kyiv, Monday to Friday, except on Ukrainian public holidays |
| A vulnerability                                                                                                          | [The table above](#reporting-a-vulnerability). It is Atlassian's, and measures time to a fix rather than to a reply      |

Both reach the same person. That is the whole of the support arrangement. It is
written down plainly, because an administrator deciding whether to depend on
this is entitled to know how thin it is.

## Who stands behind this

One person: Oleksandr Chmut, a sole trader, trading as **oc**. It is the same
vendor as this studio's other Marketplace app. There is no team behind either of
them.

That is a smaller answer than an administrator would like. It is given plainly,
because the alternative is a page implying a company and a reviewer finding out
otherwise.

| What                     | Held by            |
| ------------------------ | ------------------ |
| The response times above | One person         |
| The deploy credentials   | One person         |
| The incident plan below  | One person, to run |

It also means there is nothing between a reported problem and the person who can
fix it. A vulnerability report reaches the author of the code the same day it is
sent, on either route.

## If there is an incident

Containment here is unusually direct. A Forge deploy reaches every installation
without any action by you. So rolling back to the last good revision is both the
fastest response and a complete one. There is no upgrade for an administrator to
apply, and no site left behind on a broken build.

### What counts

| Incident                                                                                     | Why it is one                                                                                                   |
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| The app doing something to a site that it is not supposed to be able to do                   | It is what the rest of this page is about                                                                       |
| Data of yours reaching somewhere it should not                                               | It is what the rest of this page is about                                                                       |
| The app unavailable, or drawing wrong figures, across installations rather than on one board | A grid that quietly adds up wrong is worse than a grid that does not draw. Only one of the two announces itself |

### How one is noticed

In almost every case, because somebody writes. This is the thin part.

- **The app's own refusals** go to `console.warn` and into Forge's logs. They
  are read by running `forge logs`, and nothing watches them.
- **There is no alerting and no error aggregation**, and this page is not going
  to imply otherwise.
- **Atlassian's own reporting routes** arrive by mail.
- **The logs are read by hand** with `forge logs`, after a production deploy.
- **Counters of what the app spends** of Jira's hourly allowance, per site and
  per minute, are in Atlassian's developer console. Atlassian's own count of the
  requests Jira refused with `429` is beside them. They are kept for fourteen
  days. Nothing watches them either: the console cannot raise an alert on an
  app's own counters.
- **Between deploys nothing reads them.** This page does not promise a weekly
  pass that one person would quietly stop making.

So the clock below starts when a report arrives, not when the fault does.

### What happens when

**The clocks, and they are what this app commits to you.**

| When                   | What happens                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------- |
| Within one working day | The report is answered by the person who wrote the code, saying what is known         |
| Within 72 hours        | Affected sites are told, if data of yours is involved                                 |
| The same working day   | The build is rolled back, once the fault is confirmed and where a rollback is the fix |
| Within 5 working days  | A written account of what happened, to anybody who was told                           |

**The 72 hours is the one clock Atlassian recommends** rather than one chosen
here. The notification goes to the technical contact Atlassian holds for each
licensed installation. That is the only address this vendor has for you. There
is no other list: this app collects no email addresses of its own. A site that
installed the app through a distribution link, before the app was listed, has
no such contact and cannot be reached this way.

### What is done, in order

1. Roll back to the last good revision first, and diagnose afterwards. A rollback is one redeploy of the last good release, it reaches every site at
   once, and it needs nothing from you.
2. Stop deploying anything else, including unrelated fixes. A second change
   during an incident is how the first one becomes unexplainable.
3. Write down what is known and when it was known, as it is learnt rather than
   afterwards.
4. Tell Atlassian within 24 hours of becoming aware of it, as a P1 ticket in
   the Marketplace Partner Portal, and update them at least every 6 hours until
   it is over. A vulnerability that nobody has exploited goes to their
   Marketplace Security project instead.
5. Tell the affected sites on the clock above.
6. When it is over, write one account of what changed, so that this section is
   not the same next time.

### Who does what

The same person does all of it. That is the arrangement, and what it costs you
is worth stating. Nobody is on call. An incident that begins at midnight on a
Saturday is noticed on the Monday. One that begins during a week away is
noticed when that week ends, and so is any report sent during it. The clocks
above start from that.

The compensating fact is the one this section opened with. The fix is a
rollback, and it needs no action from you. The person who can do it is the
person who wrote the code.

### What this plan does not cover

Atlassian's own platform. If Forge, Jira or Atlassian's storage is what failed,
the incident is theirs. Your agreement about it is with them. What this vendor
can do is tell you what it knows.

## If the one person stops

An administrator told there is one person and no team will ask a question. What
are they left holding on the day that person stops? A page that admits the
first and does not answer the second is only half of an answer.

- **Nothing of yours is inside this app.** It reads your filters, your issues
  and your fields, and writes none of them. No issue is created, edited,
  transitioned or commented on by it. There is nothing in your site that would
  have to be unpicked. Another tool pointed at the same filter draws from the
  same place.
- **Two things exist only here.** A grid you kept is the figures as they were. A
  named setup is a gadget's settings under a name. Both are in Forge hosted
  storage on your own site, not on any server of ours. There is no server of
  ours.
- **Both come out while the site is subscribed.** Any grid on screen, today's or
  one kept a year ago, leaves as copy, CSV, Excel or a picture whenever you
  press it.
- **The subscription is the condition of drawing figures at all**, kept grids
  included. So export while one is live, not after it lapses.
- **Lapsing does not delete.** The storage stays where it is. A new subscription
  brings the grids back as they were. The gadget says so on the day it stops
  drawing.
- **Uninstalling** hands the storage back to Atlassian to remove on its
  schedule.
- **Deleting what you stored is not behind the subscription.** Forgetting a
  grid, forgetting a setup, taking your own published grid down and switching
  **Keep automatically** off all work whether or not a site is paying. A site
  that has stopped paying is precisely the site that may want what it stored
  gone.
- **The code keeps one list** of what a subscription is a condition of and what
  it deliberately is not. The test suite holds it to that list.
- **There is no switch at this end.** There is no licence server of ours to stop
  answering, and no host of ours to go dark: the app declares neither. It runs
  on Atlassian's infrastructure, and the licence check is Atlassian's own. Only a site's own
  subscription ending stops it drawing figures. Nobody here can reach it.

What stops if the one person stops is the part a person does. That is new
versions, an answer to a support message, and eventually a platform change that
nobody is here to follow. Nobody else publishes new versions: there is no
second maintainer, and no escrow arrangement that would hand the app to one.
That is the real loss: a product that stops improving. It is not a report that
disappears, and not a number you cannot get back.

## What is not claimed

| This page does not claim            | What it says instead                                                                                                                                                                                                               |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| That the code is correct            | It claims the code is checked by the gate above, and that the claims in these documents are held against it. Those are different things, and the difference matters                                                                |
| That Atlassian's platform is secure | Everything here rests on it, and your agreement about it is with them                                                                                                                                                              |
| That a label is safe                | A component named after a colleague is text your organisation chose. No rule in this app can tell it from any other text. It is the one place the axis rule stops short, so [the privacy page](/privacy#personal-data) says it too |
| Independent review                  | Nobody outside this project has audited it. If that changes, this page will say so with a date                                                                                                                                     |
