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 thecanWritefield 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, includingVERSION_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, passversion: 0. -
Without
version— the SDK first reads the current version viaget(for an empty bucket,0is used), then performs thepatch. OnVERSION_CONFLICTthe 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 aconsole.warn.
Which mode to choose
- Pass
versionexplicitly 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
versionif 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. patchwithout an explicitversionmakes up to 2 retries onVERSION_CONFLICT. After that, the error is thrown to the caller.