Vendor: Lichen. Contact: support@lichenapps.com Last updated: 2026-09-05
Sojourn reads the status-change history of the Jira issues it can see and turns it into per-status durations. Everything it derives is stored inside Atlassian's own infrastructure, in storage provisioned for your installation of the app on your site. The app makes no outbound network requests to anywhere except your own Jira site. It has no servers of its own, no analytics, no error-reporting service, and no third-party components that phone home. The vendor cannot read your data.
This is not a policy commitment that could quietly lapse. It is a structural property of how the app is built. A Forge app can only reach the outside world through declared egress or a web trigger, and this app declares neither. That is also what earns it Atlassian's Runs on Atlassian badge: Atlassian evaluates those properties for every version of the app that is deployed, so the claim is checked by the platform and not only asserted here.
The app authenticates as itself (as the app, not as you) and requests exactly these things. One consequence of that is worth stating before the table: the app sees the projects it has been granted rather than the projects you personally can open, so a report covers all of them and looks the same to everyone who can open the app. Section 3 says what follows from that for settings.
| What | Why |
|---|---|
| The list of projects it can see, and each project's avatar image address | A whole-instance query is rejected by Jira, so work is partitioned by project. The avatar address arrives in the same response and is used to draw the project's icon beside its key in the report, so two projects with similar keys can be told apart at a glance. The address is never stored, and the picture is loaded by your browser from Atlassian's own API, not by the app and not from anywhere else |
| Per issue: key, created date, project, status, issue type, assignee | These six fields and no others. Every search that reads an issue names them explicitly. The one search that builds a JQL scope asks for the issue key alone, which is less, never more |
| Per issue: the change history (changelog) | This is the raw material. It is where "moved from To Do to In Progress at 09:14" lives |
| A notification when an issue is updated | So the history stays current without re-reading everything |
About the change history and the update notification, stated precisely because they are the two places other data passes through the app. Jira's changelog endpoint returns all recorded changes for an issue, which can include edits to fields the app does not care about: a summary being rewritten, a description being edited. The update notification Jira sends when an issue changes carries the same kind of thing, plus whatever fields Jira chooses to put on the issue in that payload. In both cases those entries arrive in the app's memory as part of the message. The app reads only the status-change entries out of them and stores nothing else. Nothing from any other field is written to storage, and nothing is sent anywhere. The app never requests issue summaries, descriptions, comments, attachments or custom fields as fields.
This is the complete list. There are no other tables.
The app is also told, by Atlassian, with each request, whether this site holds an active subscription to it. That answer is read and acted on; it is not written to any table above and it is not sent anywhere.
| Table | Every column it holds |
|---|---|
intervals, one row per status period | a row id; issue key; project key; status name; entered-at and exited-at timestamps; duration in ms; a sequence number; which write path produced it; assignee display name; issue type; a bucket number for that duration; the same duration counted inside your working calendar; a bucket number for that; and a flag recording whether the period was entered outside your working hours. The last three are empty until you save a working-hours calendar: the app derives no working-hours figure against a calendar you have not given it |
issues, one row per issue | issue key; project key; issue type; assignee display name; current status; created-at; last-updated-at |
issue_cycle, one row per issue | issue key; project; assignee display name; issue type; created-at; completed-at; number of passes through the workflow; eight cycle and lead-time figures, being three definitions of cycle time and one of lead time, each also counted inside your working calendar, with a bucket number for each of the eight; the time the issue spent in the statuses you group as blocked, also counted inside your working calendar, with a bucket number for each; a fingerprint of the cycle time definition the figures were computed under, which is the statuses you mark as in progress, as done, and as blocked; a fingerprint of the working calendar the working-hours figures were computed under; and when the row was last written |
evt_log, the update ledger | a row id; issue key; the Jira changelog entry id; when the transition happened; when it was stored; the difference between the two; and which of the timestamp formats Jira sent the transition time in. Rows older than thirty days are deleted |
cycle_state, one row | a key naming the row; the cycle time definition the rebuild is running under, as a fingerprint; one issue key used as a cursor; how many issues are done; a status word; and two timestamps. No issue data beyond that one key |
bf_state, one row | a key naming the row; the history build's cursor and the list of projects in scope, each as a short piece of text; counters (issues done, rows written, pages, invocations, issues whose history could not be read, requests throttled, time spent waiting, requests shed); two timestamps; a status word; a stop flag; and a note field this version never fills |
settings, a handful of rows | a key, a value and a timestamp per row. The values are your working-hours calendar and a fingerprint of it; your status-group definitions; your cycle time definition, which is the statuses that count as in progress and as done; timing counters from the last history build; and a random secret, generated once for your installation, that is used to derive your saved-view key |
view_state, one row per user per view | a one-way hash of your Atlassian account ID, and never the account ID itself; which view it is; the view itself (chosen project, grouping, JQL text, the date range chosen in the date pickers, and whether the status-groups, working-hours and cycle-time toggles were on); and a timestamp |
scope_meta / scope_set | an id for the scope; a JQL string you typed; when it was built; how many issues it matched; a status word; and, in the second table, that id paired with each issue key it matched. Cached so the same scope is not re-fetched |
warm_log | a row id; when the ping ran; an id and an age for the container that ran it; how long its query took; and which kind of ping it was. No issue data, no user data. Only the most recent 4,032 rows are kept; older ones are deleted |
The report covers only projects you can browse in Jira. Per-issue lists show only issues you can browse. Aggregate figures within a project you can browse may include issues that an issue security level hides from you; per-issue lists do not.
A per-issue list - the one behind a "Now" count - additionally checks every issue it is about to show against your own Jira permissions, one by one, each time you open it. It reads only the issue key to do so, and stores nothing from that check.
To report a security issue, or to read how an incident would be handled, see the security page at https://lichenapps.github.io/sojourn/security/
The app BUILDS its history reading Jira as itself, because a background build has no user to run as; what it SHOWS you is checked against your own Jira permissions, as you, on every load, with no caching, so a permission change takes effect the next time you open a report. The page states how many projects were included. It never states, names or counts what was left out - Jira does not confirm that a project you cannot browse exists, and this app does not do what Jira refuses to do.
One of the columns above is personal data. A second is derived from personal data and is called out beside it, because the distinction is the sort a reader should be able to check rather than take on trust:
view_state is keyed by a value derived from it with a one-way hash, under a random secret that is generated once for your installation, kept in that installation's own storage, and never sent anywhere. The stored value is a hash and not an encrypted copy, so there is no key that turns it back, and it is meaningless outside your own installation because the secret it was derived under never leaves it. It is a key: never displayed, and not joined to anything else. A request that carries no account at all saves no view, rather than being given a fallback key it would share with every other such request.The app stores no Atlassian account IDs, no email addresses, no avatars, no names other than the assignee display name above, and no free text written by a person other than a JQL query they typed themselves.
The app is installed once for the whole site, and the tables above are the site's, not any one user's. Three things follow, and they are stated here rather than left to be discovered:
view_state is the one table with a row per person, keyed by the derived value described above, and it holds the view you last chose and nothing else.If that is broader than your site wants, the control is who you grant access to the app.
warm_log keeps its most recent 4,032 rows, a fortnight at one row per five minutes, and evt_log keeps thirty days of transitions. Older rows are deleted, not archived.The app requests read-only scopes. There are no write scopes and no egress permissions. Every scope below is held for a call the app actually makes; the calls are the complete list, not a sample.
| Scope | The call that needs it |
|---|---|
read:jira-work | Classic scope required by Jira's search and changelog endpoints through the Forge proxy. The granular scopes below are not sufficient on their own: without this one, every search returns 401 |
read:jira-user | The other half of that pair, and the assignee display name that comes back on a search result |
read:jql:jira | POST /rest/api/3/search/jql and POST /rest/api/3/search/approximate-count. The JQL you type in the report's scope box is parsed and counted by Jira before any of it is fetched |
read:issue.changelog:jira | POST /rest/api/3/changelog/bulkfetch. This is the app's whole raw material: the status-change history |
read:issue-meta:jira | The created date on each issue, which is where every duration starts |
read:issue:jira | Reading the issues a search matched at all, and GET /rest/api/3/issue/{key}?fields=key - one issue, asked for its key alone. Reconciliation uses it to tell an issue that MOVED from one that was deleted: Jira's search index takes a few seconds to catch up with a move, and stored history must never be deleted because a search was briefly out of date |
read:project:jira | GET /rest/api/3/project/search, the list of projects, and the project field on each issue. A whole-instance query is rejected by Jira, so work is partitioned by project. The same endpoint is called AS YOU, with action=browse, on every report load, to establish which projects you may browse; the answer is used and not stored |
read:status:jira | The status field, and the status names inside the change history |
read:field:jira | Resolving the six field names the app asks for by name in every request |
read:issue-type:jira | The issuetype field, which reports group by |
read:user:jira | The assignee's display name, the one piece of personal data the app stores |
Holding a permission that is never used is a claim a customer cannot check, so two that this app does not use were removed on 2026-08-28 rather than left in place:
read:issue-details:jira: the app never requests an issue summary, description, comment, attachment or custom field. The six fields it does request are listed in section 2.read:avatar:jira: the app never requests, reads or stores an avatar.Neither is requested any more, and this is the whole of what was removed. The app was redeployed and re-installed without them, and its full pipeline (the history build, the incremental update path, the JQL scope builder and the report) was then exercised against real Jira data, because a missing permission is enforced by Atlassian at request time and cannot be caught by any test that does not make the request.
None. There are no sub-processors, because there is no processing anywhere except inside your own Atlassian instance.
The app holds no credentials of its own. It runs on Atlassian Forge, authenticates as an app through Atlassian's own mechanism, and stores data only through Atlassian's provisioned storage. The vendor operates no servers, holds no keys, and has no path to your data.
If a future version of the app ever needed to send data anywhere, it would need declared egress in its manifest, would lose the Runs on Atlassian badge, and would require you to approve the new permission before it could be installed. That is not a promise; it is how the platform works. This policy would be updated before any such version was submitted.
support@lichenapps.com