Key-Value Store
Custom Entity Store

In 2024, Forge hosted storage will automatically include data residency. This means that all app data in Forge hosted storage will inherit data residency, in all current and future Atlassian-supported regions.

This implementation will provide your customers with greater control over their app data’s location. For more information, read this announcement.

Custom Entity Store

The Custom Entity Store lets you store data in custom entities, which are data structures you can define according to your app's needs. Custom entities let you assign multiple values (or "attributes") to a single key (or "entity") and define indexes to optimize queries against these values.

To start, import the Forge API package in your app, as follows:

1
2
import { storage } from '@forge/api';

Each installation of your app is subject to the Storage API's quotas and limits. See Storage quotas and Storage limits for more details.

The app storage API requires your app to have the storage:app scope. See Add scopes to call an Atlassian REST API guide to add new scopes to your app.

For a detailed tutorial on storing and querying structured data through custom entities, see Use custom entities to store structured data.

Limitations

Stored entity values must adhere to the following type requirements:

TypeRequirements
integerMust be a 32-bit signed integer, with the following value limits:
  • Minimum value: -2,147,483,648
  • Maximum value: 2,147,483,647 (inclusive)
floatThe value must either be 0, or fall within the following range limits (both inclusive):
  • Positive range: 1E-130 to 9.9999999999999999999999999999999999999E+125
  • Negative range: -9.9999999999999999999999999999999999999E+125 to -1E-130

We provide 38 digits of precision as a base-10 digit.

string
  • Must be a free-form, URT8 character sequence
  • Must contain at least one non-whitespace character
  • Must not be empty
booleanCan only be true or false
any

The any type supports the following values:

  • string
  • integer
  • float
  • boolean
  • object
  • array

The Custom Entity Store strictly enforces attribute types. Attempting to store a value whose type doesn't match its field will result in an error (for example, when you try to set a string value to an attribute with an integer type).

Partitioning

All data stored within the app storage API is namespaced. The namespace includes which Atlassian site, product, and Forge environment your app is installed on. As a result:

  • Only your app can read and write your stored data.
  • An app can only access its data for the same environment.
  • Your keys only need to be unique for an individual installation of your app.
  • Data stored by your app for one product is not accessible from other products. For example, data stored in Jira is not accessible from Confluence or vice versa.
  • Your app cannot read data from different sites, products and app environments.
  • Quotas and limits are not shared between individual installations of your app.

Data residency

The Atlassian cloud provides features that allow admins to control and verify where their Jira and Confluence data is hosted. These features allow them to meet company requirements or regulatory obligations relating to data residency.

The Storage API uses this same cloud infrastructure to store data. This allows Forge to extend similar data residency features to your app. All data stored on the Storage API automatically inherit these features.

Specifically, if your app stores data through the Storage API, an admin can control where that data is stored. For more details about how this works, see Data residency.

Eventual consistency

The read and query APIs within the app storage APIs are eventually consistent. This means that the data returned from the API may be slightly out of date.

Conflict resolution

Writes to keys using set or delete use a "last write wins" conflict resolution strategy. Writes to individual keys are atomic - For example, the value is either updated in full or not.

Supported values

The app storage API is able to persist any JSON data type except null. For example:

  • arrays
  • booleans
  • numbers
  • objects
  • strings

The JavaScript storage API serializes your objects using JSON.stringify, and as such removes functions and the value undefined from any object you attempt to serialize.

Cursors

The Key-Value Store and Custom Entity Store use cursors for paginated data access. Queries (both through the Query builder and Complex Query builder) return a cursor in the results. This cursor can be provided to subsequent queries to paginate over larger data sets than you would otherwise be able to fetch.

1
2
// Fetch a page of 10 results
const { nextCursor } = await storage.query().getMany();

// Fetch the next 10 results
await storage.query().cursor(nextCursor).getMany();

Cursors are derived from underlying storage identifiers, and hence are subject to change anytime there is any change in how these underlying storage identifiers are created. This means that cursors are not stable and should not be persisted.

You will have to use the same parameters as the initial query. See the example below.

1
2
// Fetch 15 results
const { nextCursor } = await storage
    .query()
    .limit(15)
    .where('key', startsWith('account.'))
    .getMany();

// This is expected usage
await storage.query()
    .limit(15)
    .where('key', startsWith('account.'))
    .cursor(nextCursor)
    .getMany();

// This will produce undefined results
await storage.query()
    .cursor(nextCursor)
    .getMany();

Key ordering

Keys are lexicographically ordered; this means, for example, that the Query builder and Complex Query builder) will return entities ordered by their key. This property can be used to group related entities or build ad-hoc indexes.

The Key-Value Store and Custom Entity Store don't support indexing of arbitrary attributes. However, it is possible to support this sort of access pattern if your keys are constructed in such a way as to support indexed reads.

Hierarchical keys can be constructed to allow for nested entities to be fetched in a single list operation such as the example below.

1
2
import { storage, startsWith } from '@forge/api';

// Nested entities
await storage.set('survey-responses#1#{UUID}', { });
await storage.set('survey-responses#1#{UUID}', { });
await storage.set('survey-responses#1#{UUID}', { });
await storage.set('survey-responses#1#{UUID}', { });

const results = await storage
  .query()
  .where('key', startsWith('survey-response#1#'))
  .limit(10)
  .getMany();

Rate this page: