Plugin data storage

In addition to interacting with the Tracker public API, a plugin has access to its own JSON storage — storageApi. It runs on the platform side, so the data doesn't depend on the user's device and is available in every tab where the plugin is open.

The storage is useful for plugin settings, lookup caches, and any data that does not need to be saved in Tracker as an entity. You can limit a record's scope to an organization, the current user, or a specific Tracker resource.

Contexts

Each record is stored in a context — it defines the data's visibility scope.

The following contexts are available:

  • orgShared — data shared across the whole organization. All plugin users in the organization can access the record. Use it for shared plugin settings and lookup caches.
  • user — personal data of the current user. You do not need to pass a user ID: the platform obtains it from the authenticated session. Use it for personal interface settings, selected filters, and element state.
  • resource — data for a specific Tracker resource. The required resourceId parameter identifies the resource, for example, issue:MYQUEUE-123, queue:MYQUEUE, board:42, project:<identifier>, portfolio:<identifier>, or goal:<identifier>. The record is shared by users who access the same resource.

For all contexts, the canWrite field indicates whether the current user can modify the record.

Shared record

The orgShared context is a record shared across the whole organization. Don't store the user's personal data in it, and keep in mind that concurrent write processes see each other's changes. For collaborative work, use versioning and patch merge semantics.

Resource record

The resource context is not tied to a user. All write operations that use the same resourceId and bucket access the same record. Pass a canonical resource identifier with its type: issue:<key>, queue:<key>, board:<identifier>, project:<identifier>, portfolio:<identifier>, or goal:<identifier>.

Do not store sensitive data

storageApi is not secure storage for secrets. Do not store tokens, passwords, access keys, or other sensitive data in it, regardless of the context.

Basic usage

The storageApi object is exported from the SDK:

import { storageApi } from '@weavix/tracker-plugin-sdk-react';
// or
import { storageApi } from '@weavix/tracker-plugin-sdk-core';

// Reading
const record = await storageApi.orgShared.get('settings');
// record: { data: { theme: 'dark' }, version: 5, canWrite: true, ... } | null

// Creating a record in an empty bucket
await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { theme: 'dark', notifications: true },
    version: 0,
});

// Updating without knowing the current version — the SDK reads it itself and retries the request on conflicts
await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { enabled: true },
});

// Deleting a single field — pass null
await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { theme: null },
});

// Personal settings of the current user
await storageApi.user.patch({
    bucket: 'preferences',
    data: { compactMode: true },
});

// Data associated with a specific issue
await storageApi.resource.patch({
    resourceId: 'issue:MYQUEUE-123',
    bucket: 'panel',
    data: { collapsed: true },
});

Buckets

Within a single context, a plugin can keep several independent records — each identified by the bucket parameter. It's just a string key. A record under one bucket doesn't overlap with a record under another.

await storageApi.orgShared.patch({ bucket: 'settings', data: { theme: 'dark' } });
await storageApi.orgShared.patch({ bucket: 'cache', data: { fetchedAt: Date.now() } });

If bucket isn't passed, the platform substitutes 'default'. A key that's invalid in format or length results in a BAD_KEY error.

API

storageApi.orgShared.get(bucket?)

Returns the current record for the bucket, or null if the record doesn't exist yet.

Parameters:

Parameter Type Default Description
bucket `string undefined` The platform substitutes 'default'

Returns: Promise<StorageRecord | null>

const record = await storageApi.orgShared.get('settings');

if (record) {
    console.log(record.data); // record contents
    console.log(record.version); // current version (for optimistic updates)
    console.log(record.canWrite); // whether the current user has write access
} else {
    // the record doesn't exist yet — it can be created via patch with version: 0
}

storageApi.orgShared.patch(options)

Merge patch: the fields from data are applied on top of the current record, and a null value removes the key. Returns the full merged StorageRecord with the new version — including fields written by concurrent write processes.

Parameters:

Parameter Type Default Description
bucket string The platform substitutes 'default' The record's key
data Record<string, unknown> — Merge doc; null removes a field
version `number undefined` auto-resolve

Returns: Promise<StorageRecord> — the full record after the patch is applied.

const updated = await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { theme: 'dark' },
});

updated.version; // version after the patch
updated.data; // full record contents (including other clients' fields)

storageApi.user.get(bucket?)

Returns the current user's personal record for the bucket, or null if the record does not exist yet. The platform identifies the user from the authenticated session, so the method does not take a user ID.

The parameters and return value are the same as for storageApi.orgShared.get.

const record = await storageApi.user.get('preferences');

if (record) {
    console.log(record.data); // settings for the current user only
}

storageApi.user.patch(options)

Applies a merge patch to the current user's personal record. The parameters, versioning, and return value are the same as for storageApi.orgShared.patch.

const updated = await storageApi.user.patch({
    bucket: 'preferences',
    data: { compactMode: true },
});

storageApi.resource.get(options)

Returns the record for a specific Tracker resource, or null if the record does not exist yet.

Parameters:

Parameter Type Default Description
resourceId string — Canonical identifier of the Tracker resource
bucket string | undefined The platform substitutes 'default' The record's key within the context

Returns: Promise<StorageRecord | null>

const record = await storageApi.resource.get({
    resourceId: 'issue:MYQUEUE-123',
    bucket: 'panel',
});

storageApi.resource.patch(options)

Applies a merge patch to a record for a specific resource. Unlike orgShared and user, every call requires resourceId.

Parameters:

Parameter Type Default Description
resourceId string — Canonical identifier of the Tracker resource
bucket string | undefined The platform substitutes 'default' The record's key
data Record<string, unknown> — Merge document; null removes a field
version number | undefined auto-resolve Expected current version

Returns: Promise<StorageRecord> — the full record after the patch is applied.

const updated = await storageApi.resource.patch({
    resourceId: 'issue:MYQUEUE-123',
    bucket: 'panel',
    data: { collapsed: false },
});

Versioning

The storage works on an optimistic-update scheme: every record has a version, and patch must pass the expected version. If the storage already has a newer version, the operation fails with a VERSION_CONFLICT error.

The patch behavior depends on whether you pass version:

  • With an explicit version — a single request is sent. Any error, including VERSION_CONFLICT, is thrown to the caller. Use this mode if the application itself keeps track of the current version and needs to react to a conflict — for example, to show the user "the data has changed, please reload." To create a record in an empty bucket, pass version: 0.

  • Without version — the SDK first reads the current version via get (for an empty bucket, 0 is used), then performs the patch. On VERSION_CONFLICT the cycle repeats up to twice, re-reading the version on each retry. Any other error is thrown immediately. In the worst case, the SDK accesses the platform 6 times: it performs 3 pairs of GET and PATCH requests. Each retry logs a console.warn.

Which mode to choose

  • Pass version explicitly if you've loaded the record onto the screen and the user is editing it — this way a conflict with another writer won't be silently lost.
  • Do not pass version if the patch is idempotent and its values do not depend on the current record contents — for example, when you set a single flag to a known value. For counters and other read-modify-write operations, reload the record and recalculate the patch yourself after a conflict.

Write access

The canWrite field in StorageRecord shows whether the current user can patch this record. Use it to show the user in advance that they don't have permission, without waiting for a platform error.

const record = await storageApi.orgShared.get('settings');

if (!record?.canWrite) {
    // hide or disable editing elements
}

Error codes

All storageApi errors are instances of PluginActionError with numeric codes. The constants are exported from the SDK:

import {
    VERSION_CONFLICT,
    DATA_TOO_LARGE,
    BAD_KEY,
} from '@weavix/tracker-plugin-sdk-react';
Constant Code When
VERSION_CONFLICT 1010 The passed version doesn't match the current one
DATA_TOO_LARGE 1011 The record size after the operation exceeded 256 KiB
BAD_KEY 1012 The bucket failed format or length validation

General API error codes (validation, scope, and so on) are in the API error codes section.

Example of handling VERSION_CONFLICT:

import {
    storageApi,
    PluginActionError,
    VERSION_CONFLICT,
} from '@weavix/tracker-plugin-sdk-react';

try {
    await storageApi.orgShared.patch({
        bucket: 'settings',
        data: { theme: 'dark' },
        version: knownVersion,
    });
} catch (e) {
    if (e instanceof PluginActionError && e.code === VERSION_CONFLICT) {
        // the data has changed — reload the record and offer the user to merge the changes
        const fresh = await storageApi.orgShared.get('settings');
        // ...
    } else {
        throw e;
    }
}

Types

import type {
    StorageRecord,
    StorageContextType,
    StorageGetPayload,
    StoragePatchPayload,
} from '@weavix/tracker-plugin-sdk-react';

StorageRecord

A storage record.

type StorageRecord = {
    /** Internal composite record key (context + bucket). */
    key: Array<
        | { organization_shared: string }
        | { user: string; bucket: string }
        | { resource_id: string; service_type: string; bucket: string }
    >;
    /** Record version. Pass it to patch for optimistic updates. */
    version: number;
    /** The record's payload data. */
    data: Record<string, unknown>;
    /** Whether the current user can patch. */
    canWrite: boolean;
    /** Record creation date (ISO 8601). */
    createdAt: string;
    /** Date of the last update (ISO 8601). */
    updatedAt: string;
};

StorageContextType

type StorageContextType = 'orgShared' | 'user' | 'resource';

Limits

The following quotas apply by default:

What is limited Scope Limit Error when exceeded
Record size after an operation One bucket 256 KiB DATA_TOO_LARGE
Total data volume All contexts and buckets of one plugin in an organization 100 MiB STORAGE_QUOTA_EXCEEDED
Number of orgShared buckets One plugin in an organization 2000 BUCKET_LIMIT_EXCEEDED
Number of user buckets One plugin for each user in an organization 100 BUCKET_LIMIT_EXCEEDED
Number of resource buckets One plugin for each resource in an organization 200 BUCKET_LIMIT_EXCEEDED
  • An unused record in a bucket is retained for 3 calendar years.
  • A bucket must be 1 to 64 characters long. It can contain Latin letters, digits, periods, hyphens, and underscores. An invalid key causes BAD_KEY.
  • In the resource context, resourceId is required, cannot be empty, and cannot exceed 256 characters.
  • patch without an explicit version retries up to twice on VERSION_CONFLICT. The error is then thrown to the caller.