Vaultrice SDK - v1.1.0
    Preparing search index...

    Vaultrice SDK - v1.1.0

    Vaultrice JS/TS SDK

    Tests npm version

    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!


    1. Install
    2. Quick start
    3. Feature overview
    4. Authentication
    5. API overview
    6. Presence
    7. End-to-end encryption (E2EE)
    8. Rate Limiting & Throttling
    9. SyncObject (reactive object)
    10. Offline-first APIs
    11. Durable Object Location Strategy
    12. Which API Should I Use?
    13. Real-World Examples
    14. Related Packages
    15. Support

    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:

    1. apiKey + apiSecret (SDK automatically fetches and refreshes short-lived accessToken)
      • Easiest to use for quick builds and server-side code.
      • If used in clients, combine with origin restrictions or a proxy.
    2. accessToken (short-lived token, e.g. ~1 hour) β€” manual refresh
      • Your backend issues a token and the client receives it.
      • You are responsible for refreshing the token before expiry.
    3. getAccessToken (async function that returns a token) β€” recommended for production
      • The SDK calls the function when it needs a token or when a token is close to expiry.
      • You can optionally provide an initial 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:

    • incrementItem(name, amount = 1, options?) β€” Atomically increments a numeric value on the server and returns the updated item metadata.
    • decrementItem(name, amount = 1, options?) β€” Atomically decrements a numeric value on the server and returns the updated item metadata.

    Collection / object helpers

    • push(name, element, options?) β€” Appends element to an array stored at the key. If the key does not exist it will be created as an array.
    • merge(name, objectToMerge, options?) β€” Performs a shallow merge of provided object into the stored object at key. Creates the object if missing.
    • setIn(name, path, value, options?) β€” Sets a nested value inside a stored object using dot-path (or array of keys). Parents are created as needed.
    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:

    1. No Verification (Default): The server accepts all join and send calls without user verification.
    2. User ID Signature Verification: Your backend signs a specific user ID from your payload. This is great for guaranteeing a user's identity while keeping the rest of the payload flexible.
    3. JWT Consistency Verification: Your backend issues a standard JWT. Vaultrice verifies the token and also ensures that any overlapping data between your payload and the token's claims are consistent. This is the most robust option.
    // --- 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
    • The SDK supports key versioning and lazy decryption of previous keys.
    • Use 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'
    • Local-first: Reads/writes use local storage when offline.
    • Automatic sync: Queues changes and syncs with the server when online.
    • Conflict resolution: Last-write-wins by default, customizable.
    • Custom storage adapters: Use your own storage backend (IndexedDB, SQLite, etc).

    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'
    • Proxy-based: Use like a normal JS object.
    • Events: Listen for changes, removals, presence, and messages.
    • Presence: Track who’s online, join/leave notifications.
    • Works offline: Changes are queued and synchronized automatically.

    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:

    • All subsequent writes for that ID go to that region.
    • Useful for data residency & latency considerations.
    • To enforce regions, initialize from the right region first:
    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!