---
metadata:
  - name: generator
    content: Diplodoc Platform v5.63.0
  - property: og:type
    content: article
  - property: article:section
    content: Платформа плагинов
  - property: og:title
    content: Plugin data storage
  - property: article:tag
    content: Техническая инструкция
alternate:
  - https://www.yandex.ru/support/tracker/en/plugins/storage.md
  - https://www.yandex.ru/support/tracker/ru/plugins/storage.md
  - href: https://www.yandex.ru/support/tracker/en/plugins/storage.md
    type: text/markdown
    title: Markdown version
  - href: https://www.yandex.ru/support/tracker/en/llms.txt
    rel: describedby
---
> **Documentation Index:** Fetch the complete configuration index at https://www.yandex.ru/support/tracker/en/llms.txt


# Plugin data storage

In addition to interacting with the [Tracker public API](https://www.yandex.ru/support/tracker/en/plugins/trackerApi.md), 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 {#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.

{% note warning "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](#versioning) and `patch` merge semantics.

{% endnote %}

{% note warning "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>`.

{% endnote %}

{% note warning "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.

{% endnote %}

## Basic usage {#basic-usage}

The `storageApi` object is exported from the SDK:

```typescript
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 {#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.

```typescript
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`](#errors) error.

## API {#api}

### storageApi.orgShared.get(bucket?) {#storage-api-org-shared-get}

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'` | The record's key within the context |

**Returns:** `Promise<StorageRecord | null>`

```typescript
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) {#storage-api-org-shared-patch}

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                 | Expected current version       |

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

```typescript
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?) {#storage-api-user-get}

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`](#storage-api-org-shared-get).

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

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

### storageApi.user.patch(options) {#storage-api-user-patch}

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`](#storage-api-org-shared-patch).

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

### storageApi.resource.get(options) {#storage-api-resource-get}

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>`

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

### storageApi.resource.patch(options) {#storage-api-resource-patch}

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.

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

## Versioning {#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`](#errors) 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`.

{% note info "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.

{% endnote %}

## Write access {#can-write}

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.

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

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

## Error codes {#errors}

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

```typescript
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](https://www.yandex.ru/support/tracker/en/plugins/trackerApi.md#error-codes) section.

**Example of handling `VERSION_CONFLICT`:**

```typescript
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 {#types}

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

### StorageRecord {#storage-record}

A storage record.

```typescript
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 {#storage-context-type}

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

## Limits {#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.
