A secure, real-time cloud storage SDK with a familiar localStorage
-like API β enhanced for cross-device, cross-domain sync, optional end-to-end encryption (E2EE), presence, and optional offline-first usage.
Vaultrice is ideal for state sharing between tabs, browsers, devices, or domains β with built-in real-time updates and optional encryption... without managing custom backend WebSocket infrastructure.
Vaultrice offers a free tier β get started without having to pay!
npm install @vaultrice/sdk
# or
yarn add @vaultrice/sdk
import { NonLocalStorage } from '@vaultrice/sdk'
const nls = new NonLocalStorage({
projectId: 'your-project-id',
apiKey: 'your-api-key',
apiSecret: 'your-api-secret'
}, 'your-object-id') // optional explicit ID
await nls.setItem('key', 'value')
const item = await nls.getItem('key')
console.log(item?.value) // 'value'
Feature | Description |
---|---|
localStorage -like API |
Familiar setItem , getItem , removeItem , etc. |
Cross-tab/browser/device/domain | Seamless state sharing across environments |
Real-time sync | WebSocket-based updates, instant across clients |
Optional end-to-end encryption | Data encrypted client-side, never readable on the server |
Client-Side Throttling | Built-in rate limiting to prevent accidental abuse |
TTL support | Auto-expiry per key or object |
Event system | Listen to changes, removals, messages |
SyncObject API | Reactive object that syncs automatically |
Offline-first API | createOfflineNonLocalStorage , createOfflineSyncObject |
Custom storage adapters | Use IndexedDB, SQLite, or any custom backend (default: LocalStorage) |
Full TypeScript support | Strong typings, interfaces, autocompletion |
Works in browsers and Node.js | Cross-platform by design |
The SDK supports three authentication approaches β choose based on your threat model and architecture:
accessToken
)
accessToken
together with getAccessToken
for immediate use (the SDK will validate & auto-refresh as needed).const nls = new NonLocalStorage({
projectId: 'my-project-id',
accessToken: 'initial-token-if-available',
getAccessToken: async () => {
// call your backend to get a fresh token
const r = await fetch('/api/vaultrice-token')
if (!r.ok) throw new Error('token fetch failed')
const { accessToken } = await r.json()
return accessToken
}
})
nls.onAccessTokenExpiring(() => {
// ~2 minutes before expiry - useful to prefetch a token or show UX
const token = await refreshTokenFromBackend()
nls.useAccessToken(token)
})
You can securely mint access tokens on your backend using:
import { retrieveAccessToken } from '@vaultrice/sdk'
const accessToken = await retrieveAccessToken('projectId', 'apiKey', 'apiSecret')
// Optionally pass origin if the api key has origin restriction:
// const accessToken = await retrieveAccessToken('projectId', 'apiKey', 'apiSecret', { origin: 'https://your-app.com' })
Option | Token refresh | Secrets in client? | Example uses |
---|---|---|---|
apiKey + apiSecret | Auto | Yes (if in client) | Quick setup, automatic renewal, flexible |
Short-lived accessToken | Manual | No | Environments where you avoid long-lived secrets |
Note: Both methods are fully supported β itβs up to you to decide which fits your architecture and security model.
Read more about it here.
new NonLocalStorage(credentials, options?)
credentials: { projectId, apiKey?, apiSecret?, accessToken?, getAccessToken? }
options: { id?, class?, ttl?, passphrase?, getEncryptionHandler?, ... }
// All write operations accept an optional `options` object:
// options: { ttl?: number, updatedAt?: number }
await nls.setItem('key', value, options?)
const item = await nls.getItem('key') // { value, createdAt, updatedAt, expiresAt, keyVersion? }
await nls.setItems({ key1: { value }, key2: { value, ...options } })
await nls.getItems(['key1','key2'])
await nls.getAllKeys()
await nls.getAllItems()
await nls.removeItem('key')
await nls.removeItems(['k1','k2'])
await nls.clear()
// Atomic numeric ops:
await nls.incrementItem('counter', options?) // increments by 1 (default)
await nls.incrementItem('counter', 5) // increments by 5
await nls.decrementItem('counter') // decrements by 1 (default)
await nls.decrementItem('counter', 2)
// Collection / object helpers:
await nls.push('my-array', element, options?) // append element to array (creates array if missing)
await nls.splice('my-array', startIndex, deleteCount, items?, options?) // remove/replace elements (like Array.prototype.splice)
await nls.merge('my-obj', { a: 1 }, options?) // shallow merge into an existing object (creates object if missing)
await nls.setIn('my-obj', 'user.profile.name', 'Alice', options?) // set deep path inside an object (creates parents as needed)
Example: splice
// remove 2 elements at index 1 and insert 'x','y'
await nls.setItem('arr', ['a','b','c','d'])
await nls.splice('arr', 1, 2, ['x','y'])
const updated = await nls.getItem('arr') // ['a','x','y','d']
The SDK offers atomic numeric operations for counters and similar use-cases:
Collection / object helpers
nls.on('connect', () => {})
nls.on('disconnect', () => {})
nls.on('message', msg => {})
nls.on('setItem', evt => {}) // all
nls.on('setItem', 'myKey', evt => {}) // key-specific
nls.on('removeItem', evt => {})
nls.on('error', e => {})
nls.off('setItem', handler)
nls.send({ type: 'chat', message: 'hi' }) // via WebSocket
await nls.send({ type: 'notice' }, { transport: 'http' }) // via HTTP (also reaches sender)
await nls.join({ userId: 'u1', name: 'Alice' }) // announces presence
await nls.leave() // leave presence
const conns = await nls.getJoinedConnections() // get {connectionId, joinedAt, data}
nls.on('presence:join', c => {}) // listen for joins
nls.on('presence:leave', c => {}) // listen for leaves
Messages sent through nls.send
with transport: 'ws'
are broadcast to other connected clients (not echoed to sender). transport: 'http'
reaches all clients including sender.
While the main SDK authentication verifies that your client can connect to an object, you might also need to verify that a user within that object is who they say they are. This prevents identity spoofing in multi-user applications like chat rooms.
You can enable this feature in your Class Settings in the Vaultrice dashboard. It works for both the join()
and send()
methods by passing an optional auth
object.
There are three modes available:
join
and send
calls without user verification.// --- On your backend ---
import jwt from 'jsonwebtoken'
const payload = {
sub: 'user-123', // The user's unique ID
name: 'Alice',
role: 'moderator'
}
const identityToken = jwt.sign(payload, YOUR_PRIVATE_KEY, { algorithm: 'RS256' })
// Send this token to your client
// --- In your Client-Side SDK ---
const auth = { identityToken }
// Authenticate when joining
await nls.join(
{ sub: 'user-123', name: 'Alice', customData: '...' }, // Payload must be consistent with token
auth
)
// Authenticate when sending a message
await nls.send(
{ sub: 'user-123', name: 'Alice', type: 'chat', text: 'Hello!' },
{ auth }
)
// --- On your backend ---
import crypto from 'crypto'
const userId = 'user-123'
const userIdSignature = crypto
.createHmac('sha256', YOUR_SECRET_KEY)
.update(userId)
.digest('hex')
// Send signature to your client
// --- In your Client-Side SDK ---
const auth = {
userIdSignature
}
// Authenticate when joining (userId in payload must match auth.userId)
await nls.join(
{ userId: 'user-123', name: 'Alice', role: 'user' },
auth
)
// Authenticate when sending messages
await nls.send(
{ userId: 'user-123', type: 'chat', message: 'Hello!' },
{ auth }
)
Note: User ID signature verification will not work with encrypted payloads or non-string payloads.
For a complete deep-dive into the security model and configuration options, see our Security Guide.
Enable by passing passphrase
or getEncryptionHandler
when constructing:
const nls = new NonLocalStorage(credentials, {
id: 'object-id',
passphrase: 'super secret'
})
await nls.getEncryptionSettings() // fetch salt + key version
await nls.setItem('secret', 'value') // automatically encrypted when needed
rotateEncryption()
to create a new key version.
To ensure stability and prevent accidental abuse, the Vaultrice SDK includes a built-in, configurable client-side throttle manager for all operations (both HTTP requests and WebSocket messages).
By default, the SDK limits operations to 100 per minute. This is designed to be permissive enough for almost all use cases while protecting your application and the backend service.
You can customize or disable throttling via the throttling
option during initialization.
import { NonLocalStorage } from '@vaultrice/sdk'
const nls = new NonLocalStorage(credentials, {
id: 'my-object-id',
throttling: {
enabled: true, // Enable or disable throttling
maxOperations: 200, // Max operations per window
windowMs: 30 * 1000, // Time window in milliseconds (30 seconds)
operationDelay: 50 // Add a 50ms delay between each operation
}
})
Option | Description | Default |
---|---|---|
enabled |
A boolean to enable or disable the throttle. | true |
maxOperations |
The maximum number of operations allowed within the windowMs . |
100 |
windowMs |
The time window in milliseconds to track operations. | 60000 |
operationDelay |
An artificial delay (in ms) to enforce between consecutive operations. | 0 |
High-level reactive object API that syncs fields automatically across clients with presence and messaging:
import { createSyncObject } from '@vaultrice/sdk'
const doc = await createSyncObject(credentials, 'doc-123')
doc.title = 'Hello' // auto-sync to other clients
console.log(doc.title) // for another client
doc.on('setItem', (evt) => {}) // or additionally listen to property changes
await doc.join({ name: 'Bob' }) // presence
await doc.send({ type: 'cursor', x: 10 })
Vaultrice now supports offline-first storage and sync, making your app resilient to network interruptions.
A drop-in replacement for NonLocalStorage
that works offline and automatically syncs changes when reconnected.
import { createOfflineNonLocalStorage } from '@vaultrice/sdk'
const nls = await createOfflineNonLocalStorage(
{ projectId: 'your-project-id', apiKey: 'your-api-key', apiSecret: 'your-api-secret' },
{ id: 'your-id', ttl: 60000 }
)
await nls.setItem('key', 'value') // Works offline!
const item = await nls.getItem('key')
console.log(item.value) // 'value'
A reactive object that syncs properties locally and remotely, with offline support.
import { createOfflineSyncObject } from '@vaultrice/sdk'
const obj = await createOfflineSyncObject(
{ projectId: 'your-project-id', apiKey: 'your-api-key', apiSecret: 'your-api-secret' },
{ id: 'your-id', ttl: 60000 }
)
obj.foo = 'bar' // Updates locally and syncs when online
console.log(obj.foo) // 'bar'
This makes it safe on mobile, airplane mode, or unstable networks.
You can inject your own storage backend for offline mode (for example, IndexedDB, SQLite, or any custom implementation).
Pass your adapter via the storage
option when creating an offline instance:
import { createOfflineNonLocalStorage } from '@vaultrice/sdk'
// Example: a minimal custom adapter
class MyAdapter {
async get(key) { /* ... */ },
async set(key, value) { /* ... */ },
async remove(key) { /* ... */ },
async getAll() { /* ... */ }
}
const nls = await createOfflineNonLocalStorage(
{ projectId: 'your-project-id', apiKey: 'your-api-key', apiSecret: 'your-api-secret' },
{ id: 'your-id', storage: MyAdapter }
)
// Now all offline reads/writes use your adapter!
await nls.setItem('foo', 'bar')
Requirements: Your adapter should implement these async methods:
get(key): Promise<any>
set(key, value): Promise<void>
remove(key): Promise<void>
getAll(): Promise<Record<string, any>>
This works for both createOfflineNonLocalStorage
and
Vaultrice uses Cloudflare Durable Objects. The first successful request for an ID fixes its "home" region:
const prefsUS = await createSyncObject(credentials, 'prefs-us') // US region
const prefsEU = await createSyncObject(credentials, 'prefs-eu') // EU region
Use Case | Recommended API |
---|---|
Simple, key-based storage | NonLocalStorage |
Real-time object sync | createSyncObject |
Works offline with auto-resync | createOfflineSyncObject or createOfflineNonLocalStorage |
interface DocumentState {
content?: string
title?: string
lastModified?: number
selectedText?: { start: number, end: number }
}
const doc = await createSyncObject<DocumentState>(credentials, 'doc-123')
// Join as a user
await doc.join({
userId: 'user-123',
name: 'Alice',
avatar: 'avatar1.png',
role: 'editor'
})
// Real-time collaborative editing
doc.on('setItem', 'content', (item) => {
if (item.value !== editor.getText()) {
editor.setText(item.value) // Update editor with remote changes
}
})
// Auto-save on edit
editor.on('text-change', () => {
doc.content = editor.getText()
doc.lastModified = Date.now()
})
// Show who's editing
doc.on('presence:join', (conn) => {
showActiveUser(conn.data)
showNotification(`${conn.data.name} joined the document`)
})
doc.on('presence:leave', (conn) => {
hideActiveUser(conn.connectionId)
showNotification(`${conn.data.name} left the document`)
})
// Real-time cursor sharing
doc.on('message', (msg) => {
if (msg.type === 'cursor-move') {
updateCursor(msg.userId, msg.position)
} else if (msg.type === 'selection') {
showSelection(msg.userId, msg.range)
}
})
// Send cursor updates
editor.on('cursor-move', (position) => {
doc.send({
type: 'cursor-move',
userId: 'user-123',
position,
timestamp: Date.now()
})
})
// Send text selection updates
editor.on('selection-change', (range) => {
doc.send({
type: 'selection',
userId: 'user-123',
range,
timestamp: Date.now()
})
})
// Show live user count
const updateUserCount = () => {
userCountElement.textContent = `${doc.joinedConnections.length} users online`
}
doc.on('presence:join', updateUserCount)
doc.on('presence:leave', updateUserCount)
updateUserCount() // Initial count
interface GameState {
players?: { [id: string]: { x: number, y: number, score: number, health: number } }
gameStatus?: 'waiting' | 'playing' | 'finished'
currentRound?: number
}
const game = await createSyncObject<GameState>(credentials, 'game-room-456')
// Join as player
await game.join({
playerId: 'player-123',
name: 'Alice',
character: 'warrior',
level: 15
})
// Initialize game state
if (!game.gameStatus) {
game.gameStatus = 'waiting'
game.players = {}
game.currentRound = 1
}
// Update player position
function movePlayer(x: number, y: number) {
if (!game.players) game.players = {}
game.players = {
...game.players,
'player-123': {
...(game.players['player-123'] || { score: 0, health: 100 }),
x,
y
}
}
}
// Listen for game state changes
game.on('setItem', 'players', (item) => {
updateGameBoard(item.value)
})
game.on('setItem', 'gameStatus', (item) => {
if (item.value === 'playing') {
startGameLoop()
} else if (item.value === 'finished') {
showGameResults()
}
})
// Handle player actions via messaging
game.on('message', (msg) => {
switch (msg.type) {
case 'attack':
handleAttack(msg.from, msg.target, msg.damage)
break
case 'power-up':
handlePowerUp(msg.playerId, msg.powerUpType)
break
case 'chat':
showChatMessage(msg.from, msg.message)
break
}
})
// Send attack action
function attack(targetPlayerId: string, damage: number) {
game.send({
type: 'attack',
from: 'player-123',
target: targetPlayerId,
damage,
timestamp: Date.now()
})
}
// Show live player list
game.on('presence:join', (conn) => {
addPlayerToLobby(conn.data)
})
game.on('presence:leave', (conn) => {
removePlayerFromLobby(conn.connectionId)
// Remove from game state too
if (game.players && game.players[conn.data.playerId]) {
const updatedPlayers = { ...game.players }
delete updatedPlayers[conn.data.playerId]
game.players = updatedPlayers
}
})
Have questions, ideas or feedback? Open an issue or email us at support@vaultrice.com
Made with β€οΈ for developers who need real-time storage, without the backend hassle.
Try Vaultrice for free!