Storage API
Persistent, extension-scoped storage — key-value pairs and private directories.
Overview
The Storage API provides two storage mechanisms under a single storage namespace:
storage.kv— A key-value store (likelocalStorage). Keys and values are plain strings, scoped per extension. Data persists across app restarts.storage.getDataDir()/storage.getCacheDir()— Private filesystem directories for storing files of any size.
No permissions are required. Both are available to all extensions by default.
Key-Value Storage (storage.kv)
storage.kv.setItem
Store a string value under a key. If the key already exists, the value is updated in place (upsert). Creating a new key counts against the per-extension key limit; updating an existing key does not.
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The key to store the value under. Max 256 characters. |
value | string | Yes | The string value to store. Max 4096 characters (~4 KB). |
Returns: Promise<void>
Errors:
| Condition | Error message |
|---|---|
| Key exceeds 256 characters | Storage key too long (max 256 characters) |
| Value exceeds 4096 characters | Storage value too large (max 4096 characters) |
| Extension already has 1000 keys and this is a new key | Storage quota exceeded (max 1000 keys per extension) |
import editor from "@srcnexus/ext-sdk";
// Simple string
await editor.storage.kv.setItem("theme", "dark");
// Store a complex value by serializing to JSON
await editor.storage.kv.setItem("settings", JSON.stringify({
fontSize: 14,
wordWrap: true,
language: "en",
}));
storage.kv.getItem
Retrieve the value stored under a key.
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The key to look up. |
Returns: Promise<string | null> -- the stored value, or null if the key does not exist.
const theme = await editor.storage.kv.getItem("theme");
if (theme !== null) {
console.log("Stored theme:", theme);
} else {
console.log("No theme saved yet");
}
// Retrieve and parse a JSON value
const raw = await editor.storage.kv.getItem("settings");
if (raw !== null) {
const settings = JSON.parse(raw);
console.log("Font size:", settings.fontSize);
}
storage.kv.removeItem
Delete a single key-value pair. If the key does not exist, this is a no-op.
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The key to remove. |
Returns: Promise<void>
await editor.storage.kv.removeItem("theme");
storage.kv.clear
Remove all key-value pairs stored by the calling extension. Other extensions' data is not affected.
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | -- | -- | -- |
Returns: Promise<void>
// Wipe all stored data for this extension
await editor.storage.kv.clear();
storage.kv.getAll
Retrieve every key-value pair stored by the calling extension as a single object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | -- | -- | -- |
Returns: Promise<Record<string, string>> -- an object mapping every stored key to its value. Returns an empty object {} if nothing has been stored.
const allData = await editor.storage.kv.getAll();
console.log(allData);
// { theme: "dark", settings: "{\"fontSize\":14,\"wordWrap\":true}" }
Limits and Quotas
The key-value storage enforces hard limits to prevent any single extension from consuming excessive resources:
| Limit | Value | Notes |
|---|---|---|
| Max key length | 256 characters | Enforced per call to setItem. |
| Max value size | 4,096 characters | Approximately 4 KB per value. |
| Max keys per extension | 1,000 | Only new keys count; updating an existing key is always allowed. |
Notes:
- Updating an existing key does not count against the 1,000-key limit and will always succeed, as long as the key and value lengths are within bounds.
- If you need to store values larger than 4 KB, split them across multiple keys or consider compressing the data before storing.
Directory Storage (storage.getDataDir / storage.getCacheDir)
For files of any size — binary data, large JSON, assets, databases — use the directory methods.
storage.getDataDir
Get the extension's private data directory (persistent). Created on first call. Only deleted when the extension is uninstalled.
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | -- | -- | -- |
Returns: Promise<string> — the file:// URI of the data directory.
import editor from "@srcnexus/ext-sdk";
const dataDir = await editor.storage.getDataDir();
console.log(dataDir);
// file:///data/data/<package>/app_flutter/extension_data/my-extension/
storage.getCacheDir
Get the extension's private cache directory. Intended for temporary or re-creatable data. The system may clear cache files when storage is low.
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | -- | -- | -- |
Returns: Promise<string> — the file:// URI of the cache directory.
import editor from "@srcnexus/ext-sdk";
const cacheDir = await editor.storage.getCacheDir();
console.log(cacheDir);
// file:///data/data/<package>/cache/extension_cache/my-extension/
Working with Files
The directory methods only return paths. Use the File System API (workspace.fs.*) to read, write, create, or delete files. Extensions have implicit access to their own directories — no fileSystem permission required.
import editor from "@srcnexus/ext-sdk";
const dataDir = await editor.storage.getDataDir();
// Create a file
const fileUri = await editor.workspace.fs.create(dataDir, "config.json");
// Write to it
await editor.workspace.fs.write(fileUri, JSON.stringify({ theme: "dark" }));
// Read it back
const content = await editor.workspace.fs.read(fileUri);
const config = JSON.parse(content);
// List directory contents
const files = await editor.workspace.fs.list(dataDir);
// Delete a file
await editor.workspace.fs.delete_(fileUri);
Data vs Cache — When to Use Which
| Data Directory | Cache Directory | |
|---|---|---|
| Purpose | Important, persistent data | Temporary, re-creatable data |
| Survives restarts | Yes | Yes (but system may clear) |
| Deleted on uninstall | Yes | Yes |
| Use for | User preferences files, downloaded assets, databases | HTTP response caches, temporary processing files |
Data Isolation
Each extension's storage (both key-value and directories) is fully isolated:
- Storage is scoped by the extension's unique
extensionId. An extension can only access its own data. - The
extensionIdis injected automatically by the extension host when routing RPC calls -- extensions cannot forge or spoof another extension's ID. - There is no cross-extension storage access. Extensions that need to share data should use the Messaging API instead.
Cleanup and Lifecycle
- Uninstall: When an extension is uninstalled, all storage (key-value entries and directories) is automatically deleted.
- Manual clear: Call
storage.kv.clear()to remove all key-value entries. Useworkspace.fs.*to clean up directory contents. - Removing individual keys: Use
storage.kv.removeItem(key)to delete a specific entry and free up a slot in the 1,000-key quota. - Disable/Enable: Disabling an extension does not delete its storage. Data persists across enable/disable cycles.
- Update: Updating an extension preserves all storage.
Choosing the Right Storage
storage.kv (key-value) | storage.getDataDir (directories) | |
|---|---|---|
| Data model | String key-value pairs | Files and directories |
| Size limits | 4 KB per value, 1000 keys | No hard limit |
| Best for | Small settings, preferences, tokens | Binary files, large data, assets |
| Access | storage.kv.setItem() / getItem() | storage.getDataDir() + workspace.fs.* |
| Permission | None | None |
Examples
Persisting user preferences
import editor from "@srcnexus/ext-sdk";
const PREFS_KEY = "userPreferences";
const DEFAULT_PREFS = {
autoFormat: true,
indentSize: 2,
showLineNumbers: true,
};
async function loadPreferences() {
const raw = await editor.storage.kv.getItem(PREFS_KEY);
if (raw === null) {
return { ...DEFAULT_PREFS };
}
return { ...DEFAULT_PREFS, ...JSON.parse(raw) };
}
async function savePreferences(prefs) {
await editor.storage.kv.setItem(PREFS_KEY, JSON.stringify(prefs));
}
// Usage
const prefs = await loadPreferences();
prefs.indentSize = 4;
await savePreferences(prefs);
Caching network responses
import editor from "@srcnexus/ext-sdk";
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
async function getCachedOrFetch(cacheKey, url) {
const cached = await editor.storage.kv.getItem(cacheKey);
if (cached !== null) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_TTL_MS) {
return data;
}
}
const response = await editor.network.fetch(url);
const data = response.body;
// Only cache if it fits within the 4 KB value limit
const serialized = JSON.stringify({ data, timestamp: Date.now() });
if (serialized.length <= 4096) {
await editor.storage.kv.setItem(cacheKey, serialized);
}
return data;
}
Caching downloaded assets (directory storage)
import editor from "@srcnexus/ext-sdk";
async function getCachedAsset(filename, downloadUrl) {
const cacheDir = await editor.storage.getCacheDir();
const files = await editor.workspace.fs.list(cacheDir);
const cached = files.find((f) => f.name === filename);
if (cached) {
return await editor.workspace.fs.read(cached.uri);
}
// Download and cache
const response = await editor.network.fetch(downloadUrl);
const fileUri = await editor.workspace.fs.create(cacheDir, filename);
await editor.workspace.fs.write(fileUri, response.body);
return response.body;
}
Managing a list of items
import editor from "@srcnexus/ext-sdk";
const LIST_KEY = "recentFiles";
async function addRecentFile(filePath) {
const raw = await editor.storage.kv.getItem(LIST_KEY);
const list = raw !== null ? JSON.parse(raw) : [];
// Remove duplicates, add to front, keep last 20
const updated = [filePath, ...list.filter((f) => f !== filePath)].slice(0, 20);
await editor.storage.kv.setItem(LIST_KEY, JSON.stringify(updated));
}
async function getRecentFiles() {
const raw = await editor.storage.kv.getItem(LIST_KEY);
return raw !== null ? JSON.parse(raw) : [];
}
async function clearRecentFiles() {
await editor.storage.kv.removeItem(LIST_KEY);
}