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