Tidy Find & Replace for Confluence
Data and security statement
In short. Tidy Find & Replace runs entirely on Atlassian's Forge platform. It talks only to Confluence, as the person using it, to Atlassian's own storage and personal data reporting service, and, on sites with Rovo, to Rovo, Atlassian's AI, which calls the app's agent as the person who asks. It has no servers of its own and sends nothing to us except log lines made of IDs, codes and counts. It can do only what the person using it is allowed to do in Confluence. It edits page text, and optionally titles, link addresses and code, and checks every edit before and after it saves. It keeps job history for a limited time, plus working notes that stop two jobs running at once in one place. It stores Atlassian account IDs and the find texts and replacements people type, but never page content, titles, excerpts, names, email addresses or credentials. It reports the account IDs to Atlassian on a regular cycle and erases a person's account ID when Atlassian reports that person's account closed.
This statement describes Tidy Find & Replace version 1, with its Rovo agent. Each answer comes from the app's code and manifest, except where it says "Atlassian's documentation", which marks a statement about the Forge platform that Atlassian makes. It is not a certification and not a statement of compliance with any law or standard. The privacy policy is the one place that describes retention, reporting and erasure: where this page points to it, the policy is the answer.
Hosting
Atlassian Forge, built to the "Runs on Atlassian" model: all code runs, and all data is
stored, on Atlassian's platform. No external servers, no egress, no remote calls. The manifest declares
no remotes and no external permissions, and the app has no web triggers or event triggers. Its one
scheduled trigger runs the personal data job. forge eligibility reported the development build eligible for Runs on Atlassian on
2026-10-27; it is run again on the production build. The screens are built with Atlassian's UI Kit; the app
serves no web pages, scripts or files of its own. Data flow: the person's browser talks to the app's
Forge function on Atlassian's platform, and that function calls Confluence's own REST APIs as that
person and reads and writes Forge storage. The hourly function is a second, separate function: it runs
as the app, with no user, reads and writes Forge storage, calls Atlassian's personal data reporting API,
and never calls Confluence. On sites with Rovo, the app's Rovo agent calls two more functions of its own
(one for each action), which no screen reaches; each calls Confluence as the person who asked in Rovo,
and reads and writes Forge storage.
Page content
Read from Confluence in Atlassian's document format and changed in memory, inside the function call that needs it. Excerpts for the preview are computed when a person opens a page's matches, for that person, and never stored. Page bodies and titles are never written to storage or logs.
Data stored
See the data inventory below. It is kept in Forge storage (the key-value store), scoped to the installation. Atlassian's documentation says only the app can read and write its stored data. The app stores nothing in the browser.
Encryption
Done by Atlassian's platform. Atlassian's documentation says Forge storage data is transferred, encrypted and stored according to Atlassian cloud's data encryption policies. The app adds no encryption of its own.
Storage location
Forge storage only, hosted by Atlassian. Atlassian's documentation says an admin can control where that data is stored (data residency). The app sends no stored data to us or to any third party.
Retention
See "How long" in the privacy policy. The inventory below gives the expiry of each kind of record.
Personal data
The records that hold an Atlassian account ID are marked in the inventory. Find texts,
replacements, filters and the keys of ordinary spaces come from your users, can contain personal
information, and are neither reported nor erased; they expire with the job (or, in a request from Rovo
that waits, within 24 hours). Reporting and erasure: the
privacy policy. The scope report:personal-data is used only by the hourly job; it sends account IDs
only, never the ID unknown, at most 90 a call.
Access control
- Every Confluence read and write runs as the signed-in person (
asUser, through one function insrc/adapters/content.ts). The app never calls Confluence as itself (asApp) and has no credentials of its own, so effects never exceed that person's Confluence permissions. Before each save the app also asks Confluence whether the person may update that page. - Each request is bound to the screen it came from: the app takes the screen (module), and for the space
and page screens the space or page, from the context Forge supplies, never from the request. The
site-wide page needs Confluence admin rights, checked on every call
(
GET /wiki/rest/api/user/current?expand=operations, theadministeroperation). The space screen checks on every call that the person may administer that space (GET /wiki/api/v2/spaces/{id}/operations), and acts only in that space. The page action acts only on its own page and the pages below it. - The page action carries a display condition (
hasPagePermission: EDIT) so that Confluence shows it only to people who can edit the page; Atlassian's documentation says display conditions are not a protection, which is why everything also runs as the person. - Confluence admins can switch off the space screen and the page action; their functions then refuse work, except pause and cancel, so a running job can always be stopped.
- Jobs are visible by screen: the site-wide page lists every job; the space page the jobs in its space; the page action only the person's own jobs for that page. Any other job ID answers "not found". Whoever can see a job can resume, pause, cancel or undo it, as themselves.
- Preview and report rows are checked again for each viewer: the app asks Confluence, as the viewer, for the current titles, and shows only pages the viewer can open now. Others are counted, with no ID or title.
- The licence is checked on every call from a screen, except the start-up call that explains an inactive licence.
- The Rovo agent's two actions (planning a find and replace, and the migration scan) each have their own
function, bound to its action by the key Forge supplies. The person is the account Forge gives the
action (
context.principal), never an input. Access is checked as the screens check it, as that person: a Confluence admin in any space, a space admin in their own space when the Settings switch allows it, anyone else refused. Only the declared inputs are read, each checked like a form field. Planning needs an active licence; the scan, like the screen's, does not. Neither action changes a page: planning creates a job that the person previews and starts in the app, and the scan only reads.
Permissions requested
Ten scopes in the manifest: read Confluence users, search, read spaces, read and write pages, read and write blog posts, read hierarchical content (page trees), app storage, and report personal data. Page and blog-post writes are the only write access. The app does not touch comments, attachments, restrictions, labels or settings.
Content safety checks
Before saving, the edited document is compared with the original, and any difference other than the planned text changes stops the save. After saving, the saved version is read back and compared; if Confluence stored something else (other than the equivalent link forms it writes itself, which are compared by what they point to), the previous version is saved back. Pages with a link that Confluence would remove or rewrite on a save (a macro, file, emoji, date, placeholder or image from another website inside a link, a link to an attached file, or a shortcut link) are never saved, and neither are pages with content in an older Confluence format that the app has not seen survive a save. Pages with unpublished changes are skipped, a person's draft that takes the app's own change back included: the app tells it from Confluence's copy lagging one save behind by when each was written.
Input handling
Every request is validated against a strict schema, and unknown fields are refused.
Find texts and replacements are length-limited and refuse control characters. Regular expressions run in
a linear-time engine (RE2, the re2js package pinned at 2.8.6), so no pattern can hang a function; a
job whose patterns take over a second to compile is refused. Search values are escaped. Advanced CQL is
accepted from Confluence admins only, up to 2,000 characters, with balanced parentheses and no ORDER BY,
combined with the app's own clauses and run as that admin. Content IDs must be digits.
Logging
Structured log lines (Forge logs): event name, job IDs, chunk and item positions, kind of scope, status and result codes, flags, counts, function names, error class names and HTTP status codes. Never an account ID, page title, page text, find or replacement text, link address or space key.
Secrets
None. The app holds no credentials, tokens or API keys.
Sub-processors
None besides Atlassian.
Vulnerability reports
Support@prosoftwares.dev. We acknowledge a report within 24 hours. We follow Atlassian's security bug fix policy for Marketplace cloud apps, which sets the longest time we may take to fix a vulnerability, by severity: critical (CVSS 9.0 or higher) 10 days, high (7.0 to 8.9) 4 weeks, medium (4.0 to 6.9) 12 weeks, low (below 4.0) 25 weeks, counted from the day it is reported or triaged, in calendar days. The policy: https://developer.atlassian.com/platform/marketplace/security-bugfix-policy/
Development practice
Strict TypeScript, linting with no warnings allowed, automated tests with at
least 90 % line coverage on every logic and data-access directory (the test run fails below that),
dependencies pinned to exact versions, a dependency audit (npm run audit:prod, 0 findings on
2026-10-27), and a manual test checklist run on a Confluence Cloud test site before release (on
development builds 4.2.0 to 4.13.0 on 2026-10-27 and 2026-10-28, the first run including a scan of the
app's log lines for anything they must not hold, and the Rovo agent's checks on builds 4.17.0 and 4.18.0
on 2026-10-31).
Data inventory
Every kind of record the app stores in Forge storage. Records that hold an account ID say so.
| Record (key) | What it holds | How long it is kept |
|---|---|---|
Job record job:<jobId> |
The request (pairs of find text and replacement, options, place, filters), the account ID of the person who started it, status, times, progress, result counts, links to an undo job or its original. While reading pages: its place in Confluence's lists. While waiting for Confluence's hourly allowance: the time it continues. | Expires 90 days after the job was created. |
Items ops:<jobId>:<n> |
Up to 500 pages a chunk: content ID, page or blog post, version read, match counts by kind, flags. | Same as the job. |
Selection sel:<jobId>:<n> |
Unticked pages and unticked match numbers, with counts. Only once someone unticks something. | Same as the job. |
Results res:<jobId>:<n> |
Per page: result code, versions before and after, counts replaced and skipped, notes, retry counters and marks. | Same as the job. |
Preview preview:<jobId> |
Totals, counts and warnings. No titles, no page text. | Same as the job. |
Planned IDs planned:<jobId> |
While a job reads pages: the content IDs already planned. | Deleted when reading ends; at most as long as the job. |
Pause or cancel request intent:<jobId> |
The word pause or cancel. | Deleted when carried out; otherwise expires with the job. |
Lease lease:<jobId> |
A timestamp, held while a step writes to a job. | Deleted when the step ends; the hourly job deletes any an hour old. |
Scope lock lock:site, lock:space:<spaceId> |
The job ID, the account ID of the person running it, the last heartbeat, and the end of a wait for Confluence. | Deleted when the job finishes, is cancelled or is trimmed; taken over after 15 idle minutes; deleted by the hourly job when its job is finished or gone, or its holder was reported closed. At most as long as its job. |
Settings settings:access |
The two switches. | While the app is installed. |
Request from Rovo rovo:request:<encoded account ID> |
A request from Rovo that waits for its person, kept only when Confluence asked that person to allow the app before the agent could act for them: the account ID, its kind, and for a find and replace the find text, the replacement, the three options and the space key named (or none), or for a migration scan the space key named (or none); when it was made. One a person, replaced by their next one. | Up to 24 hours (its expiry); deleted earlier when used or dismissed. Reported and erased like a job's creator. |
Rovo scan lease lease:rovo-scan-site, lease:rovo-scan-space-<spaceId> |
A timestamp, held while the Rovo scan action finds or starts the scan of a place. No account ID. | Deleted when the action is done; the hourly job deletes any an hour old. |
Report record privacy:reported:<encoded account ID> |
The account ID and the time it was last sent. | 30 days after it was written; renewed at each report. |
Closed-account mark privacy:pending:<encoded account ID> |
The account ID, while its erasure is unfinished. | Deleted as the last step of the erasure. |
Refused-ID mark privacy:unreportable:<encoded account ID> |
The account ID and the HTTP status of the refusal. | 30 days. |
Proof record privacy:proof |
The time Atlassian last took IDs. No account ID. | 7 days. |
The key of a personal space (~ and its owner's identifier) in a job's place names that person; it is
replaced when the owner is reported closed.
| Record (key) | What it holds | How long it is kept |
|---|---|---|
Scan record job:<scanId> |
A job record of the type scan: where it reads, the account ID of the person who started it, status and progress, and what it counted: the site's own address; candidate old-server host names (at most 50 by name) and the confirmed ones with their rules, each with link counts by pattern; macro names from apps (at most 200; a Forge macro's app ID and module key), each with a page count and one page ID; the spaces holding old links, by ID (keys only for spaces the scope names); the headline counts and the 20 pages with the most links, by ID. |
Expires 90 days after the scan was created; counts among the newest 500 jobs. |
Scan rows scanops:<scanId>:<n> |
Up to 500 pages a chunk, at most 10,000 pages: content ID, page or blog post, version read, space ID, link counts by (host, pattern) and flags. | Same as the scan. |
Report-only lists scanrep:<scanId>:<kind> |
Up to 2,000 content IDs per list: pages with app macros, pages with legacy content, pages the app could not read. | Same as the scan. |
Pages counted past the rows scanpast:<scanId>:<n> |
While the scan reads: content IDs, 5,000 a chunk, so that no page is counted twice. | Deleted when the scan finishes; at most as long as the scan. |
Fix lookup table table:<jobId>:<n> |
For each old address a fix job looks up: a one-way hash of the address made with a secret kept in the job record, and what it resolves to (a page or blog post ID with its space key, a space key, or the reason it needs a manual edit). No title and no address. At most 20,000 entries, 2,000 a chunk. | Same as the fix job. |