Introducing Offline-First APIs: Building Resilient, Real-Time Apps

Real-time is great, but what happens when the network isn't? In the real world, users are on flaky mobile connections, in tunnels, or on spotty Wi-Fi. Traditional real-time applications often freeze or fail in these situations, leading to a frustrating user experience and lost data.
Today, we're thrilled to announce a powerful solution to this problem: Offline-First support is now available in the Vaultrice SDK.
You can now build applications that feel instant and reliable, whether the user is online or offline. The new APIs use a "local-first" pattern: all changes are saved immediately to local storage and then seamlessly synchronized with the cloud in the background whenever a connection is available.
Two Ways to Go Offline: SyncObject and NonLocalStorage
We've brought offline support to both of our core APIs, so you can choose the pattern that best fits your application's architecture.
1. createOfflineSyncObject: The Reactive, "Easy Button" Approach
The OfflineSyncObject is the simplest way to make a collaborative, stateful object work offline. It has the same reactive, proxy-based interface as the original SyncObject, but with all the offline logic handled for you automatically.
Let's build a simple to-do list that works offline and syncs between users when they're online.
import { createOfflineSyncObject } from '@vaultrice/sdk';
interface TodoList {
[key: string]: { text: string; completed: boolean } | any;
}
// This object will now read from and write to local storage first,
// and sync with the cloud in the background.
const list = await createOfflineSyncObject<TodoList>(credentials, 'shared-todo-list');
function addTask(text: string) {
const taskId = `task-${Date.now()}`;
// This write is INSTANT, even if the user is on an airplane.
// The change is queued in the background for the next sync.
list[`task:${taskId}`] = { text, completed: false };
}
// Reads are also instant and come from the local cache.
function render() {
// Filter only task keys that start with "task:"
const taskKeys = Object.keys(list).filter(key => key.startsWith('task:'));
const tasks = taskKeys.reduce((acc, key) => {
acc[key] = list[key];
return acc;
}, {} as { [key: string]: { text: string; completed: boolean } });
// ... render the UI using tasks object
}
// Listen for changes to any task keys
list.on('setItem', (data) => {
if (data.prop.startsWith('task:')) render();
});
render(); // Initial render
2. createOfflineNonLocalStorage: For Full, Method-Based Control
For developers who prefer an explicit, method-based API, createOfflineNonLocalStorage offers the same offline power with the familiar localStorage-like interface.
import { createOfflineNonLocalStorage } from '@vaultrice/sdk';
const settings = await createOfflineNonLocalStorage(credentials, 'user-settings');
async function saveTheme(theme) {
// This promise resolves immediately after saving to local storage.
// The network sync happens in the background.
await settings.setItem('theme', theme);
console.log('Theme saved locally!');
}
async function loadTheme() {
// This read is always fast, fetching from the local cache first.
const themeItem = await settings.getItem('theme');
return themeItem?.value || 'light';
}
The Magic Under the Hood
How does this work? We've built a robust offline engine based on a few key principles.
Pluggable Storage Adapters
By default, the offline APIs use localStorage in the browser. But what if you need to store more than 5-10MB of data, or you're running in a Node.js environment?
The storage option allows you to plug in any storage mechanism you need. For example adapters for IndexedDB (for large browser storage) or the file system (for Node.js), but you can easily write your own.
class MyStorageAdapter {
constructor (options: { projectId: string, class: string, id: string, ttl: number }) {
// ...
}
async get (key: string): Promise<any | null> {
// ...
}
async set (key: string, value: any): Promise<void> {
// ...
}
async remove (key: string): Promise<void> {
// ...
}
async getAll (): Promise<Record<string, any>> {
// ...
}
}
The Outbox Pattern & Conflict Resolution
When you make a change while offline, the operation is added to a persistent "outbox." When your application comes back online, the engine processes this outbox, sending the changes to the server.
But what if the server's data has also changed? By default, we use a "last-write-wins" strategy based on timestamps. For more complex cases, you can provide your own conflict resolution function to merge data exactly how you want.
// Custom resolver that merges two arrays
const resolveConflict = (local, remote) => {
const mergedValue = [...new Set([...local.value, ...remote.value])];
return { ...remote, value: mergedValue }; // Return the resolved item
};
const list = await createOfflineSyncObject(credentials, {
id: 'shared-list',
resolveConflict
});
Conclusion
With these new offline-first APIs, you no longer have to choose between building a real-time application and a resilient one. You can now give your users a fast, reliable experience that works seamlessly, whether their network connection is perfect or not.
This is a huge step forward for the platform, and we can't wait to see the amazing, resilient applications you'll build with it.






