DEV Community

Cover image for From API Keys to E2EE: A Practical Guide to Securing Your Real-Time App
Adriano Raiano
Adriano Raiano

Posted on • Originally published at vaultrice.com

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

When you're building a new real-time feature, the initial focus is on making it work. You spin up a prototype, get the data flowing, and celebrate that first magical moment when two browser tabs update simultaneously. But before you ship to production, a critical question arises: Is this secure?

Security in real-time applications is non-trivial. How do you protect your API credentials on the client-side? How do you ensure one user can't access another user's data? How do you handle sensitive information with maximum confidentiality?

At Vaultrice, we believe security shouldn't be an afterthought. It should be a series of layers you can apply as your application grows in complexity. In this guide, we'll walk through a practical, progressive journey of securing a real-time React application, moving from a basic prototype to a production-ready, end-to-end encrypted system using Vaultrice's security model.

The Scenario: A Collaborative "To-Do" List

Imagine we're building a simple collaborative to-do list app. Multiple users can join a shared list and add or remove items.

  • Prototype Goal: Get a basic real-time list working.
  • Production Goal: Ensure only authorized users can access their own lists, their identity is verified, and the to-do items (which might be sensitive) are kept private.

Stage 1: The Prototype (Security Level 0) 🐣

At the start, we just want to get things working. We'll use Direct Authentication with an API key and secret.

The Code:

import { NonLocalStorage } from '@vaultrice/sdk';

// For a quick prototype, we place credentials directly.
const credentials = {
  projectId: 'YOUR_PROJECT_ID',
  apiKey: 'YOUR_API_KEY',
  apiSecret: 'YOUR_API_SECRET'
};

const sharedTodoList = new NonLocalStorage(credentials, 'our-first-todo-list');

// We can now read and write to the list.
await sharedTodoList.setItem('task-1', { text: 'Build prototype' });
Enter fullscreen mode Exit fullscreen mode

Security Analysis (Level 0):

  • What's working: All communication is automatically encrypted in transit with TLS (HTTPS), and all data is encrypted at rest by default on the backend. This is SF 0: Transport + Default At-Rest Encryption and it's always active on all plans.
  • The Risk: Our apiSecret is exposed in the client-side code. A malicious actor could steal these credentials and use them from anywhere to access our data. This is fine for a local demo, but unacceptable for production.

Stage 2: Production-Ready (Security Levels 1 & 2) 🚀

Now, let's take our app to production. We need to lock down our credentials, control data access, and verify user identities.

Upgrading Authentication: Secure Access Tokens

First and foremost, we must remove the long-lived apiSecret from our client-side code. The gold standard for this is using Backend-Issued Access Tokens.

  • The Pattern: Your own backend has a secure endpoint that uses your apiKey and apiSecret to generate a short-lived access token for the client. The client then initializes the SDK with this temporary token, never knowing the secret.

  • Action: Create a serverless function or API endpoint on your trusted backend.

    // On your trusted backend (e.g., /api/vaultrice-token)
    import { retrieveAccessToken } from '@vaultrice/sdk';
    
    async function generateTokenForClient(origin) {
      const accessToken = await retrieveAccessToken(
        'YOUR_PROJECT_ID',
        'YOUR_API_KEY',    // Kept securely on the server
        'YOUR_API_SECRET', // Kept securely on the server
        { origin } // Pass the client's origin if using Origin Restrictions
      );
      return { accessToken };
    }
    
  • Client-Side Implementation: Your client code is now much more secure. You can use the getAccessToken provider for automatic, hands-free token management.

    // In your React App
    import { NonLocalStorage } from '@vaultrice/sdk';
    
    const secureTodoList = new NonLocalStorage({
      projectId: 'YOUR_PROJECT_ID',
      // The SDK will call this function automatically to get and refresh tokens
      getAccessToken: async () => {
        const response = await fetch('/api/vaultrice-token');
        if (!response.ok) throw new Error('Failed to fetch token');
        const { accessToken } = await response.json();
        return accessToken;
      }
    }, 'your-object-id');
    
  • Result: You've achieved maximum credential security. Your apiSecret is never exposed to the browser, and the client operates on temporary, revokable tokens.

Layer 1: Perimeter Defense with Origin Restrictions (SF 1)

Next, we prevent our API key from being used on any other website.

  • Action: In the Vaultrice dashboard, we edit our API key and add our app's domain (e.g., https://mytodoapp.com) to the Origin Restrictions list.
  • Result: The Vaultrice backend will now reject any request made with this key that doesn't originate from our domain. This is an essential first step for all production applications.

Layer 2: Data Access Control with ID Signatures (SF 2)

We must ensure a user can only access their own to-do list, not someone else's. We can't trust the client to be honest about which list it wants to access.

  • Action: We enable Object ID Signature Verification in our project's Class settings. Then, we create an endpoint on our backend that signs the objectId for a logged-in user.

    // On your trusted backend server
    function getSignedTodoListId(user) {
      const objectId = `todo-list-${user.id}`;
      // You sign the ID with your private key
      const signature = signWithYourPrivateKey(objectId);
      return { objectId, signature };
    }
    
  • Client-Side Implementation: The client now fetches this signed ID from the backend before initializing the SDK.

    // Fetch the authorized object ID and signature from our backend
    const { objectId, signature } = await fetch('/api/get-todo-list-id');
    
    // Vaultrice will now verify this signature on every request
    const userTodoList = new NonLocalStorage(credentials, {
      id: objectId,
      idSignature: signature
    });
    
  • Result: A user can no longer guess another user's objectId and access their data. The Vaultrice API will reject any request where the signature doesn't match the ID.

Layer 3: Optional Server-Side Hardening (SF 3)

For an extra layer of protection on the server, you can enable Automatic At-Rest Encryption.

  • What it is: This feature transparently encrypts data values on the Vaultrice server with a unique key before they are stored in the database. This protects against a direct breach of the underlying database infrastructure.
  • Action: In your Class settings, simply enable the "Additional At-Rest Encryption" checkbox.
  • Result: Enhanced server-side data protection with no code changes required in your SDK implementation.

Stage 3: Maximum Confidentiality (Security Level 3) 🛡️

Our app is now secure for general use. But what if our to-do items contain highly sensitive information? For this, we need Security Level 3 and SF 4: End-to-End Encryption (E2EE).

  • Action: We enable E2EE by providing a passphrase during SDK initialization. This passphrase is known only to the client and is never sent to the server.

    import { NonLocalStorage } from '@vaultrice/sdk';
    
    const sensitiveList = new NonLocalStorage(credentials, {
      id: signedObjectId, // from Stage 2
      idSignature: signature, // from Stage 2
      passphrase: 'user-secret-passphrase-never-sent-to-server'
    });
    
    // We must fetch the salt from the server to derive the encryption key
    await sensitiveList.getEncryptionSettings();
    
    // This data is encrypted ON THE DEVICE before being sent.
    // Not even Vaultrice can read its content.
    await sensitiveList.setItem('task-1', { text: 'Top secret plan' });
    
  • Result: The content of our to-do list is now fully confidential. Vaultrice only stores ciphertext, providing the highest level of privacy. It's important to note that server-side atomic operations are incompatible with E2EE, as the server cannot decrypt the data to operate on it. So those operations are not E2EE encrypted.

Conclusion

Security is a journey, not a destination. By starting with a simple prototype and progressively adding layers of protection as needed, you can move to production with confidence. Vaultrice's layered security model provides a clear path to secure your real-time application, from basic transport encryption all the way to zero-knowledge, end-to-end encrypted collaboration.

Choose the security level that matches your needs and start building with confidence on the Vaultrice free tier.

Top comments (0)