@koolbase/react-native 9.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 (44) hide show
  1. package/README.md +1025 -0
  2. package/dist/analytics.d.ts +26 -0
  3. package/dist/analytics.js +138 -0
  4. package/dist/apple-auth.d.ts +22 -0
  5. package/dist/apple-auth.js +74 -0
  6. package/dist/auth-errors.d.ts +117 -0
  7. package/dist/auth-errors.js +250 -0
  8. package/dist/auth-storage.d.ts +26 -0
  9. package/dist/auth-storage.js +105 -0
  10. package/dist/auth.d.ts +199 -0
  11. package/dist/auth.js +794 -0
  12. package/dist/cache-store.d.ts +11 -0
  13. package/dist/cache-store.js +136 -0
  14. package/dist/code-push.d.ts +59 -0
  15. package/dist/code-push.js +255 -0
  16. package/dist/database-errors.d.ts +95 -0
  17. package/dist/database-errors.js +173 -0
  18. package/dist/database.d.ts +208 -0
  19. package/dist/database.js +508 -0
  20. package/dist/device-metadata.d.ts +36 -0
  21. package/dist/device-metadata.js +102 -0
  22. package/dist/flags.d.ts +15 -0
  23. package/dist/flags.js +76 -0
  24. package/dist/functions.d.ts +8 -0
  25. package/dist/functions.js +70 -0
  26. package/dist/index.d.ts +45 -0
  27. package/dist/index.js +194 -0
  28. package/dist/logic-engine.d.ts +17 -0
  29. package/dist/logic-engine.js +193 -0
  30. package/dist/messaging.d.ts +20 -0
  31. package/dist/messaging.js +58 -0
  32. package/dist/realtime.d.ts +19 -0
  33. package/dist/realtime.js +148 -0
  34. package/dist/record.d.ts +2 -0
  35. package/dist/record.js +20 -0
  36. package/dist/storage-errors.d.ts +163 -0
  37. package/dist/storage-errors.js +249 -0
  38. package/dist/storage.d.ts +184 -0
  39. package/dist/storage.js +438 -0
  40. package/dist/sync-engine.d.ts +16 -0
  41. package/dist/sync-engine.js +86 -0
  42. package/dist/types.d.ts +470 -0
  43. package/dist/types.js +40 -0
  44. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,1025 @@
1
+ # @koolbase/react-native
2
+
3
+ [![npm](https://img.shields.io/npm/v/@koolbase/react-native.svg)](https://www.npmjs.com/package/@koolbase/react-native)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ React Native SDK for [Koolbase](https://koolbase.com) — Backend as a Service built for mobile developers.
7
+
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
+
10
+ ---
11
+
12
+ ## Get started in 2 minutes
13
+
14
+ 1. Create a free account at [app.koolbase.com](https://app.koolbase.com)
15
+ 2. Create a project and copy your public key from Environments
16
+ 3. Add the SDK:
17
+
18
+ ```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
26
+ ```
27
+
28
+ 4. Initialize at app startup:
29
+
30
+ ```typescript
31
+ import { Koolbase } from '@koolbase/react-native';
32
+
33
+ await Koolbase.initialize({
34
+ publicKey: 'pk_live_xxxx',
35
+ baseUrl: 'https://api.koolbase.com',
36
+ });
37
+ ```
38
+
39
+ That's it. Every feature below is now available via `Koolbase.*`.
40
+
41
+ ---
42
+
43
+ > **Auth is automatic (v3+).** Database, storage, and functions calls
44
+ > 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.
47
+
48
+ ---
49
+
50
+ ## Authentication
51
+
52
+ Email + password, Apple Sign-In, Google Sign-In, and phone + OTP — out of the box.
53
+
54
+ ```typescript
55
+ // Register
56
+ await Koolbase.auth.register({ email: 'user@example.com', password: 'password' });
57
+
58
+ // Login
59
+ const session = await Koolbase.auth.login({ email: 'user@example.com', password: 'password' });
60
+
61
+ // Current user
62
+ const me = Koolbase.auth.currentUser;
63
+
64
+ // Logout
65
+ await Koolbase.auth.logout();
66
+
67
+ // Password reset
68
+ await Koolbase.auth.forgotPassword('user@example.com');
69
+
70
+ // Listen to auth state changes (fires immediately with current state)
71
+ const unsubscribe = Koolbase.auth.onAuthStateChange((user) => {
72
+ console.log(user ? 'signed in' : 'signed out');
73
+ });
74
+ ```
75
+
76
+ ---
77
+
78
+ ### OAuth — Apple
79
+
80
+ Apple Sign-In uses the native authentication flow via `@invertase/react-native-apple-authentication` as a peer dependency:
81
+
82
+ ```typescript
83
+ import appleAuth from '@invertase/react-native-apple-authentication';
84
+ import { Koolbase } from '@koolbase/react-native';
85
+
86
+ const response = await appleAuth.performRequest({
87
+ requestedOperation: appleAuth.Operation.LOGIN,
88
+ requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
89
+ });
90
+
91
+ const session = await Koolbase.auth.signInWithApple({
92
+ identityToken: response.identityToken!,
93
+ nonce: response.nonce,
94
+ fullName: response.fullName
95
+ ? {
96
+ givenName: response.fullName.givenName ?? undefined,
97
+ familyName: response.fullName.familyName ?? undefined,
98
+ }
99
+ : undefined,
100
+ });
101
+ ```
102
+
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).
104
+
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:
110
+
111
+ ```typescript
112
+ import { GoogleSignin } from '@react-native-google-signin/google-signin';
113
+ import { Koolbase } from '@koolbase/react-native';
114
+
115
+ GoogleSignin.configure({
116
+ webClientId: '<your-web-client-id>.apps.googleusercontent.com',
117
+ });
118
+
119
+ const userInfo = await GoogleSignin.signIn();
120
+
121
+ const session = await Koolbase.auth.signInWithGoogle({
122
+ idToken: userInfo.idToken!,
123
+ });
124
+ ```
125
+
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
+ ---
129
+
130
+ ### Phone + OTP
131
+
132
+ ```typescript
133
+ // Send a one-time code
134
+ await Koolbase.auth.sendOtp({ phoneNumber: '+233200000000' });
135
+
136
+ // Verify and sign in
137
+ await Koolbase.auth.verifyOtp({
138
+ phoneNumber: '+233200000000',
139
+ code: '123456',
140
+ });
141
+
142
+ // Or link a phone to an existing account
143
+ await Koolbase.auth.linkPhone({
144
+ phoneNumber: '+233200000000',
145
+ code: '123456',
146
+ });
147
+ ```
148
+
149
+ Configure your SMS provider (Twilio, Africa's Talking, or Hubtel) in the dashboard under Phone Auth.
150
+
151
+ ---
152
+
153
+ ## Database
154
+
155
+ ```typescript
156
+ // Insert
157
+ await Koolbase.db.insert('posts', { title: 'Hello', published: true });
158
+
159
+ // Query
160
+ const { records } = await Koolbase.db.query('posts', {
161
+ filters: { published: true },
162
+ limit: 10,
163
+ orderBy: 'created_at',
164
+ orderDesc: true,
165
+ });
166
+
167
+ // Read fields off a record
168
+ const post = records[0];
169
+ console.log(post.data.title); // your fields live under .data
170
+ console.log(post.id, post.collection); // metadata
171
+
172
+ // Populate related records
173
+ const { records: postsWithAuthor } = await Koolbase.db.query('posts', {
174
+ populate: ['author_id:users'],
175
+ });
176
+
177
+ // Update / Delete
178
+ await Koolbase.db.update('record-id', { title: 'Updated' });
179
+ await Koolbase.db.delete('record-id');
180
+ ```
181
+
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
+ ### Upsert
289
+
290
+ Insert a record, or update the existing one matching a filter.
291
+
292
+ ```ts
293
+ const result = await Koolbase.db.upsert(
294
+ 'profiles',
295
+ { user_id: userId },
296
+ { weightKg: 70 }
297
+ );
298
+
299
+ console.log(result.created); // true if inserted, false if updated
300
+ console.log(result.record.id);
301
+ ```
302
+
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.
305
+
306
+ ### Delete where
307
+
308
+ Bulk-delete every record matching a filter. Returns the number deleted.
309
+
310
+ ```ts
311
+ const deleted = await Koolbase.db.deleteWhere('sessions', {
312
+ user_id: userId,
313
+ status: 'expired',
314
+ });
315
+ ```
316
+
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
+ ---
332
+
333
+ ### Atomic batch writes
334
+
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.
336
+
337
+ ```ts
338
+ import { Koolbase, BatchOp } from '@koolbase/react-native';
339
+
340
+ const results = await Koolbase.db.batch([
341
+ BatchOp.insert('orders', { total: 50, customer_id: customerId }),
342
+ BatchOp.update(inventoryId, { stock: 9 }),
343
+ BatchOp.upsert('counters', {
344
+ match: { name: 'orders' },
345
+ data: { value: 1 },
346
+ }),
347
+ BatchOp.delete(cartItemId),
348
+ ]);
349
+
350
+ // 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 }
354
+ ```
355
+
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
+ ---
359
+
360
+ ### Handling write conflicts
361
+
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):
363
+
364
+ ```ts
365
+ import { KoolbaseConflictError } from '@koolbase/react-native';
366
+
367
+ try {
368
+ await Koolbase.db.insert('users', { email, name });
369
+ } catch (e) {
370
+ if (e instanceof KoolbaseConflictError) {
371
+ showError(`That ${e.field ?? 'value'} is already in use.`);
372
+ } else {
373
+ throw e;
374
+ }
375
+ }
376
+ ```
377
+
378
+ When the device is offline, these writes are queued and synced automatically when connectivity returns.
379
+
380
+ ---
381
+
382
+ ### Semantic, lexical, and hybrid search
383
+
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.
387
+
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).
390
+
391
+ #### The three search modes
392
+
393
+ ```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.
396
+ const result = await Koolbase.db.searchSemantic({
397
+ collection: 'articles',
398
+ field: 'content_embedding',
399
+ queryText: 'how do I move quicker?',
400
+ limit: 10,
401
+ });
402
+
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({
406
+ collection: 'articles',
407
+ field: 'content_embedding',
408
+ queryText: 'CVE-2024-1234',
409
+ mode: 'lexical',
410
+ limit: 10,
411
+ });
412
+
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({
417
+ collection: 'articles',
418
+ field: 'content_embedding',
419
+ queryText: 'production deploy pipeline',
420
+ mode: 'hybrid',
421
+ limit: 10,
422
+ });
423
+ ```
424
+
425
+ #### Filtering weak matches
426
+
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:
430
+
431
+ ```typescript
432
+ const result = await Koolbase.db.searchSemantic({
433
+ collection: 'articles',
434
+ field: 'content_embedding',
435
+ queryText: 'how do I move quicker?',
436
+ mode: 'hybrid',
437
+ minSimilarity: 70, // only matches at least 70% similar
438
+ limit: 10,
439
+ });
440
+ ```
441
+
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
+
446
+ #### Server-side embedding (recommended)
447
+
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:
453
+
454
+ ```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
+ },
463
+ });
464
+
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:
471
+ await Koolbase.db.embedText({
472
+ collection: 'articles',
473
+ recordId: article.$id,
474
+ vectorField: 'content_embedding',
475
+ });
476
+
477
+ // Or override the source — useful for combining fields:
478
+ await Koolbase.db.embedText({
479
+ collection: 'articles',
480
+ recordId: article.$id,
481
+ vectorField: 'content_embedding',
482
+ text: `${article.title}\n\n${article.summary}`,
483
+ });
484
+ ```
485
+
486
+ #### Client-side embedding (advanced)
487
+
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":
491
+
492
+ ```typescript
493
+ // Set a vector you've encoded yourself
494
+ await Koolbase.db.setVector(
495
+ articleId,
496
+ 'embedding',
497
+ await myEmbeddingModel.encode(article.content),
498
+ );
499
+
500
+ // Read it back
501
+ const v = await Koolbase.db.getVector(articleId, 'embedding');
502
+ console.log(`${v.vector.length}-dim, updated ${v.updatedAt}`);
503
+
504
+ // Search with a precomputed vector — semantic mode only.
505
+ const result = await Koolbase.db.searchSemantic({
506
+ collection: 'articles',
507
+ field: 'embedding',
508
+ queryVector: await myEmbeddingModel.encode(userQuery),
509
+ limit: 10,
510
+ where: { category: 'tech' },
511
+ });
512
+
513
+ // Remove a record's vector when no longer needed
514
+ await Koolbase.db.deleteVector(articleId, 'embedding');
515
+ ```
516
+
517
+ #### Behaviors worth knowing
518
+
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
525
+ `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.
543
+
544
+ ---
545
+
546
+ ## Storage
547
+
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.
552
+
553
+ ```typescript
554
+ // Upload — rejects if `user-${userId}.jpg` already exists
555
+ const { object, downloadUrl } = await Koolbase.storage.upload({
556
+ bucket: 'avatars',
557
+ path: `user-${userId}.jpg`,
558
+ file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
559
+ });
560
+
561
+ // Upload — silently replaces any existing object at this path
562
+ await Koolbase.storage.upload({
563
+ bucket: 'avatars',
564
+ path: `user-${userId}.jpg`,
565
+ file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
566
+ overwrite: true,
567
+ });
568
+
569
+ // Get download URL
570
+ const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg`);
571
+
572
+ // Delete
573
+ await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);
574
+ ```
575
+
576
+ ---
577
+
578
+ ### Handling upload conflicts
579
+
580
+ For user-supplied filenames, prompt the user before overwriting:
581
+
582
+ ```typescript
583
+ import { KoolbaseStorageConflictError } from '@koolbase/react-native';
584
+
585
+ try {
586
+ await Koolbase.storage.upload({
587
+ bucket: 'documents',
588
+ path: filename,
589
+ file: { uri, name: filename, type: mimeType },
590
+ });
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
+ }
605
+ }
606
+ ```
607
+
608
+ See [Error handling](#error-handling) for the full set of storage errors.
609
+
610
+ ---
611
+
612
+ ### Handling bucket limits
613
+
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:
618
+
619
+ ````typescript
620
+ import {
621
+ KoolbaseStorageQuotaError,
622
+ KoolbaseStorageFileTooLargeError,
623
+ KoolbaseStorageMimeTypeError,
624
+ } from '@koolbase/react-native';
625
+
626
+ try {
627
+ await Koolbase.storage.upload({
628
+ bucket: 'user-photos',
629
+ path: filename,
630
+ file: { uri, name: filename, type: mimeType },
631
+ });
632
+ } catch (e) {
633
+ if (e instanceof KoolbaseStorageMimeTypeError) {
634
+ showError('That file type is not allowed in this bucket.');
635
+ } else if (e instanceof KoolbaseStorageFileTooLargeError) {
636
+ showError('That file is too big — pick a smaller one.');
637
+ } else if (e instanceof KoolbaseStorageQuotaError) {
638
+ showError('This bucket is full — delete some files and try again.');
639
+ } else {
640
+ throw e;
641
+ }
642
+ }
643
+ ````
644
+
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.
649
+
650
+ ---
651
+
652
+ ### Object versioning
653
+
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.
657
+
658
+ ```typescript
659
+ // List all versions of a path, newest first
660
+ const versions = await Koolbase.storage.listVersions('documents', 'contract.pdf');
661
+
662
+ for (const v of versions) {
663
+ console.log(`${v.versionId}: size=${v.size} isCurrent=${v.isCurrent}`);
664
+ }
665
+
666
+ // Download a specific historical version
667
+ const url = await Koolbase.storage.getDownloadUrl(
668
+ 'documents',
669
+ 'contract.pdf',
670
+ '019e98ed-eed6-7e71-...',
671
+ );
672
+
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
+ );
680
+
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
+ );
687
+
688
+ // Wipe the entire timeline for a path - every version, every R2 key
689
+ await Koolbase.storage.delete('documents', 'contract.pdf', true);
690
+ ```
691
+
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.
707
+
708
+ ---
709
+
710
+ ## Realtime
711
+
712
+ 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`.
715
+
716
+ ```ts
717
+ const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
718
+ // event.type -> 'created' | 'updated' | 'deleted'
719
+ if (event.type === 'deleted') {
720
+ console.log('deleted', event.recordId); // recordId on deletes
721
+ } else {
722
+ console.log(event.type, event.record!.data); // record on created/updated
723
+ }
724
+ });
725
+
726
+ unsubscribe();
727
+ ```
728
+
729
+ The socket opens lazily, is shared, and reconnects automatically. The project is
730
+ taken from the user's session.
731
+
732
+ ---
733
+
734
+ ## Functions
735
+
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.
737
+
738
+ ```typescript
739
+ // Invoke a deployed function
740
+ const result = await Koolbase.functions.invoke('send-welcome-email', {
741
+ userId: '123',
742
+ });
743
+ if (result.success) console.log(result.data);
744
+ ```
745
+
746
+ Inside the function, read the caller:
747
+
748
+ ```typescript
749
+ export async function handler(ctx) {
750
+ const userId = ctx.auth?.user_id;
751
+ if (!userId) {
752
+ return { error: { code: 'AUTH_REQUIRED' }, status: 401 };
753
+ }
754
+ // Authenticated logic here
755
+ return { ok: true };
756
+ }
757
+ ```
758
+
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).
760
+
761
+ ---
762
+
763
+ ## Feature Flags & Remote Config
764
+
765
+ ```typescript
766
+ if (Koolbase.isEnabled('new_checkout')) { /* ... */ }
767
+
768
+ 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);
771
+ ```
772
+
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
+ ```
783
+
784
+ ---
785
+
786
+ ## Code Push
787
+
788
+ Push config overrides, feature flag overrides, and directive-driven behaviour without a store release.
789
+
790
+ ```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
+ });
842
+
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
+ }
848
+ }
849
+ ```
850
+
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
+ ---
856
+
857
+ ## Analytics
858
+
859
+ Track screen views, custom events, and user behaviour. View DAU, WAU, MAU, funnels, and retention in the Koolbase dashboard.
860
+
861
+ ```typescript
862
+ await Koolbase.initialize({
863
+ publicKey: 'pk_live_xxxx',
864
+ baseUrl: 'https://api.koolbase.com',
865
+ analyticsEnabled: true,
866
+ appVersion: '1.0.0',
867
+ });
868
+
869
+ // Custom events
870
+ Koolbase.analytics.track('purchase', { value: 1200, currency: 'GHS' });
871
+
872
+ // Screen views
873
+ Koolbase.analytics.screenView('checkout');
874
+
875
+ // User identity
876
+ Koolbase.analytics.identify(user.id);
877
+ Koolbase.analytics.setUserProperty('plan', 'pro');
878
+
879
+ // On logout
880
+ Koolbase.analytics.reset();
881
+ ```
882
+
883
+ ---
884
+
885
+ ## Cloud Messaging
886
+
887
+ ```typescript
888
+ await Koolbase.initialize({
889
+ publicKey: 'pk_live_xxxx',
890
+ baseUrl: 'https://api.koolbase.com',
891
+ messagingEnabled: true,
892
+ });
893
+
894
+ // Register FCM token (after obtaining from @react-native-firebase/messaging)
895
+ const fcmToken = await messaging().getToken();
896
+ await Koolbase.messaging.registerToken({
897
+ token: fcmToken,
898
+ platform: 'android', // or 'ios'
899
+ });
900
+
901
+ // Send to a specific device
902
+ await Koolbase.messaging.send({
903
+ to: deviceToken,
904
+ title: 'Your order is ready',
905
+ body: 'Pick up at counter 3',
906
+ data: { order_id: '123' },
907
+ });
908
+ ```
909
+
910
+ ---
911
+
912
+ ## Error handling
913
+
914
+ Koolbase throws typed errors selected from the server's stable error `code`, so
915
+ handling doesn't depend on message text.
916
+
917
+ ### Database errors
918
+
919
+ All data-layer failures extend `KoolbaseDataError` (which extends `Error`):
920
+
921
+ | Error | When |
922
+ |---|---|
923
+ | `KoolbaseConflictError` | A write violates a unique constraint (409). Exposes `.field` — the field that collided, when the server reports it. |
924
+ | `KoolbaseNotFoundError` | The record or collection doesn't exist (404). |
925
+ | `KoolbaseValidationError` | The request was rejected as invalid (400). |
926
+ | `KoolbasePermissionError` | An access rule denied the operation (403). |
927
+ | `KoolbaseRateLimitError` | The caller is being rate-limited (429). |
928
+ | `KoolbaseVectorDimensionMismatchError` | A vector's length doesn't match the field's declared dimension (400, code `vector_dimension_mismatch`). |
929
+
930
+ ```ts
931
+ import { KoolbaseConflictError, KoolbaseDataError } from '@koolbase/react-native';
932
+
933
+ try {
934
+ await Koolbase.db.upsert('users', { email }, { name });
935
+ } catch (e) {
936
+ if (e instanceof KoolbaseConflictError) {
937
+ showError(`That ${e.field ?? 'value'} is already taken.`);
938
+ } else if (e instanceof KoolbaseDataError) {
939
+ showError(e.message);
940
+ }
941
+ }
942
+ ```
943
+
944
+ > `query`, `get`, `upsert`, and `deleteWhere` throw these typed errors. `insert`,
945
+ > `update`, and `delete` are optimistic/offline-first — they queue and sync in
946
+ > the background, so their conflicts surface via the sync engine, not as a
947
+ > thrown error.
948
+
949
+ ---
950
+
951
+ ### Storage errors
952
+
953
+ All storage failures extend `KoolbaseStorageError` (which extends `Error`):
954
+
955
+ | Error | When |
956
+ |---|---|
957
+ | `KoolbaseStorageConflictError` | An upload targets a path that's already taken and `overwrite: false` (409, code `PATH_CONFLICT`). Exposes `.path` — the colliding path. |
958
+ | `KoolbaseStorageNotFoundError` | The bucket or object doesn't exist (404). |
959
+ | `KoolbaseStorageValidationError` | The request was rejected as invalid — bad path, missing field (400). |
960
+ | `KoolbaseStoragePermissionError` | The caller is not allowed to perform the operation (403). |
961
+
962
+ ```ts
963
+ import {
964
+ KoolbaseStorageConflictError,
965
+ KoolbaseStorageError,
966
+ KoolbaseStoragePermissionError,
967
+ } from '@koolbase/react-native';
968
+
969
+ try {
970
+ await Koolbase.storage.upload({
971
+ bucket: 'avatars',
972
+ path: 'me.png',
973
+ file: { uri, name: 'me.png', type: 'image/png' },
974
+ });
975
+ } catch (e) {
976
+ if (e instanceof KoolbaseStorageConflictError) {
977
+ // Already exists — prompt user to confirm overwrite
978
+ promptOverwrite(e.path);
979
+ } else if (e instanceof KoolbaseStoragePermissionError) {
980
+ showError('You do not have permission to upload here.');
981
+ } else if (e instanceof KoolbaseStorageError) {
982
+ // Catch-all for any other storage error
983
+ showError(e.message);
984
+ } else {
985
+ throw e;
986
+ }
987
+ }
988
+ ```
989
+
990
+ ---
991
+
992
+ ## What's included
993
+
994
+ - Authentication: email + password, Apple Sign-In, Google Sign-In, phone + OTP
995
+ - Database with offline-first cache, realtime subscriptions, populate for related records, semantic search over vectors
996
+ - Storage with presigned uploads and downloads, safe-by-default conflict handling, image transforms, object versioning (history + restore + soft-delete)
997
+ - Realtime subscriptions over WebSocket
998
+ - Authenticated functions (`ctx.auth` exposes the caller automatically)
999
+ - Feature flags and remote config
1000
+ - Version enforcement
1001
+ - Code push (config + flag overrides + directives, no store release)
1002
+ - Logic engine (conditional flows as data, updatable OTA)
1003
+ - Analytics (DAU/WAU/MAU, funnels, retention)
1004
+ - Cloud Messaging (FCM token registration, targeted send, broadcast)
1005
+ - TypeScript-native with full type definitions
1006
+
1007
+ ---
1008
+
1009
+ ## Documentation
1010
+
1011
+ Full documentation at [docs.koolbase.com](https://docs.koolbase.com)
1012
+
1013
+ ## Dashboard
1014
+
1015
+ Manage your projects at [app.koolbase.com](https://app.koolbase.com)
1016
+
1017
+ ## Support
1018
+
1019
+ - [GitHub Issues](https://github.com/kennedyowusu/koolbase-react-native/issues)
1020
+ - [docs.koolbase.com](https://docs.koolbase.com)
1021
+ - Email: <hello@koolbase.com>
1022
+
1023
+ ## License
1024
+
1025
+ MIT