@koolbase/react-native 9.1.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +1342 -0
  2. package/README.md +462 -511
  3. package/dist/{auth-storage.d.ts → cjs/auth-storage.d.ts} +1 -1
  4. package/dist/cjs/index.d.ts +19 -0
  5. package/dist/cjs/index.js +125 -0
  6. package/dist/cjs/package.json +3 -0
  7. package/dist/cjs/platform.d.ts +2 -0
  8. package/dist/cjs/platform.js +43 -0
  9. package/dist/esm/auth-storage.d.ts +26 -0
  10. package/dist/esm/auth-storage.js +100 -0
  11. package/dist/esm/index.d.ts +19 -0
  12. package/dist/esm/index.js +106 -0
  13. package/dist/esm/package.json +3 -0
  14. package/dist/esm/platform.d.ts +2 -0
  15. package/dist/esm/platform.js +37 -0
  16. package/package.json +30 -24
  17. package/dist/analytics.d.ts +0 -24
  18. package/dist/analytics.js +0 -114
  19. package/dist/apple-auth.d.ts +0 -22
  20. package/dist/apple-auth.js +0 -74
  21. package/dist/auth-errors.d.ts +0 -117
  22. package/dist/auth-errors.js +0 -250
  23. package/dist/auth.d.ts +0 -199
  24. package/dist/auth.js +0 -794
  25. package/dist/cache-store.d.ts +0 -11
  26. package/dist/cache-store.js +0 -136
  27. package/dist/code-push.d.ts +0 -59
  28. package/dist/code-push.js +0 -255
  29. package/dist/database-errors.d.ts +0 -95
  30. package/dist/database-errors.js +0 -173
  31. package/dist/database.d.ts +0 -208
  32. package/dist/database.js +0 -508
  33. package/dist/device-id.d.ts +0 -1
  34. package/dist/device-id.js +0 -60
  35. package/dist/device-metadata.d.ts +0 -36
  36. package/dist/device-metadata.js +0 -102
  37. package/dist/flags.d.ts +0 -15
  38. package/dist/flags.js +0 -76
  39. package/dist/functions.d.ts +0 -8
  40. package/dist/functions.js +0 -70
  41. package/dist/index.d.ts +0 -45
  42. package/dist/index.js +0 -193
  43. package/dist/logic-engine.d.ts +0 -17
  44. package/dist/logic-engine.js +0 -193
  45. package/dist/messaging.d.ts +0 -13
  46. package/dist/messaging.js +0 -36
  47. package/dist/realtime.d.ts +0 -19
  48. package/dist/realtime.js +0 -148
  49. package/dist/record.d.ts +0 -2
  50. package/dist/record.js +0 -20
  51. package/dist/storage-errors.d.ts +0 -163
  52. package/dist/storage-errors.js +0 -249
  53. package/dist/storage.d.ts +0 -184
  54. package/dist/storage.js +0 -438
  55. package/dist/sync-engine.d.ts +0 -16
  56. package/dist/sync-engine.js +0 -86
  57. package/dist/types.d.ts +0 -470
  58. package/dist/types.js +0 -40
  59. /package/dist/{auth-storage.js → cjs/auth-storage.js} +0 -0
package/README.md CHANGED
@@ -3,9 +3,12 @@
3
3
  [![npm](https://img.shields.io/npm/v/@koolbase/react-native.svg)](https://www.npmjs.com/package/@koolbase/react-native)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- React Native SDK for [Koolbase](https://koolbase.com) Backend as a Service built for mobile developers.
6
+ **From idea to app. And everything after.** Design your app, power it with a
7
+ complete backend, and keep shipping after release.
7
8
 
8
- Auth, database, storage, realtime, functions, feature flags, remote config, version enforcement, code push, logic engine, analytics, and cloud messaging — one SDK, one `initialize()` call.
9
+ Auth, database, storage, realtime, functions, feature flags, remote config,
10
+ version enforcement, analytics, and cloud messaging — one SDK,
11
+ one `initialize()` call.
9
12
 
10
13
  ---
11
14
 
@@ -13,75 +16,102 @@ Auth, database, storage, realtime, functions, feature flags, remote config, vers
13
16
 
14
17
  1. Create a free account at [app.koolbase.com](https://app.koolbase.com)
15
18
  2. Create a project and copy your public key from Environments
16
- 3. Add the SDK:
19
+ 3. Install the SDK and its peer dependencies:
17
20
 
18
21
  ```bash
19
- npm install @koolbase/react-native
20
- # or
21
- yarn add @koolbase/react-native
22
- # or
23
- pnpm add @koolbase/react-native
24
- # or
25
- bun add @koolbase/react-native
22
+ npm install @koolbase/react-native
23
+ npm install @react-native-async-storage/async-storage @react-native-community/netinfo
26
24
  ```
27
25
 
26
+ > The native modules are peer dependencies so exactly one copy of each exists in
27
+ > your app. Two copies of a native module is a runtime failure that looks like an
28
+ > SDK bug, which is why they are not bundled.
29
+
28
30
  4. Initialize at app startup:
29
31
 
30
32
  ```typescript
31
- import { Koolbase } from '@koolbase/react-native';
33
+ import { Koolbase } from '@koolbase/react-native';
32
34
 
33
- await Koolbase.initialize({
34
- publicKey: 'pk_live_xxxx',
35
- baseUrl: 'https://api.koolbase.com',
36
- });
35
+ await Koolbase.initialize({
36
+ publicKey: 'pk_live_xxxx',
37
+ baseUrl: 'https://api.koolbase.com',
38
+ });
37
39
  ```
38
40
 
39
41
  That's it. Every feature below is now available via `Koolbase.*`.
40
42
 
43
+ ### Optional peer dependencies
44
+
45
+ | Package | Needed for |
46
+ |---|---|
47
+ | `react-native-keychain` | Persistent sessions in Keychain / Keystore. Without it the SDK warns once and runs without persistence. |
48
+ | `@invertase/react-native-apple-authentication` | Sign in with Apple |
49
+ | `@react-native-google-signin/google-signin` | Sign in with Google |
50
+ | `@react-native-firebase/messaging` | Push notification tokens |
51
+
52
+ > **Expo Go:** `react-native-keychain` and the native sign-in modules are not
53
+ > available there. Sessions still work — implement `KoolbaseAuthStorage` over
54
+ > `expo-secure-store` or `AsyncStorage` and pass it as `config.authStorage`.
55
+ > Native Apple/Google sign-in and remote push require a development build.
56
+
41
57
  ---
42
58
 
43
59
  > **Auth is automatic (v3+).** Database, storage, and functions calls
44
60
  > authenticate as the currently signed-in user — nothing to pass, no manual
45
- > wiring. Log in (or restore a session) and every request carries that
46
- > identity. `owner`/`authenticated` collections require an active session.
61
+ > wiring. Sign in (or restore a session) and every request carries that
62
+ > identity. `owner` and `authenticated` collections require an active session.
47
63
 
48
64
  ---
49
65
 
50
66
  ## Authentication
51
67
 
52
- Email + password, Apple Sign-In, Google Sign-In, and phone + OTP — out of the box.
68
+ Email and password, Apple Sign-In, Google Sign-In, and phone + OTP.
53
69
 
54
70
  ```typescript
55
- // Register
56
71
  await Koolbase.auth.register({ email: 'user@example.com', password: 'password' });
57
72
 
58
- // Login
59
- const session = await Koolbase.auth.login({ email: 'user@example.com', password: 'password' });
73
+ const session = await Koolbase.auth.login({
74
+ email: 'user@example.com',
75
+ password: 'password',
76
+ });
60
77
 
61
- // Current user
62
78
  const me = Koolbase.auth.currentUser;
63
79
 
64
- // Logout
65
80
  await Koolbase.auth.logout();
66
81
 
67
- // Password reset
68
82
  await Koolbase.auth.forgotPassword('user@example.com');
69
83
 
70
- // Listen to auth state changes (fires immediately with current state)
84
+ // Fires immediately with the current state, then on every change
71
85
  const unsubscribe = Koolbase.auth.onAuthStateChange((user) => {
72
86
  console.log(user ? 'signed in' : 'signed out');
73
87
  });
74
88
  ```
75
89
 
76
- ---
90
+ ### Sessions across restarts
91
+
92
+ With `react-native-keychain` installed, sessions persist. Restore at launch
93
+ before rendering:
94
+
95
+ ```typescript
96
+ import { RestoreResult } from '@koolbase/react-native';
77
97
 
78
- ### OAuth Apple
98
+ const result = await Koolbase.auth.restoreSession();
79
99
 
80
- Apple Sign-In uses the native authentication flow via `@invertase/react-native-apple-authentication` as a peer dependency:
100
+ switch (result) {
101
+ case RestoreResult.Restored: navigate('Home'); break;
102
+ case RestoreResult.Offline: navigate('Home'); break; // optimistic, no network
103
+ case RestoreResult.Expired: navigate('Login'); break;
104
+ case RestoreResult.NoSession: navigate('Login'); break;
105
+ }
106
+ ```
107
+
108
+ Optimistic state is read from disk before any network call, so authenticated UI
109
+ renders with no round-trip.
110
+
111
+ ### Sign in with Apple
81
112
 
82
113
  ```typescript
83
114
  import appleAuth from '@invertase/react-native-apple-authentication';
84
- import { Koolbase } from '@koolbase/react-native';
85
115
 
86
116
  const response = await appleAuth.performRequest({
87
117
  requestedOperation: appleAuth.Operation.LOGIN,
@@ -100,17 +130,13 @@ const session = await Koolbase.auth.signInWithApple({
100
130
  });
101
131
  ```
102
132
 
103
- Configure Apple Sign-In for your environment with your iOS app's Bundle ID. Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
133
+ Configure Apple Sign-In for your environment with your iOS Bundle ID. Setup
134
+ guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
104
135
 
105
- ---
106
-
107
- ### OAuth — Google
108
-
109
- Google Sign-In uses the native authentication flow via `@react-native-google-signin/google-signin` as a peer dependency:
136
+ ### Sign in with Google
110
137
 
111
138
  ```typescript
112
139
  import { GoogleSignin } from '@react-native-google-signin/google-signin';
113
- import { Koolbase } from '@koolbase/react-native';
114
140
 
115
141
  GoogleSignin.configure({
116
142
  webClientId: '<your-web-client-id>.apps.googleusercontent.com',
@@ -123,40 +149,36 @@ const session = await Koolbase.auth.signInWithGoogle({
123
149
  });
124
150
  ```
125
151
 
126
- Configure Google Sign-In for your environment with the OAuth client IDs from Google Cloud Console (typically one each for iOS, Android, and web). Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
127
-
128
- ---
152
+ Configure the OAuth client IDs from Google Cloud Console (one each for iOS,
153
+ Android, and web).
129
154
 
130
155
  ### Phone + OTP
131
156
 
132
157
  ```typescript
133
- // Send a one-time code
134
158
  await Koolbase.auth.sendOtp({ phoneNumber: '+233200000000' });
135
159
 
136
- // Verify and sign in
137
160
  await Koolbase.auth.verifyOtp({
138
161
  phoneNumber: '+233200000000',
139
162
  code: '123456',
140
163
  });
141
164
 
142
- // Or link a phone to an existing account
165
+ // Or attach a phone to an account that already exists
143
166
  await Koolbase.auth.linkPhone({
144
167
  phoneNumber: '+233200000000',
145
168
  code: '123456',
146
169
  });
147
170
  ```
148
171
 
149
- Configure your SMS provider (Twilio, Africa's Talking, or Hubtel) in the dashboard under Phone Auth.
172
+ Configure your SMS provider (Twilio, Africa's Talking, or Hubtel) in the
173
+ dashboard under Phone Auth.
150
174
 
151
175
  ---
152
176
 
153
177
  ## Database
154
178
 
155
179
  ```typescript
156
- // Insert
157
180
  await Koolbase.db.insert('posts', { title: 'Hello', published: true });
158
181
 
159
- // Query
160
182
  const { records } = await Koolbase.db.query('posts', {
161
183
  filters: { published: true },
162
184
  limit: 10,
@@ -164,204 +186,82 @@ const { records } = await Koolbase.db.query('posts', {
164
186
  orderDesc: true,
165
187
  });
166
188
 
167
- // Read fields off a record
168
189
  const post = records[0];
169
190
  console.log(post.data.title); // your fields live under .data
170
191
  console.log(post.id, post.collection); // metadata
171
192
 
172
- // Populate related records
173
- const { records: postsWithAuthor } = await Koolbase.db.query('posts', {
193
+ // Related records
194
+ const { records: withAuthor } = await Koolbase.db.query('posts', {
174
195
  populate: ['author_id:users'],
175
196
  });
176
197
 
177
- // Update / Delete
178
198
  await Koolbase.db.update('record-id', { title: 'Updated' });
179
199
  await Koolbase.db.delete('record-id');
180
200
  ```
181
201
 
182
- ---
183
-
184
- ### Handling unique-constraint conflicts
185
-
186
- A write that would violate a unique constraint throws `KoolbaseConflictError`:
187
-
188
- ```ts
189
- try {
190
- await Koolbase.db.upsert('users', { email }, { name });
191
- } catch (e) {
192
- if (e instanceof KoolbaseConflictError) {
193
- showError('That email is already registered.');
194
- }
195
- }
196
- ```
197
-
198
- ---
199
-
200
- ### Public bucket URLs
201
-
202
- For files in public buckets, you can construct the stable CDN URL directly — no
203
- network call, no expiry, embeddable anywhere a browser fetches a URL.
204
-
205
- ```typescript
206
- import { KoolbaseStorage } from '@koolbase/react-native';
207
-
208
- // From a KoolbaseObject you already have (e.g. from upload() or another read)
209
- const { object } = await Koolbase.storage.upload({
210
- bucket: 'avatars',
211
- path: `user-${userId}.jpg`,
212
- file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
213
- });
214
-
215
- const url = KoolbaseStorage.publicUrlForObject(object, 'avatars');
216
- // url is null for private-bucket objects; the CDN URL for public-bucket ones.
217
-
218
- if (url) {
219
- // Safe to use — file lives in the public R2 bucket
220
- return <Image source={{ uri: url }} />;
221
- }
222
-
223
- // For build-time URL construction (no Object on hand)
224
- const url = KoolbaseStorage.publicUrl({
225
- projectId: 'proj_abc',
226
- bucket: 'avatars',
227
- path: 'user-123.jpg',
228
- });
229
- // Always returns the URL pattern; caller is responsible for knowing
230
- // the file lives in a public bucket. For files in private buckets,
231
- // the resulting URL will 404.
232
- ```
233
-
234
- URLs follow the pattern `https://cdn.koolbase.com/{project_id}/{bucket}/{path}` — long-lived, edge-cached, no authentication. For files in private buckets, use `getDownloadUrl` instead, which returns a 1-hour presigned URL.
235
-
236
- ---
237
-
238
- ### Image transforms
239
-
240
- Public bucket URLs can be transformed at the edge — resize, reformat,
241
- optimize — without any preprocessing. Two ways:
242
-
243
- **Direct transforms** — pass a `transform` option to `publicUrl`:
244
-
245
- ```ts
246
- const url = KoolbaseStorage.publicUrl({
247
- projectId: 'proj_abc',
248
- bucket: 'avatars',
249
- path: 'user-123.jpg',
250
- transform: {
251
- width: 200,
252
- height: 200,
253
- fit: 'cover',
254
- format: 'auto',
255
- quality: 85,
256
- },
257
- });
258
- ```
259
-
260
- **Named presets** — store an option set server-side (via the dashboard or
261
- REST API), reference it by name:
262
-
263
- ```ts
264
- const url = KoolbaseStorage.publicUrlWithPreset({
265
- projectId: 'proj_abc',
266
- presetName: 'thumbnail',
267
- bucket: 'avatars',
268
- path: 'user-123.jpg',
269
- });
270
-
271
- // Or from a KoolbaseObject instance:
272
- const url = KoolbaseStorage.publicUrlForObjectWithPreset(object, 'avatars', 'thumbnail');
273
- ```
274
-
275
- Available options: `width` and `height` (1–2000), `format`
276
- (`auto`/`webp`/`avif`/`jpeg`/`png`), `quality` (1–100), `fit`
277
- (`scale-down`/`contain`/`cover`/`crop`/`pad`), `dpr` (1–3), `gravity`
278
- (`auto`/`center`/`top`/`bottom`/`left`/`right`/`top-left`/`top-right`/
279
- `bottom-left`/`bottom-right`). Transformed responses are edge-cached for 4
280
- hours; Cloudflare includes 5,000 unique transformations/month free per
281
- account.
282
-
283
- See the [Image Transforms docs](https://docs.koolbase.com/storage/image-transforms)
284
- for the full reference.
285
-
286
- ---
287
-
288
202
  ### Upsert
289
203
 
290
204
  Insert a record, or update the existing one matching a filter.
291
205
 
292
- ```ts
206
+ ```typescript
293
207
  const result = await Koolbase.db.upsert(
294
208
  'profiles',
295
209
  { user_id: userId },
296
- { weightKg: 70 }
210
+ { weightKg: 70 },
297
211
  );
298
212
 
299
- console.log(result.created); // true if inserted, false if updated
213
+ console.log(result.created); // true if inserted, false if updated
300
214
  console.log(result.record.id);
301
215
  ```
302
216
 
303
- > Online-only: needs the server's view to decide insert vs update, so unlike
304
- > `insert` it isn't queued offline and throws on network failure.
217
+ > Online-only: deciding insert versus update needs the server's view, so unlike
218
+ > `insert` it is not queued offline and throws on network failure.
305
219
 
306
220
  ### Delete where
307
221
 
308
222
  Bulk-delete every record matching a filter. Returns the number deleted.
309
223
 
310
- ```ts
224
+ ```typescript
311
225
  const deleted = await Koolbase.db.deleteWhere('sessions', {
312
226
  user_id: userId,
313
227
  status: 'expired',
314
228
  });
315
229
  ```
316
230
 
317
- > A non-empty filter is required. The collection's delete rule applies; for
318
- > `owner`/`scoped` rules the delete is scoped to your own records. Online-only.
319
-
320
- ---
321
-
322
- ### Offline-first
323
-
324
- ```typescript
325
- const { records, isFromCache } = await Koolbase.db.query('posts', { limit: 20 });
326
- if (isFromCache) console.log('Served from local cache');
327
-
328
- await Koolbase.db.syncPendingWrites();
329
- ```
330
-
331
- ---
231
+ > A non-empty filter is required. The collection's delete rule applies; under
232
+ > `owner` or `scoped` rules the delete is scoped to your own records.
233
+ > Online-only.
332
234
 
333
235
  ### Atomic batch writes
334
236
 
335
- Run multiple writes in a single server-side transaction. All operations commit together or none are applied — any failure rolls back the entire batch.
237
+ All operations commit together or none are applied.
336
238
 
337
- ```ts
239
+ ```typescript
338
240
  import { Koolbase, BatchOp } from '@koolbase/react-native';
339
241
 
340
242
  const results = await Koolbase.db.batch([
341
243
  BatchOp.insert('orders', { total: 50, customer_id: customerId }),
342
244
  BatchOp.update(inventoryId, { stock: 9 }),
343
- BatchOp.upsert('counters', {
344
- match: { name: 'orders' },
345
- data: { value: 1 },
346
- }),
245
+ BatchOp.upsert('counters', { match: { name: 'orders' }, data: { value: 1 } }),
347
246
  BatchOp.delete(cartItemId),
348
247
  ]);
349
248
 
350
249
  // results[i] corresponds to operations[i]:
351
- // - insert / update: { type, record }
352
- // - upsert: { type, record, created } // created = true if inserted
353
- // - delete: { type, deleted: true }
250
+ // insert / update -> { type, record }
251
+ // upsert -> { type, record, created }
252
+ // delete -> { type, deleted: true }
354
253
  ```
355
254
 
356
- **Online-only by design.** Atomicity needs the server's authoritative view, so `batch()` is never queued offline — it throws on network failure (like `upsert` and `deleteWhere`). A server-side rejection throws a `KoolbaseDataError` with the failing operation's details; nothing was persisted.
357
-
358
- ---
255
+ Atomicity needs the server's authoritative view, so `batch()` is never queued
256
+ offline — it throws on network failure. A rejection throws a `KoolbaseDataError`
257
+ carrying the failing operation's details; nothing was persisted.
359
258
 
360
259
  ### Handling write conflicts
361
260
 
362
- `insert`, `update`, and `upsert` are online-first: when the server is reachable they throw a typed error on rejection. Catch `KoolbaseConflictError` to handle unique-constraint violations (e.g. a duplicate email):
261
+ `insert`, `update`, and `upsert` are online-first: when the server is reachable
262
+ they throw on rejection.
363
263
 
364
- ```ts
264
+ ```typescript
365
265
  import { KoolbaseConflictError } from '@koolbase/react-native';
366
266
 
367
267
  try {
@@ -375,24 +275,125 @@ try {
375
275
  }
376
276
  ```
377
277
 
378
- When the device is offline, these writes are queued and synced automatically when connectivity returns.
379
-
380
278
  ---
381
279
 
382
- ### Semantic, lexical, and hybrid search
280
+ ## Offline-first
383
281
 
384
- Find records by meaning, exact terms, or both. Koolbase ships three
385
- retrieval modes from a single API pick the one that matches your
386
- query characteristics, or use `'hybrid'` as a strong production default.
282
+ Reads come from a local cache when the network is unavailable, and `insert`,
283
+ `update`, and `delete` are queued and sent when it returns.
387
284
 
388
- Declare a vector field on the collection from the dashboard or CLI first
389
- (picking a dimension; v1 supports 384, 768, 1024, and 1536).
285
+ ```typescript
286
+ const { records, isFromCache } = await Koolbase.db.query('posts', { limit: 20 });
287
+ if (isFromCache) console.log('Served from local cache');
390
288
 
391
- #### The three search modes
289
+ // Queued offline, applied on reconnect
290
+ await Koolbase.db.update(id, { title: 'Corrected' });
291
+ await Koolbase.db.delete(id);
292
+
293
+ // Sync happens on reconnect. To force it:
294
+ await Koolbase.db.syncPendingWrites();
295
+ ```
296
+
297
+ A server-side rejection is never queued. A unique-constraint violation, a
298
+ validation failure, or a permission denial surfaces immediately — only a genuine
299
+ network failure defers.
300
+
301
+ ### Showing what is waiting
302
+
303
+ ```typescript
304
+ const pending = await Koolbase.db.pendingWrites(); // oldest first
305
+
306
+ if (pending.length) {
307
+ showSyncBadge(pending.length);
308
+ }
309
+ ```
310
+
311
+ Queues are per-user and survive logout by design, so unsynced edits sync
312
+ whenever that user next signs in on this device — possibly never. Warn before
313
+ signing out with a non-empty queue.
314
+
315
+ > Requires a signed-in user. Per-user surfaces refuse rather than falling back
316
+ > to a shared anonymous bucket, so a signed-out call throws instead of reporting
317
+ > a misleading zero.
318
+
319
+ ### Editing offline requires having read the record
320
+
321
+ An update or delete is queued only if the SDK knows what the record looked like
322
+ when the change was made. Replaying a change without that means applying it
323
+ blindly: whatever else happened to the record meanwhile is overwritten,
324
+ silently, with nobody able to tell.
325
+
326
+ The SDK has that state if the record has been seen on this device — through a
327
+ query, a single read, a realtime event, or because it was created here and is
328
+ still queued. If not, the write is refused rather than queued:
329
+
330
+ ```typescript
331
+ try {
332
+ await Koolbase.db.update(id, { title: 'Corrected' });
333
+ } catch (e) {
334
+ if (e instanceof KoolbaseOfflineBaselineUnavailableError) {
335
+ // Never seen on this device. Read it, or make the change while online.
336
+ }
337
+ }
338
+ ```
339
+
340
+ That is deliberate rather than lenient. Queueing it anyway would mean most
341
+ offline updates are conflict-safe and some quietly are not, which is a worse
342
+ guarantee than a clear refusal.
343
+
344
+ ### When a queued write cannot be applied
345
+
346
+ On replay the server applies a queued write only if the record still carries the
347
+ revision the change was based on. If something changed it meanwhile — another
348
+ device, another user, a Function — the write is refused and held for a decision.
349
+ It is not lost, and not applied, and it survives restarts.
392
350
 
393
351
  ```typescript
394
- // Semantic (default) pure vector search via HNSW + cosine. Best for
395
- // fuzzy or conceptual queries where exact words don't have to match.
352
+ const conflicts = await Koolbase.db.conflicts();
353
+
354
+ for (const c of conflicts) {
355
+ c.local; // the change the user made
356
+ c.server; // the record as the server holds it now
357
+ c.divergentFields; // where they disagree
358
+ c.operation; // 'insert' | 'update' | 'delete'
359
+ c.reason; // why it is waiting
360
+
361
+ await c.resolveWithLocal(); // reapply the user's change
362
+ await c.resolveWithServer(); // keep the server's version
363
+ await c.resolveWithMerge({ ... }); // something composed from both
364
+ await c.abandon(); // drop it, neither side wins
365
+ }
366
+ ```
367
+
368
+ `reason` distinguishes two situations. `concurrent_modification` means the record
369
+ moved while the change was queued. `baseline_unavailable` means the change was
370
+ queued by a version of this SDK that did not record what it was based on — those
371
+ are migrated on upgrade rather than replayed, since there is nothing to check
372
+ them against.
373
+
374
+ A refused insert is a conflict too, and resolving it retries the insert, carrying
375
+ the conflict id as an idempotency key so a retry cannot double-write.
376
+
377
+ Resolving is itself conditional: if the record has moved again while someone was
378
+ deciding, resolution produces a new conflict rather than overwriting a change
379
+ nobody has seen.
380
+
381
+ > **These do not expire.** An app that never reads `conflicts()` accumulates them
382
+ > in local storage indefinitely, invisible to the user, with the changes they
383
+ > hold never applied. If you support offline editing, surface them somewhere.
384
+ > Automatic expiry would hide the problem while quietly losing the work.
385
+
386
+ ---
387
+
388
+ ## Search — semantic, lexical, and hybrid
389
+
390
+ Find records by meaning, by exact terms, or both. Declare a vector field on the
391
+ collection from the dashboard or CLI first, picking a dimension (384, 768, 1024,
392
+ or 1536).
393
+
394
+ ```typescript
395
+ // Semantic (default) — vector search via HNSW + cosine. Fuzzy or conceptual
396
+ // queries where exact words need not match.
396
397
  const result = await Koolbase.db.searchSemantic({
397
398
  collection: 'articles',
398
399
  field: 'content_embedding',
@@ -400,9 +401,9 @@ const result = await Koolbase.db.searchSemantic({
400
401
  limit: 10,
401
402
  });
402
403
 
403
- // Lexical — pure BM25 over the field's source text (Postgres
404
- // ts_rank_cd). Best for exact terms, product codes, names, acronyms.
405
- const result = await Koolbase.db.searchSemantic({
404
+ // Lexical — BM25 over the field's source text. Exact terms, product codes,
405
+ // names, acronyms.
406
+ await Koolbase.db.searchSemantic({
406
407
  collection: 'articles',
407
408
  field: 'content_embedding',
408
409
  queryText: 'CVE-2024-1234',
@@ -410,99 +411,86 @@ const result = await Koolbase.db.searchSemantic({
410
411
  limit: 10,
411
412
  });
412
413
 
413
- // Hybrid — vector + lexical fused with reciprocal rank fusion (k=60).
414
- // Generally the strongest default; both rankers vote and the fused
415
- // score promotes records that score well on either signal.
416
- const result = await Koolbase.db.searchSemantic({
414
+ // Hybrid — both, fused with reciprocal rank fusion (k=60). Generally the
415
+ // strongest default.
416
+ await Koolbase.db.searchSemantic({
417
417
  collection: 'articles',
418
418
  field: 'content_embedding',
419
419
  queryText: 'production deploy pipeline',
420
420
  mode: 'hybrid',
421
421
  limit: 10,
422
422
  });
423
+
424
+ for (const hit of result.hits) {
425
+ console.log(`${hit.record.data.title} ${hit.distance.toFixed(3)}`);
426
+ }
423
427
  ```
424
428
 
425
- #### Filtering weak matches
429
+ ### Filtering weak matches
426
430
 
427
- For `'semantic'` and `'hybrid'` modes, pass `minSimilarity` (0..100) to
428
- drop results below a similarity threshold server-side — saves bandwidth
429
- on weak matches:
431
+ For `semantic` and `hybrid`, `minSimilarity` (0100) drops results below a
432
+ threshold server-side:
430
433
 
431
434
  ```typescript
432
- const result = await Koolbase.db.searchSemantic({
435
+ await Koolbase.db.searchSemantic({
433
436
  collection: 'articles',
434
437
  field: 'content_embedding',
435
438
  queryText: 'how do I move quicker?',
436
439
  mode: 'hybrid',
437
- minSimilarity: 70, // only matches at least 70% similar
440
+ minSimilarity: 70,
438
441
  limit: 10,
439
442
  });
440
443
  ```
441
444
 
442
- `minSimilarity` is rejected by the server when used with `'lexical'` —
443
- BM25 rank scores aren't comparable to cosine similarity, and silently
444
- ignoring the parameter would produce confusing behavior.
445
+ The server rejects `minSimilarity` on `lexical` mode BM25 ranks are not
446
+ comparable to cosine similarity, and silently ignoring it would be confusing.
445
447
 
446
- #### Server-side embedding (recommended)
448
+ ### Server-side embedding (recommended)
447
449
 
448
- Configure an AI provider on the project once (Gemini's free tier works;
449
- OpenAI also supported), tag the vector field with the
450
- provider/model/source_field, and Koolbase auto-embeds records as
451
- they're inserted or updated. Lexical indexing happens automatically on
452
- the same write, so all three search modes work without extra setup:
450
+ Configure an AI provider on the project once (Gemini's free tier works; OpenAI
451
+ is also supported), tag the vector field with provider, model, and source field,
452
+ and records are embedded as they are written. Lexical indexing happens on the
453
+ same write, so all three modes work with no extra setup.
453
454
 
454
455
  ```typescript
455
- // One-time setup via dashboard. Then just write records normally
456
- // vectors AND lexical rows land within ~1s.
457
- await Koolbase.db.insert({
458
- collection: 'articles',
459
- data: {
460
- title: 'How to ship faster',
461
- content: 'Cut scope ruthlessly. Ship the smallest useful slice...',
462
- },
456
+ // Write records normally vectors and lexical rows land within about a second
457
+ await Koolbase.db.insert('articles', {
458
+ title: 'How to ship faster',
459
+ content: 'Cut scope ruthlessly. Ship the smallest useful slice...',
463
460
  });
464
461
 
465
- // Iterate over hits the same way regardless of mode:
466
- for (const hit of result.hits) {
467
- console.log(`${hit.record.data.title} ${hit.distance.toFixed(3)}`);
468
- }
469
-
470
- // Backfill records that pre-date the auto-embed config:
462
+ // Backfill records that pre-date the auto-embed config
471
463
  await Koolbase.db.embedText({
472
464
  collection: 'articles',
473
- recordId: article.$id,
465
+ recordId: article.id,
474
466
  vectorField: 'content_embedding',
475
467
  });
476
468
 
477
- // Or override the source useful for combining fields:
469
+ // Or override the source, to combine fields
478
470
  await Koolbase.db.embedText({
479
471
  collection: 'articles',
480
- recordId: article.$id,
472
+ recordId: article.id,
481
473
  vectorField: 'content_embedding',
482
474
  text: `${article.title}\n\n${article.summary}`,
483
475
  });
484
476
  ```
485
477
 
486
- #### Client-side embedding (advanced)
478
+ ### Client-side embedding
487
479
 
488
- If you'd rather control the embedding model yourself, pass a vector
489
- instead of text. Note that lexical and hybrid modes require text, since
490
- BM25 has no notion of "vector queries":
480
+ Pass a vector instead of text if you would rather control the model. Lexical and
481
+ hybrid modes require text, since BM25 has no notion of a vector query.
491
482
 
492
483
  ```typescript
493
- // Set a vector you've encoded yourself
494
484
  await Koolbase.db.setVector(
495
485
  articleId,
496
486
  'embedding',
497
487
  await myEmbeddingModel.encode(article.content),
498
488
  );
499
489
 
500
- // Read it back
501
490
  const v = await Koolbase.db.getVector(articleId, 'embedding');
502
491
  console.log(`${v.vector.length}-dim, updated ${v.updatedAt}`);
503
492
 
504
- // Search with a precomputed vector — semantic mode only.
505
- const result = await Koolbase.db.searchSemantic({
493
+ await Koolbase.db.searchSemantic({
506
494
  collection: 'articles',
507
495
  field: 'embedding',
508
496
  queryVector: await myEmbeddingModel.encode(userQuery),
@@ -510,55 +498,41 @@ const result = await Koolbase.db.searchSemantic({
510
498
  where: { category: 'tech' },
511
499
  });
512
500
 
513
- // Remove a record's vector when no longer needed
514
501
  await Koolbase.db.deleteVector(articleId, 'embedding');
515
502
  ```
516
503
 
517
- #### Behaviors worth knowing
504
+ ### Behaviours worth knowing
518
505
 
519
- - **Pass exactly one of `queryVector` or `queryText`.** Supplying both
520
- or neither throws an `Error`.
521
- - **`queryVector` is for semantic mode only.** Lexical and hybrid need
522
- raw text — the server uses it for BM25 ranking (and embeds it inline
523
- for the vector half of hybrid).
524
- - **Vector length must match the declared dimension.** Mismatches throw
506
+ - **Pass exactly one of `queryVector` or `queryText`.** Both or neither throws.
507
+ - **`queryVector` is semantic-mode only.** Lexical and hybrid need raw text.
508
+ - **Vector length must match the declared dimension**, or
525
509
  `KoolbaseVectorDimensionMismatchError`.
526
- - **`minSimilarity` must be 0..100.** Values outside that range throw
527
- an `Error` client-side before the request is sent.
528
- - **Online-only.** Vector operations are not cached locally or queued
529
- offline — HNSW similarity and BM25 ranking have no useful offline
530
- semantics.
531
- - **Read rule applies post-search.** `owner`/`scoped`/`conditional`
532
- records are filtered to the caller after retrieval, so strict rules
533
- may return fewer than `limit` results.
534
- - **`embedText` is async.** Returns when the job is queued (~100ms).
535
- The vector lands within 1 second once the worker picks it up.
536
- - **Higher dimensions coming.** `text-embedding-3-large` (3072 dim)
537
- supported once pgvector is upgraded. Use `dimensions=1536`
538
- Matryoshka truncation in the meantime.
539
-
540
- See [Semantic search docs](https://docs.koolbase.com/database/vectors)
541
- for setup, provider configuration, embedding model recommendations,
542
- and when to pick each mode.
510
+ - **Online-only.** Vector operations are not cached or queued — similarity
511
+ search has no useful offline semantics.
512
+ - **Read rules apply after retrieval.** Strict rules may return fewer than
513
+ `limit` results.
514
+ - **`embedText` is async.** It returns once the job is queued; the vector lands
515
+ within about a second.
516
+
517
+ See [the vector docs](https://docs.koolbase.com/database/vectors) for provider
518
+ setup and when to pick each mode.
543
519
 
544
520
  ---
545
521
 
546
522
  ## Storage
547
523
 
548
- Upload and serve files via presigned URLs to Cloudflare R2. Uploads are
549
- **safe-by-default** (v5+) — uploading to a path that's already taken throws
550
- `KoolbaseStorageConflictError` instead of silently replacing the existing
551
- file. Pass `overwrite: true` for true upsert semantics.
524
+ Presigned uploads and downloads backed by Cloudflare R2. Uploads are
525
+ safe-by-default: a path that is already taken throws rather than silently
526
+ replacing the file.
552
527
 
553
528
  ```typescript
554
- // Upload — rejects if `user-${userId}.jpg` already exists
555
529
  const { object, downloadUrl } = await Koolbase.storage.upload({
556
530
  bucket: 'avatars',
557
531
  path: `user-${userId}.jpg`,
558
532
  file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
559
533
  });
560
534
 
561
- // Upload silently replaces any existing object at this path
535
+ // Replace whatever is there
562
536
  await Koolbase.storage.upload({
563
537
  bucket: 'avatars',
564
538
  path: `user-${userId}.jpg`,
@@ -566,18 +540,14 @@ await Koolbase.storage.upload({
566
540
  overwrite: true,
567
541
  });
568
542
 
569
- // Get download URL
570
543
  const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg`);
571
544
 
572
- // Delete
573
545
  await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);
574
546
  ```
575
547
 
576
- ---
577
-
578
548
  ### Handling upload conflicts
579
549
 
580
- For user-supplied filenames, prompt the user before overwriting:
550
+ For user-supplied filenames, prompt before overwriting:
581
551
 
582
552
  ```typescript
583
553
  import { KoolbaseStorageConflictError } from '@koolbase/react-native';
@@ -588,35 +558,29 @@ try {
588
558
  path: filename,
589
559
  file: { uri, name: filename, type: mimeType },
590
560
  });
591
- } catch (e) catch (e) {
592
- if (e instanceof KoolbaseStorageConflictError) {
593
- const ok = await confirm(${e.path} already exists. Overwrite?);
594
- if (ok) {
595
- await Koolbase.storage.upload({
596
- bucket: 'documents',
597
- path: filename,
598
- file: { uri, name: filename, type: mimeType },
599
- overwrite: true,
600
- });
601
- }
602
- } else {
603
- throw e;
604
- }
561
+ } catch (e) {
562
+ if (e instanceof KoolbaseStorageConflictError) {
563
+ const ok = await confirm(`${e.path} already exists. Overwrite?`);
564
+ if (ok) {
565
+ await Koolbase.storage.upload({
566
+ bucket: 'documents',
567
+ path: filename,
568
+ file: { uri, name: filename, type: mimeType },
569
+ overwrite: true,
570
+ });
571
+ }
572
+ } else {
573
+ throw e;
574
+ }
605
575
  }
606
576
  ```
607
577
 
608
- See [Error handling](#error-handling) for the full set of storage errors.
609
-
610
- ---
611
-
612
- ### Handling bucket limits
578
+ ### Bucket limits
613
579
 
614
- Buckets can be configured at creation time with a total size cap
615
- (`max_size_bytes`), a per-file cap (`max_file_size_bytes`), and a
616
- content-type allowlist (`allowed_mime_types`, supports `image/*`-style
617
- wildcards). The server surfaces violations as typed errors:
580
+ Buckets can carry a total size cap, a per-file cap, and a content-type
581
+ allowlist (`image/*` wildcards supported). Violations arrive as typed errors:
618
582
 
619
- ````typescript
583
+ ```typescript
620
584
  import {
621
585
  KoolbaseStorageQuotaError,
622
586
  KoolbaseStorageFileTooLargeError,
@@ -640,110 +604,155 @@ try {
640
604
  throw e;
641
605
  }
642
606
  }
643
- ````
607
+ ```
644
608
 
645
- MIME enforcement runs at presign time no bytes are transferred before
646
- rejection. File-size and quota enforcement run at confirm time; the
647
- server cleans up the underlying R2 object before returning the error,
648
- so nothing leaks.
609
+ MIME enforcement runs at presign time, so no bytes move before rejection.
610
+ Size and quota enforcement run at confirm time, and the server removes the
611
+ underlying R2 object before returning the error.
649
612
 
650
- ---
613
+ ### Public bucket URLs
614
+
615
+ Files in public buckets have a stable CDN URL — no network call, no expiry,
616
+ embeddable anywhere.
617
+
618
+ ```typescript
619
+ import { KoolbaseStorage } from '@koolbase/react-native';
620
+
621
+ // From an object you already have
622
+ const url = KoolbaseStorage.publicUrlForObject(object, 'avatars');
623
+ // null for private-bucket objects; the CDN URL for public ones
624
+
625
+ if (url) {
626
+ return <Image source={{ uri: url }} />;
627
+ }
628
+
629
+ // Build-time construction, no object on hand
630
+ const built = KoolbaseStorage.publicUrl({
631
+ projectId: 'proj_abc',
632
+ bucket: 'avatars',
633
+ path: 'user-123.jpg',
634
+ });
635
+ // Always returns the pattern; the caller is responsible for knowing the file
636
+ // is public. A private-bucket path will 404.
637
+ ```
638
+
639
+ URLs follow `https://cdn.koolbase.com/{project_id}/{bucket}/{path}` — long-lived
640
+ and edge-cached. For private buckets use `getDownloadUrl`, which returns a
641
+ one-hour presigned URL.
642
+
643
+ ### Image transforms
644
+
645
+ Public URLs can be resized and reformatted at the edge with no preprocessing.
646
+
647
+ ```typescript
648
+ const url = KoolbaseStorage.publicUrl({
649
+ projectId: 'proj_abc',
650
+ bucket: 'avatars',
651
+ path: 'user-123.jpg',
652
+ transform: { width: 200, height: 200, fit: 'cover', format: 'auto', quality: 85 },
653
+ });
654
+ ```
655
+
656
+ Or store the option set server-side and reference it by name:
657
+
658
+ ```typescript
659
+ const url = KoolbaseStorage.publicUrlWithPreset({
660
+ projectId: 'proj_abc',
661
+ presetName: 'thumbnail',
662
+ bucket: 'avatars',
663
+ path: 'user-123.jpg',
664
+ });
665
+
666
+ const fromObject = KoolbaseStorage.publicUrlForObjectWithPreset(
667
+ object,
668
+ 'avatars',
669
+ 'thumbnail',
670
+ );
671
+ ```
672
+
673
+ Options: `width` and `height` (1–2000), `format`
674
+ (`auto`/`webp`/`avif`/`jpeg`/`png`), `quality` (1–100), `fit`
675
+ (`scale-down`/`contain`/`cover`/`crop`/`pad`), `dpr` (1–3), and `gravity` (ten
676
+ anchor positions). Transformed responses are edge-cached for four hours, and
677
+ every account includes 5,000 unique transformations a month.
651
678
 
652
679
  ### Object versioning
653
680
 
654
- For buckets with versioning enabled, every overwrite preserves the prior
655
- content as a history version, and deletes are soft (recoverable until
656
- force-purged). Enable versioning on a bucket from the dashboard.
681
+ On buckets with versioning enabled, overwrites preserve the prior content and
682
+ deletes are soft.
657
683
 
658
684
  ```typescript
659
- // List all versions of a path, newest first
660
685
  const versions = await Koolbase.storage.listVersions('documents', 'contract.pdf');
661
686
 
662
687
  for (const v of versions) {
663
688
  console.log(`${v.versionId}: size=${v.size} isCurrent=${v.isCurrent}`);
664
689
  }
665
690
 
666
- // Download a specific historical version
691
+ // Download a historical version
667
692
  const url = await Koolbase.storage.getDownloadUrl(
668
693
  'documents',
669
694
  'contract.pdf',
670
695
  '019e98ed-eed6-7e71-...',
671
696
  );
672
697
 
673
- // Bring a history version back as current
674
- // (the existing current is snapshotted to history first)
675
- const restored = await Koolbase.storage.restoreVersion(
676
- 'documents',
677
- 'contract.pdf',
678
- '019e98ed-eed6-7e71-...',
679
- );
698
+ // Bring one back as current (the existing current is snapshotted first)
699
+ await Koolbase.storage.restoreVersion('documents', 'contract.pdf', '019e98ed-...');
680
700
 
681
- // Hard-remove a single history version (row + R2 bytes)
682
- await Koolbase.storage.purgeVersion(
683
- 'documents',
684
- 'contract.pdf',
685
- 'old-version-id',
686
- );
701
+ // Hard-remove one history version, row and bytes
702
+ await Koolbase.storage.purgeVersion('documents', 'contract.pdf', 'old-version-id');
687
703
 
688
- // Wipe the entire timeline for a path - every version, every R2 key
704
+ // Wipe the whole timeline for a path
689
705
  await Koolbase.storage.delete('documents', 'contract.pdf', true);
690
706
  ```
691
707
 
692
- A few behaviors worth knowing:
693
-
694
- - **Overwrite snapshots automatically.** Upload to a path that already
695
- exists in a versioned bucket and the prior bytes are preserved as
696
- history; the upload becomes the new current.
697
- - **Delete is soft by default.** On a versioned bucket, `delete`
698
- snapshots the current content and records a delete marker. The
699
- content is still recoverable via `restoreVersion` until force-purged.
700
- - **Restore is itself a versioned event.** The previously-current row
701
- gets snapshotted before the target's bytes overwrite canonical. The
702
- restored row gets a fresh `versionId`; the target stays in history at
703
- its original id - so you can always undo a restore.
704
- - **Delete markers can be listed but not downloaded.** A marker has
705
- `size === 0`, `isDeleteMarker === true`, and no bytes. Calling
706
- `getDownloadUrl` with a marker's `versionId` throws.
708
+ - **Overwrite snapshots automatically.** The prior bytes become history; the
709
+ upload becomes current.
710
+ - **Delete is soft.** The current content is snapshotted and a delete marker
711
+ recorded, recoverable until force-purged.
712
+ - **Restore is itself versioned.** The restored row gets a fresh `versionId` and
713
+ the target stays in history at its original id, so a restore can be undone.
714
+ - **Delete markers list but do not download.** `size === 0`,
715
+ `isDeleteMarker === true`, and `getDownloadUrl` on one throws.
707
716
 
708
717
  ---
709
718
 
710
719
  ## Realtime
711
720
 
712
721
  Subscribe to live changes on a collection. Uses the signed-in user's session, so
713
- subscribe after login. Streams `created`, `updated`, and `deleted` events for
714
- collections whose read rule is `public` or `authenticated`.
722
+ subscribe after sign-in.
715
723
 
716
- ```ts
724
+ ```typescript
717
725
  const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
718
- // event.type -> 'created' | 'updated' | 'deleted'
719
726
  if (event.type === 'deleted') {
720
- console.log('deleted', event.recordId); // recordId on deletes
727
+ console.log('deleted', event.recordId); // recordId on deletes
721
728
  } else {
722
- console.log(event.type, event.record!.data); // record on created/updated
729
+ console.log(event.type, event.record!.data); // record on created/updated
723
730
  }
724
731
  });
725
732
 
726
733
  unsubscribe();
727
734
  ```
728
735
 
729
- The socket opens lazily, is shared, and reconnects automatically. The project is
730
- taken from the user's session.
736
+ The socket opens lazily, is shared across subscriptions, and reconnects with
737
+ backoff that doubles up to a minute and resets when a connection opens. The
738
+ project is taken from the session.
731
739
 
732
740
  ---
733
741
 
734
742
  ## Functions
735
743
 
736
- Invoke deployed serverless functions. When a user is signed in via `Koolbase.auth`, their access token is automatically forwarded — the function receives the caller's identity via `ctx.auth`. No token handling on the client side.
744
+ Invoke deployed functions. A signed-in user's access token is forwarded
745
+ automatically, so the function sees the caller on `ctx.auth`.
737
746
 
738
747
  ```typescript
739
- // Invoke a deployed function
740
748
  const result = await Koolbase.functions.invoke('send-welcome-email', {
741
749
  userId: '123',
742
750
  });
751
+
743
752
  if (result.success) console.log(result.data);
744
753
  ```
745
754
 
746
- Inside the function, read the caller:
755
+ Inside the function:
747
756
 
748
757
  ```typescript
749
758
  export async function handler(ctx) {
@@ -751,112 +760,48 @@ export async function handler(ctx) {
751
760
  if (!userId) {
752
761
  return { error: { code: 'AUTH_REQUIRED' }, status: 401 };
753
762
  }
754
- // Authenticated logic here
755
763
  return { ok: true };
756
764
  }
757
765
  ```
758
766
 
759
- Token refresh is transparent the SDK reads the current token fresh on every invoke. Full docs at [docs.koolbase.com/functions/authentication](https://docs.koolbase.com/functions/authentication).
767
+ Failures are typed`FunctionNotFoundError`, `FunctionPermissionError`,
768
+ `FunctionValidationError`, `FunctionQuotaExceededError`,
769
+ `FunctionExecutionError` — so handling never depends on message text. Token
770
+ refresh is transparent.
760
771
 
761
772
  ---
762
773
 
763
- ## Feature Flags & Remote Config
774
+ ## Feature flags and remote config
764
775
 
765
776
  ```typescript
766
777
  if (Koolbase.isEnabled('new_checkout')) { /* ... */ }
767
778
 
768
779
  const timeout = Koolbase.configNumber('timeout_seconds', 30);
769
- const apiUrl = Koolbase.configString('api_url', 'https://api.myapp.com');
770
- const dark = Koolbase.configBool('force_dark_mode', false);
780
+ const apiUrl = Koolbase.configString('api_url', 'https://api.myapp.com');
781
+ const dark = Koolbase.configBool('force_dark_mode', false);
771
782
  ```
772
783
 
773
- ---
774
-
775
- ## Version Enforcement
776
-
777
- ```typescript
778
- const result = Koolbase.checkVersion('1.2.3');
779
- if (result.status === 'force_update') {
780
- // block and show update screen
781
- }
782
- ```
784
+ Rollout buckets are computed from a stable per-install device id, so a 10%
785
+ rollout is genuinely 10% of devices.
783
786
 
784
787
  ---
785
788
 
786
- ## Code Push
787
-
788
- Push config overrides, feature flag overrides, and directive-driven behaviour without a store release.
789
+ ## Version enforcement
789
790
 
790
791
  ```typescript
791
- await Koolbase.initialize({
792
- publicKey: 'pk_live_xxxx',
793
- baseUrl: 'https://api.koolbase.com',
794
- codePushChannel: 'stable',
795
- });
796
-
797
- // Bundle values override Remote Config + Feature Flags transparently
798
- const timeout = Koolbase.configNumber('api_timeout_ms', 3000);
799
-
800
- // Directive handlers
801
- Koolbase.codePush.onDirective('force_logout_all', (value) => {
802
- if (value) Koolbase.auth.logout();
803
- });
804
- Koolbase.codePush.applyDirectives();
805
- ```
806
-
807
- ---
808
-
809
- ### Mandatory updates
810
-
811
- Mark a bundle **mandatory** in the dashboard (or via `PATCH /mandatory`) when every device must apply it before continuing — surfaced as a push callback and a pollable flag:
812
-
813
- ```typescript
814
- await Koolbase.initialize({
815
- publicKey: 'pk_live_xxxx',
816
- baseUrl: 'https://api.koolbase.com',
817
- // Fires the moment a mandatory bundle is staged for the next launch
818
- onMandatoryUpdate: ({ version }) => {
819
- showRestartRequiredDialog(version);
820
- },
821
- });
822
-
823
- // Or poll it — e.g. on app resume — before letting the user proceed
824
- if (Koolbase.codePush.hasMandatoryUpdate) {
825
- showRestartRequiredDialog();
826
- }
827
- ```
828
-
829
- A mandatory bundle still activates on the next cold launch like any other; the callback and flag just let you prompt the user to restart now instead of waiting.
830
-
831
- ---
832
-
833
- ## Logic Engine
834
-
835
- Define conditional app behavior as data in your Runtime Bundle — no code changes required.
836
-
837
- ```typescript
838
- const result = Koolbase.executeFlow('on_checkout_tap', {
839
- plan: user.plan,
840
- usage: user.usage,
841
- });
792
+ const result = Koolbase.checkVersion('1.2.3');
842
793
 
843
- if (result.hasEvent) {
844
- switch (result.eventName) {
845
- case 'show_upgrade': navigation.navigate('Upgrade'); break;
846
- case 'go_checkout': navigation.navigate('Checkout'); break;
847
- }
794
+ if (result.status === 'force_update') {
795
+ // block and show an update screen
848
796
  }
849
797
  ```
850
798
 
851
- **v2 operators:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, `ends_with`, `in_list`, `not_in_list`, `between`, `is_true`, `is_false`, `exists`, `not_exists`, `and`, `or`
852
-
853
- Full docs at [docs.koolbase.com/sdk/logic-engine](https://docs.koolbase.com/sdk/logic-engine).
854
-
855
799
  ---
856
800
 
857
801
  ## Analytics
858
802
 
859
- Track screen views, custom events, and user behaviour. View DAU, WAU, MAU, funnels, and retention in the Koolbase dashboard.
803
+ Screen views, custom events, and user properties. DAU, WAU, MAU, funnels and
804
+ retention appear in the dashboard.
860
805
 
861
806
  ```typescript
862
807
  await Koolbase.initialize({
@@ -866,23 +811,21 @@ await Koolbase.initialize({
866
811
  appVersion: '1.0.0',
867
812
  });
868
813
 
869
- // Custom events
870
814
  Koolbase.analytics.track('purchase', { value: 1200, currency: 'GHS' });
871
-
872
- // Screen views
873
815
  Koolbase.analytics.screenView('checkout');
874
-
875
- // User identity
876
816
  Koolbase.analytics.identify(user.id);
877
817
  Koolbase.analytics.setUserProperty('plan', 'pro');
878
818
 
879
- // On logout
819
+ // On sign-out
880
820
  Koolbase.analytics.reset();
881
821
  ```
882
822
 
823
+ Events batch and flush every 30 seconds, on backgrounding, on close, or at 20
824
+ events.
825
+
883
826
  ---
884
827
 
885
- ## Cloud Messaging
828
+ ## Cloud messaging
886
829
 
887
830
  ```typescript
888
831
  await Koolbase.initialize({
@@ -891,17 +834,18 @@ await Koolbase.initialize({
891
834
  messagingEnabled: true,
892
835
  });
893
836
 
894
- // Register FCM token (after obtaining from @react-native-firebase/messaging)
895
837
  const fcmToken = await messaging().getToken();
838
+
896
839
  await Koolbase.messaging.registerToken({
897
840
  token: fcmToken,
898
841
  platform: 'android', // or 'ios'
899
842
  });
900
843
  ```
901
844
 
902
- Sending is server-initiated from your backend or a Koolbase Function with a
903
- secret `kb_live_` key, never the app (the publishable key ships in your bundle
904
- and can't send). See the [Cloud Messaging docs](https://docs.koolbase.com/sdk/messaging).
845
+ Sending is server-initiated only. It needs a secret `kb_live_` key and must run
846
+ from your backend or a Koolbase Function — the publishable key ships inside your
847
+ bundle, so a client that could send would let anyone who extracts it push to
848
+ your users.
905
849
 
906
850
  ```bash
907
851
  curl -X POST https://api.koolbase.com/v1/messaging/send \
@@ -914,29 +858,44 @@ curl -X POST https://api.koolbase.com/v1/messaging/send \
914
858
 
915
859
  ## Error handling
916
860
 
917
- Koolbase throws typed errors selected from the server's stable error `code`, so
918
- handling doesn't depend on message text.
861
+ Errors are selected from the server's stable error `code`, so handling never
862
+ depends on message text. Everything the SDK raises extends `KoolbaseError`.
863
+
864
+ ### Raised by any subsystem
919
865
 
920
- ### Database errors
866
+ | Error | When |
867
+ |---|---|
868
+ | `KoolbaseUnauthenticatedError` | The server would not accept the caller's credentials (401) — expired session, revoked key, or none at all; it does not distinguish. A session stops working for the whole SDK at once, so this comes from database, storage, Functions and background sync alike. **The user is already signed out by the time you catch it** — route to login rather than retrying. |
869
+ | `KoolbaseOfflineBaselineUnavailableError` | An offline update or delete could not be queued: the record has never been seen on this device, so there is nothing to apply the change against. |
921
870
 
922
- All data-layer failures extend `KoolbaseDataError` (which extends `Error`):
871
+ `KoolbasePermissionError` (403) is different: the credentials *were* accepted and
872
+ this caller may not proceed. Nobody is signed out — doing so for opening the
873
+ wrong record would be worse than the failure itself.
874
+
875
+ ### Database
923
876
 
924
877
  | Error | When |
925
878
  |---|---|
926
- | `KoolbaseConflictError` | A write violates a unique constraint (409). Exposes `.field` the field that collided, when the server reports it. |
927
- | `KoolbaseNotFoundError` | The record or collection doesn't exist (404). |
879
+ | `KoolbaseConflictError` | A write violates a unique constraint (409). `.field` names the collision when the server reports it. |
880
+ | `KoolbaseNotFoundError` | The record or collection does not exist (404). |
928
881
  | `KoolbaseValidationError` | The request was rejected as invalid (400). |
929
882
  | `KoolbasePermissionError` | An access rule denied the operation (403). |
930
883
  | `KoolbaseRateLimitError` | The caller is being rate-limited (429). |
931
- | `KoolbaseVectorDimensionMismatchError` | A vector's length doesn't match the field's declared dimension (400, code `vector_dimension_mismatch`). |
884
+ | `KoolbaseVectorDimensionMismatchError` | A vector's length does not match the field's declared dimension. |
932
885
 
933
- ```ts
934
- import { KoolbaseConflictError, KoolbaseDataError } from '@koolbase/react-native';
886
+ ```typescript
887
+ import {
888
+ KoolbaseConflictError,
889
+ KoolbaseDataError,
890
+ KoolbaseUnauthenticatedError,
891
+ } from '@koolbase/react-native';
935
892
 
936
893
  try {
937
894
  await Koolbase.db.upsert('users', { email }, { name });
938
895
  } catch (e) {
939
- if (e instanceof KoolbaseConflictError) {
896
+ if (e instanceof KoolbaseUnauthenticatedError) {
897
+ goToLogin(); // already signed out
898
+ } else if (e instanceof KoolbaseConflictError) {
940
899
  showError(`That ${e.field ?? 'value'} is already taken.`);
941
900
  } else if (e instanceof KoolbaseDataError) {
942
901
  showError(e.message);
@@ -944,67 +903,59 @@ try {
944
903
  }
945
904
  ```
946
905
 
947
- > `query`, `get`, `upsert`, and `deleteWhere` throw these typed errors. `insert`,
948
- > `update`, and `delete` are optimistic/offline-first they queue and sync in
949
- > the background, so their conflicts surface via the sync engine, not as a
950
- > thrown error.
951
-
952
- ---
906
+ > `insert`, `update`, and `delete` queue when the network is unreachable, but
907
+ > they still throw. A server that answered has refused, and that is surfaced
908
+ > rather than queued, since it would be refused again on every retry.
909
+ >
910
+ > What does not throw is a write refused during replay, hours after it was made:
911
+ > nobody is waiting on it, so it becomes a conflict you read from
912
+ > `Koolbase.db.conflicts()`.
953
913
 
954
- ### Storage errors
955
-
956
- All storage failures extend `KoolbaseStorageError` (which extends `Error`):
914
+ ### Storage
957
915
 
958
916
  | Error | When |
959
917
  |---|---|
960
- | `KoolbaseStorageConflictError` | An upload targets a path that's already taken and `overwrite: false` (409, code `PATH_CONFLICT`). Exposes `.path` the colliding path. |
961
- | `KoolbaseStorageNotFoundError` | The bucket or object doesn't exist (404). |
962
- | `KoolbaseStorageValidationError` | The request was rejected as invalid — bad path, missing field (400). |
963
- | `KoolbaseStoragePermissionError` | The caller is not allowed to perform the operation (403). |
964
-
965
- ```ts
966
- import {
967
- KoolbaseStorageConflictError,
968
- KoolbaseStorageError,
969
- KoolbaseStoragePermissionError,
970
- } from '@koolbase/react-native';
971
-
972
- try {
973
- await Koolbase.storage.upload({
974
- bucket: 'avatars',
975
- path: 'me.png',
976
- file: { uri, name: 'me.png', type: 'image/png' },
977
- });
978
- } catch (e) {
979
- if (e instanceof KoolbaseStorageConflictError) {
980
- // Already exists — prompt user to confirm overwrite
981
- promptOverwrite(e.path);
982
- } else if (e instanceof KoolbaseStoragePermissionError) {
983
- showError('You do not have permission to upload here.');
984
- } else if (e instanceof KoolbaseStorageError) {
985
- // Catch-all for any other storage error
986
- showError(e.message);
987
- } else {
988
- throw e;
989
- }
990
- }
991
- ```
918
+ | `KoolbaseStorageConflictError` | The path is taken and `overwrite` is false (409). `.path` names it. |
919
+ | `KoolbaseStorageNotFoundError` | The bucket or object does not exist (404). |
920
+ | `KoolbaseStorageValidationError` | Bad path or missing field (400). |
921
+ | `KoolbaseStoragePermissionError` | The caller may not perform the operation (403). |
922
+ | `KoolbaseStorageQuotaError` | The upload would exceed the bucket's total size cap. |
923
+ | `KoolbaseStorageFileTooLargeError` | The file exceeds the bucket's per-file cap. |
924
+ | `KoolbaseStorageMimeTypeError` | The content type is not in the bucket's allowlist. |
925
+ | `KoolbaseStorageMetadataInvalidError` | Object metadata failed validation. `.detail` names the key and rule. |
926
+
927
+ ### Auth
928
+
929
+ `InvalidCredentialsError`, `EmailAlreadyInUseError`, `UserDisabledError`,
930
+ `WeakPasswordError`, `SessionExpiredError`, `TokenRevokedError`,
931
+ `AccountLockedError` (with `lockedUntil`), `UnlockTokenInvalidError`,
932
+ `RateLimitError`, `NetworkError`, plus the OAuth family
933
+ (`AppleSignInNotConfiguredError`, `InvalidAppleTokenError`,
934
+ `GoogleSignInNotConfiguredError`, `InvalidGoogleTokenError`,
935
+ `OAuthEmailConflictError`) and the phone family (`InvalidPhoneNumberError`,
936
+ `OtpExpiredError`, `OtpInvalidError`, `OtpMaxAttemptsError`,
937
+ `OtpRateLimitError`, `PhoneAlreadyLinkedError`, `SmsConfigMissingError`). All
938
+ extend `KoolbaseAuthError`.
992
939
 
993
940
  ---
994
941
 
995
942
  ## What's included
996
943
 
997
- - Authentication: email + password, Apple Sign-In, Google Sign-In, phone + OTP
998
- - Database with offline-first cache, realtime subscriptions, populate for related records, semantic search over vectors
999
- - Storage with presigned uploads and downloads, safe-by-default conflict handling, image transforms, object versioning (history + restore + soft-delete)
1000
- - Realtime subscriptions over WebSocket
1001
- - Authenticated functions (`ctx.auth` exposes the caller automatically)
944
+ - Authentication: email and password, Apple, Google, phone + OTP, persistent
945
+ sessions, auth state listener
946
+ - Database with populate for related records, atomic batches, upsert and bulk
947
+ delete
948
+ - Offline-first cache, a durable write queue with baselines, and conflicts you
949
+ resolve four ways
950
+ - Semantic, lexical and hybrid search over vectors, with server-side embedding
951
+ - Storage with presigned uploads, safe-by-default conflicts, bucket limits,
952
+ image transforms, and object versioning
953
+ - Realtime subscriptions over WebSocket with backoff
954
+ - Functions with the caller's identity forwarded automatically
1002
955
  - Feature flags and remote config
1003
956
  - Version enforcement
1004
- - Code push (config + flag overrides + directives, no store release)
1005
- - Logic engine (conditional flows as data, updatable OTA)
1006
- - Analytics (DAU/WAU/MAU, funnels, retention)
1007
- - Cloud Messaging (FCM token registration, targeted send, broadcast)
957
+ - Analytics DAU, WAU, MAU, funnels, retention
958
+ - Cloud messaging device token registration
1008
959
  - TypeScript-native with full type definitions
1009
960
 
1010
961
  ---
@@ -1019,9 +970,9 @@ Manage your projects at [app.koolbase.com](https://app.koolbase.com)
1019
970
 
1020
971
  ## Support
1021
972
 
1022
- - [GitHub Issues](https://github.com/kennedyowusu/koolbase-react-native/issues)
973
+ - [GitHub Issues](https://github.com/koolbase/koolbase-react-native/issues)
1023
974
  - [docs.koolbase.com](https://docs.koolbase.com)
1024
- - Email: <hello@koolbase.com>
975
+ - Email: <dev@koolbase.com>
1025
976
 
1026
977
  ## License
1027
978