Web SDK
Koolbase in the browser — auth, database, semantic search, storage, realtime, functions, feature flags, analytics, and an offline write queue with conflict resolution. TypeScript throughout, ESM and CommonJS, about 16 kB gzipped.
Same core as React Native. @koolbase/js and @koolbase/react-native are one codebase composed for two hosts, and one test suite runs against both. Where they differ, this page says so.
Installation
npm install @koolbase/jsNo peer dependencies and no build configuration. It works in any modern browser and in any framework that renders in one — React, Vue, Svelte, Angular, or no framework at all.
Setup
Initialize once, as early as your app starts. Your publishable key is under Settings → Environments in the dashboard.
import { Koolbase, RestoreResult } from '@koolbase/js';
export async function startKoolbase() {
await Koolbase.initialize({
publicKey: 'pk_live_your_key_here',
baseUrl: 'https://api.koolbase.com',
analyticsEnabled: true,
appVersion: '1.0.0',
authTimeout: 10000, // optional, default 10s
});
// Restore a session persisted from a previous visit. Optimistic state is
// read from storage before any network call, so authenticated UI renders
// without a round trip.
const result = await Koolbase.auth.restoreSession();
switch (result) {
case RestoreResult.Restored: return 'signed-in';
case RestoreResult.Offline: return 'signed-in'; // optimistic, no network yet
case RestoreResult.Expired: return 'signed-out';
case RestoreResult.NoSession: return 'signed-out';
}
}Add your origin to Trusted Origins
http://localhost:3000 while developing — under Settings → Trusted Origins on your project. Without it every call fails as a CORS error in the browser console.Auth
Email and password, phone with OTP, and OAuth. Once signed in, every database, storage and function call authenticates as that user — nothing to pass.
import { Koolbase } from '@koolbase/js';
// Two outcomes, and you must tell them apart: with verification required
// on the project, the account is created and NO session is issued.
const result = await Koolbase.auth.register({
email: '[email protected]',
password: 'password',
});
if (result.status === 'verification_required') {
showCheckYourEmail(result.user.email); // account exists, not signed in
} else {
showApp(); // result.session is live
}
const session = await Koolbase.auth.login({
email: '[email protected]',
password: 'password',
});
const me = Koolbase.auth.currentUser;
await Koolbase.auth.logout();
await Koolbase.auth.forgotPassword('[email protected]');
// Fires immediately with the current state, then on every change.
const unsubscribe = Koolbase.auth.onAuthStateChange((user) => {
render(user ? 'signed in' : 'signed out');
});Email verification and password reset
Both flows send the user a link carrying a token. Where that link points is yours to decide — set the URL templates under Branding in the dashboard, and your own page reads the token from the query string and passes it here. Left unset, the links fall back to an API-hosted page that completes verification without any configuration.
// Your /verify-email page, with ?token= from the link
await Koolbase.auth.verifyEmail(token);
// Your /reset-password page
await Koolbase.auth.resetPassword(token, newPassword);
// A "resend" button, for a signed-in but unverified user
const result = await Koolbase.auth.resendVerificationEmail();
if (result.alreadyVerified) {
// They verified in another tab. Not an error — say so and move on.
showVerified();
}If your project requires a verified contact, a new account gets no session until it verifies — so a user whose email never arrived cannot sign in to ask for another. That is what the by-address call is for:
// A "didn't get it?" link on your check-your-email page
await Koolbase.auth.resendVerificationEmailToAddress(email);
// Show the same message either way — see belowIt returns nothing and needs no session. The server answers identically whether the address has an account, has none, or is already verified — anything else would let anyone discover who has signed up — so show one message regardless, and never say “we sent it”.
resendVerificationEmail needs a session: the server verifies the caller rather than taking an email address, which is what stops it being an open mail relay. It is throttled — a short cooldown between sends and a daily cap — and a refusal arrives typed, carrying when to retry:
import {
VerificationResendCooldownError,
VerificationResendDailyCapError,
} from '@koolbase/js';
try {
await Koolbase.auth.resendVerificationEmail();
} catch (e) {
if (e instanceof VerificationResendCooldownError) {
// e.cooldownUntil is a Date — show a countdown, not a bare refusal
startCountdown(e.cooldownUntil);
} else if (e instanceof VerificationResendDailyCapError) {
showMessage('Try again tomorrow.');
} else {
throw e;
}
}Phone and OTP
await Koolbase.auth.sendOtp({ phoneNumber: '+233200000000' });
const result = await Koolbase.auth.verifyOtp({
phoneNumber: '+233200000000',
code: '123456',
});
if (result.isNewUser) showOnboarding();Google and Apple
The native sign-in libraries are a React Native feature; a browser runs the provider's web OAuth flow instead. Get an ID token however you like, then hand it to the same method — the server-side verification is identical.
// After your web OAuth flow returns an ID token:
const session = await Koolbase.auth.signInWithGoogle({ idToken });
// Apple, likewise:
const session = await Koolbase.auth.signInWithApple({ identityToken });Where the session lives
In IndexedDB by default, so a refresh does not sign the user out. The SDK picks its store by trying it: IndexedDB first, then localStorage, then memory. A browser in private mode, or one with site data blocked, degrades rather than failing.
import { browserPlatform } from '@koolbase/js';
// 'indexeddb' | 'localstorage' | 'memory'
const tier = await browserPlatform().storageTier();
if (tier === 'memory') {
showNotice('Your session will end when you close this tab.');
}Browser storage is not a keychain
KoolbaseAuthStorage and pass it as config.authStorage and choose where the token lives among the stores JavaScript can reach, or hand the SDK a token your own backend fetched. What this cannot be is an httpOnly cookie: if JavaScript cannot read it, the SDK cannot attach it to a request.Database
await Koolbase.db.insert('posts', { title: 'Hello', published: true });
const { records, total, isFromCache } = await Koolbase.db.query('posts', {
filters: { published: true },
limit: 10,
orderBy: 'created_at',
orderDesc: true,
});
const post = records[0];
post.data.title; // your fields live under .data
post.id, post.collection;
await Koolbase.db.update(post.id, { title: 'Updated' });
await Koolbase.db.delete(post.id);total is the size of the set you are authorized to read, on every page — so paginating with limit and offset is exact rather than approximate.
Upsert, bulk delete, batches
import { Koolbase, BatchOp } from '@koolbase/js';
const { record, created } = await Koolbase.db.upsert(
'profiles', { user_id: userId }, { weightKg: 70 },
);
const deleted = await Koolbase.db.deleteWhere('sessions', {
user_id: userId, status: 'expired',
});
// All of these commit together, or none do.
const results = await Koolbase.db.batch([
BatchOp.insert('orders', { total: 50, customer_id: customerId }),
BatchOp.update(inventoryId, { stock: 9 }),
BatchOp.delete(cartItemId),
]);Online only
Offline
Reads come from a local cache when the network is gone, and insert, update and delete queue and replay when it returns. This is the same engine that runs in the React Native SDK, device-verified, not a browser approximation.
const { records, isFromCache } = await Koolbase.db.query('posts', { limit: 20 });
// Queued if offline; sent on reconnect, or force it:
await Koolbase.db.update(id, { title: 'Corrected' });
await Koolbase.db.syncPendingWrites();
// For a sync indicator, and to warn before signing out with unsent work:
const pending = await Koolbase.db.pendingWrites();
if (pending.length) showBadge(pending.length);Conflicts
An offline edit is queued with the revision it was based on. If the record changed meanwhile — another device, another user, a Function — the server refuses the write on replay and it becomes a conflict you resolve, rather than an overwrite nobody sees.
const conflicts = await Koolbase.db.conflicts();
for (const c of conflicts) {
c.local; // the change the user made
c.server; // the record as the server holds it now
c.divergentFields; // where they disagree
c.reason; // concurrent_modification | baseline_unavailable
await c.resolveWithLocal();
// or c.resolveWithServer(), c.resolveWithMerge({ ... }), c.abandon()
}Conflicts do not expire
conflicts() somewhere. Automatic expiry would hide the problem while quietly losing the work.Several tabs
Tabs on one origin share a queue and are coordinated through the Web Locks API: one tab replays it, state changes from different tabs cannot overwrite each other, and a tab that closes mid-sync hands off automatically. Nothing to configure.
Search
Semantic, lexical, or both fused together. Declare a vector field on the collection first, then let the server embed your records as they are written.
// Semantic — meaning, not keywords
const result = await Koolbase.db.searchSemantic({
collection: 'articles',
field: 'content_embedding',
queryText: 'how do I move quicker?',
limit: 10,
});
// Lexical — exact terms, codes, names
await Koolbase.db.searchSemantic({ ..., mode: 'lexical' });
// Hybrid — both, reciprocal rank fusion. Usually the best default.
await Koolbase.db.searchSemantic({ ..., mode: 'hybrid', minSimilarity: 70 });
for (const hit of result.hits) {
hit.record.data.title;
hit.distance;
}Storage
Pass a File from an input, or any Blob you built. The upload presigns, sends the bytes, and confirms — the SDK handles all three.
const file = input.files[0];
const { object, downloadUrl } = await Koolbase.storage.upload({
bucket: 'avatars',
path: `user-${userId}.jpg`,
file, // a File IS a Blob — pass it straight through
});
const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg`);
await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);Uploads are safe by default: a path that already exists throws KoolbaseStorageConflictError unless you pass overwrite: true. Bucket size caps, per-file caps and content-type allowlists arrive as typed errors, and MIME rejection happens at presign time so no bytes move.
Realtime
const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
if (event.type === 'deleted') {
remove(event.recordId);
} else {
upsert(event.record!);
}
});
// Later
unsubscribe();One WebSocket, shared across subscriptions, reconnecting with backoff that doubles to a minute and resets on connect. Events are filtered by the collection's read rule, so a subscriber sees only what a query would return them.
Functions
const result = await Koolbase.functions.invoke('send-welcome-email', {
userId: '123',
});
if (result.success) console.log(result.data);The signed-in user's token is forwarded automatically, so the function reads the caller on ctx.auth. Failures are typed: FunctionNotFoundError, FunctionPermissionError, FunctionValidationError, FunctionQuotaExceededError, FunctionExecutionError.
Flags and remote config
if (Koolbase.isEnabled('new_checkout')) showNewFlow();
const timeout = Koolbase.configNumber('timeout_seconds', 30);
const apiUrl = Koolbase.configString('api_url', 'https://api.myapp.com');
const dark = Koolbase.configBool('force_dark_mode', false);
const v = Koolbase.checkVersion('1.2.3');
if (v.status === 'force_update') blockAndPrompt();Rollout buckets are computed from a stable per-install device id, so a 10% rollout is genuinely 10% of visitors rather than a coin flip per page load.
Analytics
Koolbase.analytics.track('purchase', { value: 1200, currency: 'GHS' });
Koolbase.analytics.screenView('checkout');
Koolbase.analytics.identify(user.id);
Koolbase.analytics.setUserProperty('plan', 'pro');
Koolbase.analytics.reset(); // on sign-outEvents batch and flush every 30 seconds, when the tab is hidden, and on pagehide — so a visitor closing the tab does not lose the last few.
Server rendering
The SDK imports and initializes under Node without crashing, so a Next.js or Nuxt server render will not fail on it. With no browser storage available it keeps nothing, and restoreSession() returns NoSession.
It is a client SDK
Error handling
Errors are selected from the server's stable error code, never from message text, and everything the SDK raises extends KoolbaseError.
import {
KoolbaseError,
KoolbaseDataError,
KoolbaseConflictError,
KoolbaseUnauthenticatedError,
KoolbaseOfflineBaselineUnavailableError,
} from '@koolbase/js';
try {
await Koolbase.db.upsert('users', { email }, { name });
} catch (e) {
if (e instanceof KoolbaseUnauthenticatedError) {
goToLogin(); // already signed out by the time you catch it
} else if (e instanceof KoolbaseConflictError) {
showError(`That ${e.field ?? 'value'} is already taken.`);
} else if (e instanceof KoolbaseDataError) {
showError(e.message);
} else {
throw e;
}
}A 401 signs you out; a 403 does not
KoolbaseUnauthenticatedError means the credentials were refused, and the session is already cleared — route to login rather than retrying. KoolbasePermissionError means the credentials were accepted and this caller may not do that: nobody is signed out.Not in the browser package
Stated so nothing is discovered as a method that fails:
- Code push — it patches the Dart VM, which no browser has. The web already ships on deploy. Flutter only.
- Push notifications — device tokens come from a native module. Use Web Push through your own service worker and backend.
- The native sign-in libraries — the methods are here and work; only the credential-fetching library is native. Run the provider's web OAuth flow and pass the ID token.