There is a persistent misconception that pure client-side applications (Single Page Applications, SPAs) operating without their own dedicated backend and hosted on free hosting platforms like GitHub Pages are doomed to isolation. It is widely assumed that their absolute limit is storing user data in the current browser’s localStorage or IndexedDB.
This approach imposes serious limitations: users cannot synchronize their data across devices, and clearing the cache or switching browsers completely erases all important information.
However, modern Serverless architecture and the concept of BaaS (Backend-as-a-Service) completely change the rules. A client-side application is fully capable of communicating directly with third-party databases, providing seamless synchronization, user registration, and secure data storage.
An excellent real-world implementation of this approach is the open-source project Traliran AI Hub (demo available at traliran.github.io). In this article, using the project’s source code, we will break down how to build a flexible, cross-platform data synchronization system directly from the browser.
Solution Architecture: A Bridge Between Browser and Cloud
In the Traliran AI Hub codebase, the synchronization system is divided into two key modules:
DB_CONNECTOR (implemented in db-connector.js) — a universal driver that abstracts interaction with various database providers (Firebase, Supabase, PocketBase, or a custom REST API).
SYNC_MANAGER (implemented in sync-manager.js) — a local state coordinator that monitors online status, optimizes request frequency (debouncing), and manages the queue of pending changes.
Let’s analyze each of these modules in detail.
Part 1. DB_CONNECTOR — A Universal Database Interface
The main challenge when integrating a static application with cloud databases is the sheer variety of protocols. Google Firebase Firestore requires a highly specific JSON format, Supabase utilizes standard PostgREST, and PocketBase relies on its own custom endpoint structure.
The module in db-connector.js solves this problem by using polymorphism at the API request level.
Auto-Detecting the Database Provider
The application is not locked to any single vendor. The detectType method in db-connector.js automatically determines the type of the database based on the URL provided by the user:
detectType(url) {
if (!url) return '';
const u = url.toLowerCase();
if (u.includes('firebase') || u.includes('firestore') || u.includes('identitytoolkit')) return 'firebase';
if (u.includes('supabase')) return 'supabase';
if (u.includes('pocketbase') || u.includes('pb.')) return 'pocketbase';
return 'generic';
}
Enter fullscreen mode Exit fullscreen mode
This implementation enables a true BYODB (Bring Your Own Database) concept: advanced users can host their own database instances (such as a free tier Supabase or PocketBase instance) and simply connect it via the application’s user interface by entering their URL and API key.
Direct Authentication from the Browser
Typically, user registration and login are handled by an intermediary backend server. In our serverless model, the client interacts directly with the provider’s authentication REST API. The login method in db-connector.js demonstrates exactly how this occurs:
For Firebase: The request is sent directly to the Google Identity Toolkit API.
For Supabase: Interaction goes through the /auth/v1/token endpoint.
For PocketBase: Authorization happens via the /api/collections/users/auth-with-password endpoint.
The acquired authorization tokens (JWT) are stored locally and attached to the headers of every subsequent request:
if (cfg.token) headers['Authorization'] = `Bearer ${cfg.token}`;
Enter fullscreen mode Exit fullscreen mode
Data Serialization (Using Firestore as an Example)
Google Firestore requires a strictly typed description of fields in REST format. For example, a string must be passed as { "stringValue": "text" }, while an array must be represented as { "arrayValue": { "values": [...] } }.
To keep the application’s core logic clean and free of this boilerplate, db-connector.js contains a built-in type translator using _objectToFirestoreDoc and _firestoreDocToObject. It transforms standard JS objects into Firestore’s structure on write operations and reconstructs them on read:
_jsToFirestoreValue(value) {
if (value === null || value === undefined) return { nullValue: null };
if (typeof value === 'string') return { stringValue: value };
if (typeof value === 'number') {
if (Number.isInteger(value)) return { integerValue: String(value) };
return { doubleValue: value };
}
if (typeof value === 'boolean') return { booleanValue: value };
if (Array.isArray(value)) return { arrayValue: { values: value.map(v => this._jsToFirestoreValue(v)) } };
if (typeof value === 'object') return { mapValue: this._objectToFirestoreDoc(value) };
return { stringValue: String(value) };
}
Enter fullscreen mode Exit fullscreen mode
Part 2. SYNC_MANAGER — Optimization and Offline-First
Every single UI event (such as editing a note or toggling a theme setting) should not trigger an immediate, blocking HTTP request. Doing so would saturate network bandwidth and severely degrade the responsiveness of the interface.
The module in sync-manager.js acts as an intelligent buffer between local storage (localStorage) and the cloud.
1. Data Mapping (Collection Map)
The manager knows exactly which local keys map to specific cloud database collections. This is defined inside COLLECTION_MAP in sync-manager.js:
COLLECTION_MAP: {
hub_sessions: { key: 'gem_sessions' },
hub_notes: { key: 'gem_notes' },
hub_settings: { keys: ['gem_provider', 'gem_bot_name', ...], isSettings: true },
ide_files: { key: 'ide_project_files' },
}
Enter fullscreen mode Exit fullscreen mode
2. Request Debouncing
When local data changes, pushToCloud is called. Instead of sending an immediate HTTP request, it sets a wait timer (a timeout of 500ms). If the user continues typing or modifying settings, the timer resets. The network request is dispatched only when the user pauses:
if (this._debounceTimers[collection]) {
clearTimeout(this._debounceTimers[collection]);
}
this._debounceTimers[collection] = setTimeout(async () => {
const data = this._readCollectionData(collection);
await DB_CONNECTOR.saveData(collection, 'current', data);
}, 500);
Enter fullscreen mode Exit fullscreen mode
3. Native Offline Mode Support
If the user loses internet connection, the application does not crash or throw fatal errors. Instead, sync-manager.js subscribes to network lifecycle events:
window.addEventListener('online', () => {
this._isOnline = true;
this._processQueue(); // Push pending local changes to cloud
});
window.addEventListener('offline', () => {
this._isOnline = false;
});
Enter fullscreen mode Exit fullscreen mode
When the network is unreachable, modifications are pushed into a local array queue called _syncQueue:
if (!this._isOnline) {
this._syncQueue.push({ type: 'push', collection });
this.updateStatus('Queued for sync (offline)', true);
return;
}
Enter fullscreen mode Exit fullscreen mode
As soon as connection is restored, _processQueue automatically plays back all queued save operations in the background, keeping local and remote states fully consistent.
Developer & User Benefits
Implementing direct database integration in static web apps, as seen in Traliran AI Hub, yields substantial benefits:
Zero-Cost Infrastructure: The client bundle can be hosted absolutely free of charge (e.g., on GitHub Pages). Modern BaaS providers like Supabase or Firebase offer generous free tiers that are more than sufficient for personal projects, portfolios, or small communities.
Privacy & BYODB (Bring Your Own Database): Users gain complete control over their information. They can use the default cloud hosted by the developers or easily provide their own database keys, ensuring that their prompts, notes, and personal keys belong entirely to them.
No Backend Overhead: There is no Node.js/Python server to manage, no libraries to patch, no memory leaks to track, and no server-side DDoS vectors to defend against. Your infrastructure is fully delegated to battle-tested BaaS platforms.
Conclusion
The modern web continuously demonstrates that “static” no longer means “limited.” The synchronization architecture of Traliran AI Hub proves that with clean, framework-agnostic JavaScript (without even importing heavy proprietary SDKs), you can build highly reliable, fast, and offline-resilient apps that can seamlessly work with multiple database providers out of the box.
Explore the complete source code or fork the project to host your own instance by visiting the Traliran/traliran-ai-hub GitHub repository.
답글 남기기