Sign-ups paused

Sign-ups and billing are temporarily unavailable while we rebuild our infrastructure. The SDK and documentation remain available, and existing accounts are unaffected.

Vaultrice

Blog / Tutorials

Tutorials

From API Keys to E2EE: A Practical Guide to Securing Your Real-Time App

Four layers in the order they matter: secrets out of the browser, choosing object ids, picking a security level, and what the service should never see.

5 min read Updated 2026-08-XX @vaultrice/sdk

Shared state has a threat model that local state does not. Four things to get right, in the order they matter.

For a separate operational perspective on how teams organise and measure work, employee time clock software.

For independent background and broader industry context, see OWASP.

1. Get the secret out of the browser

The SDK accepts three authentication methods, and the difference is not stylistic.

apiKey and apiSecret

const nls = new NonLocalStorage({
  projectId: 'your-project-id',
  apiKey: 'your-api-key',
  apiSecret: 'your-api-secret'
}, 'my-id')

Simplest, and the secret is in code the browser downloads. Anyone who opens devtools has it, and with it, everything that key can reach.

Use for: a local experiment, a server-side script, a prototype nobody will deploy.

Not for: anything a user loads.

accessToken

const nls = new NonLocalStorage({
  projectId: 'your-project-id',
  accessToken: 'token-your-backend-issued'
}, 'my-id')

Your backend mints it. No secret in the client. You handle refresh, and a token that expires mid-session breaks things.

getAccessToken

const nls = new NonLocalStorage({
  projectId: 'your-project-id',
  getAccessToken: async () => {
    const res = await fetch('/api/vaultrice-token')
    if (!res.ok) throw new Error('Failed to fetch token')
    const { accessToken } = await res.json()
    return accessToken
  }
}, 'my-id')

The documented recommendation. No secret in the client, and the SDK refreshes automatically when the token expires.

This is the default choice for anything user-facing.

2. The token endpoint is where authorisation happens

The point most easily missed.

Your endpoint is not just a token vending machine. It is the only place in the system where you can decide whether this user should reach this object.

// your backend
app.get('/api/vaultrice-token', async (req, res) => {
  const user = await authenticate(req)
  if (!user) return res.status(401).end()

  const objectId = req.query.id
  if (!await userMayAccess(user, objectId)) return res.status(403).end()

  const accessToken = await mintVaultriceToken({ /* per your setup */ })
  res.json({ accessToken })
})

With direct apiKey credentials there is no such place, because the client holds the key and never asks anyone.

Check membership before issuing. A support room, a document, a team's shared state — each has a rule about who belongs, and this is where it lives.

3. Object ids are addresses

Anyone with valid credentials and the id reaches the object. So the id is part of your security model, not just a name.

Do not use guessable ids for private data. support-room-42, user-1, doc-3 are enumerable. If the contents are not meant to be shared, the id should not be predictable.

Derive ids from something non-sequential. A random identifier stored alongside the resource, or a value derived from it that is not the raw primary key.

And do not treat an unguessable id as authorisation. It raises the cost of finding an object; it does not decide who may use it. That is the token endpoint's job.

4. Choose the security level deliberately

The service offers levels, and the default is not the strongest. Higher levels are available on higher plans.

The question that decides it: should the service be able to read this?

If yes — collaborative documents where server-side features matter, public polls, presence data — a lower level is a reasonable trade.

If no — personal data, messages between people, anything you would rather not be readable if the service were compromised — use end-to-end encryption, where keys stay with your clients and the service holds ciphertext.

Read the security guide for what each level does before choosing, rather than accepting the default because it worked in development.

And note what E2EE costs you: the service cannot help with anything requiring readable content. That is the point, and it is a real trade-off to make knowingly.

What to decide alongside

Retention. How long objects live, and whether a TTL is set. Data you no longer need is exposure you did not need.

const nls = await createOfflineNonLocalStorage(credentials, {
  id: 'my-id',
  ttl: 60000
})

What goes in. The strongest configuration does not protect data you did not need to store. Ask whether the value belongs in shared state at all.

Offline copies. The offline APIs keep data in browser storage. That copy is subject to whatever protects the device.

And logging. Do not log object contents or tokens on your side. This is the most common way carefully protected data ends up somewhere unprotected.

A checklist

  • [ ] No apiSecret in client code
  • [ ] getAccessToken() in use
  • [ ] Token endpoint authenticates and authorises
  • [ ] Object ids not enumerable where contents are private
  • [ ] Security level chosen deliberately, not by default
  • [ ] TTL set where data should not persist
  • [ ] Offline storage considered
  • [ ] Nothing sensitive in your own logs

The short version

Three auth methods, and only getAccessToken() belongs in a browser.

The token endpoint is where authorisation lives — with direct keys, there is nowhere to put it.

Object ids are addresses. Guessable ids are shared ids.

The default security level is not the strongest — choose by asking whether the service should be able to read this.

And the strongest configuration does not protect what you should not have stored.