Security
Written to answer the Marketplace security self-assessment and the questions a customer's IT team will ask. Most answers are short because the app's architecture removes the question.
Architecture in one line
A Forge app that runs entirely on Atlassian's infrastructure, reads Jira as the app while checking each viewer's own permissions before returning anything, writes one project property, and has no external network access.
Common security questions
Where is customer data stored? Inside the customer's own Atlassian Cloud site. Team configuration is a Jira project property (capacity-tracker.team). Display preferences use Forge storage on Atlassian's infrastructure. The partner operates no storage of any kind.
Does the app send data outside Atlassian? No. The app makes no outbound network requests. There are no external APIs, analytics, error-reporting services or CDNs. The UI bundle is fully self-contained, including its charts — no third-party script is loaded at runtime.
What authentication does the app use? Forge's built-in authentication. Jira calls run as the app (api.asApp()), and the app checks the viewer's own permissions before returning anything: boards are filtered to projects the user can BROWSE_PROJECTS, and every write requires ADMINISTER_PROJECTS on that project, both verified through Jira's bulk permissions API. asUser() is not used because it requires each individual to authorise the app before any call succeeds, which a Custom UI app has no way to prompt for. The viewer's own Jira permissions are enforced by the app on every request instead. The app holds no credentials, API tokens or secrets.
Can a user see data they aren't permitted to see? No, but the mechanism is worth stating accurately rather than reassuringly. Calls run as the app, so Jira is not filtering by the viewer's permissions on its own. The app asks instead: before returning anything, it checks the viewer's own BROWSE_PROJECTS against Jira's permissions API and drops every board they cannot open. A board with no identifiable project is hidden rather than shown — the check fails closed, and src/permissions.test.js pins that behaviour. A user who cannot open a board in Jira cannot see it in the app.
What are the app's permissions and why? See the table below.
Is data encrypted? In transit and at rest by Atlassian, as part of the Forge platform. The app adds no storage of its own to encrypt.
Third-party dependencies? React and React DOM in the UI bundle; @forge/api, @forge/resolver and @forge/bridge from Atlassian. No charting, analytics or utility libraries. The charts are hand-written SVG specifically to keep the dependency surface near zero.
Do you have a vulnerability disclosure process? Yes — see below.
Permissions, and why each one
| Scope | Why | Risk if abused |
|---|---|---|
read:jira-work | Issues, fields, boards, sprints, project metadata, and the project property the roster lives in | Read-only, filtered to projects the viewer can browse |
write:jira-work | One of two write scopes. The team roster, leave and holidays, as a project property | Limited to one named property |
manage:jira-project | Required by the project-property write. Scopes never override Jira's own permissions, so this grants nothing the signed-in user could not already do | Gated on ADMINISTER_PROJECTS, checked per request |
read:jira-user | Assignable users, for the "add someone to the team" picker | Read-only |
read:project:jira | Required by GET /rest/agile/1.0/board. Classic read:jira-work does not satisfy it | Read-only |
read:issue-details:jira | Required by the Agile backlog and sprint-issue endpoints | Read-only |
read:jql:jira | Required by the Agile sprint-issue endpoint | Read-only |
read:board-scope:jira-software | Boards and backlogs from the Agile API | Read-only |
read:sprint:jira-software | Sprints and the issues in them | Read-only |
storage:app | Display preferences per board | Atlassian-hosted |
Classic scopes for the Jira platform API, granular for everything the Agile API touches. That split is not a style choice. Jira Software supports no classic scopes at all, and its endpoints additionally require granular Jira platform scopes that classic does not satisfy — the board list needs read:project:jira even when read:jira-work is granted. forge lint reports zero errors against manifests that Jira rejects on every call, so the per-endpoint OAuth 2.0 scopes required line in the REST reference is the only source of truth here.
Classic is broader than granular, and that is a real trade. The honest description is that the app reads issues, fields, boards and sprints across the projects a viewer may browse, and writes a single project property.
The narrowing that matters is not in the scope list anyway. It is in src/access.js: every read is filtered to projects the viewer holds BROWSE_PROJECTS on, and every write requires ADMINISTER_PROJECTS, both checked against Jira on each request.
On the write scope
This is the one a reviewer will ask about. The full answer:
> The app writes exactly one Jira project property, capacity-tracker.team, on the project > being viewed. It holds one team per board: the roster, per-person availability settings, > the leave-source configuration and the public holiday calendar. > > This is deliberate rather than convenient: storing that configuration in the customer's own > Jira means they can read it over the REST API, export it, and keep it if the app is > uninstalled. The alternative — Forge storage — would make the customer's configuration the > app's property rather than theirs. > > The app never creates, edits, transitions, comments on or deletes an issue, sprint, board > or project. > > Every write is authorised explicitly. Calls run as the app rather than as the viewer, > because a Custom UI app cannot prompt for the per-user consent asUser() requires. That > removes Jira's own 403 as a safety net, so the app reinstates it: each write resolver first > asks Jira's permissions API whether the requesting account holds ADMINISTER_PROJECTS on > that project, and refuses if not. The check is per request, against Jira, and is not > cacheable or skippable from the browser — requireProjectAdmin in src/index.js gates > every one of them.
Where injection could happen, and what stops it
JQL. The leave-source feature accepts a JQL query, and only from a project admin — the same ADMINISTER_PROJECTS gate as every other write. It is passed to Jira's search API as a parameter, so Jira parses it; the app never concatenates it into anything.
The query runs as the app, not as the viewer, which is worth being precise about: it is bounded by the app's scopes rather than the reader's permissions. Two things contain that. The query is set by a project admin, who could read those issues anyway. And the picker now generates the query from a chosen project rather than expecting one to be typed, so the common path involves no hand-written JQL at all. The worst outcome is a query returning nothing useful, which the Test it dry run surfaces before saving.
CSV. Exported cells beginning =, +, - or @ are prefixed with an apostrophe, so a crafted Jira summary cannot become a formula when the file opens in Excel. This is tested (src/csv.test.js).
Rendered content. The UI renders text as React children, never dangerouslySetInnerHTML. Issue summaries and user names are escaped by React.
Stored configuration. Numeric settings are clamped to sane ranges on save; leave dates are validated and rejected if malformed. Records read from Jira issues are never written back into storage.
Data minimisation
- Issue descriptions are checked only for existence, never read or stored
- Backlog reads skip changelog expansion, since backlog items haven't moved
- The team property is pruned: leave and holidays older than ~400 days are dropped, which limits how much personal data accumulates as well as keeping the property small
- That property now holds one roster per board, so its size is checked across the whole property rather than per team — checking per team would pass and Jira would then reject the write. The limit is enforced at 30KB with a clear error naming the largest boards
- The Portfolio page reads several boards in one request. Each is read through the same code path and the same permission filter as the single-board view, and a board the viewer cannot browse is reported as an error row rather than silently omitted or bypassed
- Aggregation happens server-side, so raw issue payloads never reach the browser
Vulnerability disclosure
Report to support@foooverse.com.
- Acknowledgement within 2 business days
- Assessment and a fix plan within 5 business days
- We won't pursue legal action for good-faith research
This app does not currently participate in a bug bounty programme. Reports come directly to the address above and are handled by the developer who wrote the code.
Incident response
If a security incident affects customer data:
- Take the affected version off the Marketplace
- Notify affected customers within 72 hours
- Ship a fix and publish what happened
- Post a summary on the listing's security tab
There is no escalation chain: the person who receives the report is the person who fixes it. Contact support@foooverse.com for anything security-related, including during an incident. Atlassian is notified in parallel with customers, through the Marketplace security process.
What the app is not
Stated plainly, because it saves a round of questions:
- Not a leave management system — it reads leave, it doesn't approve or manage it
- Not a system of record — delete it and your Jira is unchanged
- Not a holder of your data — it is a processor under GDPR, but it processes in-flight inside your own site and keeps no copy. See the privacy policy
- Not able to modify your work — no issue, sprint or board write scope exists