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 host 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 user settings, lookup caches, and any data the plugin needs to keep close to the user without saving it in Tracker as an entity.

Contexts

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

Currently, a single context is available:

  • orgShared — data shared across the whole organization. The record is visible to all plugin users in that organization. Write access is granted by the host in the canWrite field of each record read.

The list of contexts may expand in the future — for example, a user-level context. The API is designed so that adding a new context won't break existing code.

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.

Basic usage

The storageApi object is exported from the SDK:

import { storageApi } from '@weavix/sdk-react';
// or
import { storageApi } from '@weavix/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: { count: 42 },
});

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

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 host 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 host 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 host 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)

Versioning

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

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, this results in 6 requests to the host (3 GET + PATCH pairs). 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.
  • Don't pass version if the change doesn't depend on the previous state — for example, incrementing a counter or appending a new field.

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 an error from the host.

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/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/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/sdk-react';

StorageRecord

A storage record.

type StorageRecord = {
    /** Internal composite record key (context + bucket). */
    key: Array<{ organization_shared: 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';

Limits

  • The maximum record size is 256 KiB after the operation is applied. Exceeding it results in DATA_TOO_LARGE.
  • The bucket key is validated by the host for format and length. An invalid key results in BAD_KEY.
  • patch without an explicit version makes up to 2 retries on VERSION_CONFLICT. After that, the error is thrown to the caller.