@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.
- package/CHANGELOG.md +1342 -0
- package/README.md +403 -568
- package/dist/{auth-storage.d.ts → cjs/auth-storage.d.ts} +1 -1
- package/dist/cjs/index.d.ts +19 -0
- package/dist/cjs/index.js +125 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/platform.d.ts +2 -0
- package/dist/cjs/platform.js +43 -0
- package/dist/esm/auth-storage.d.ts +26 -0
- package/dist/esm/auth-storage.js +100 -0
- package/dist/esm/index.d.ts +19 -0
- package/dist/esm/index.js +106 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/platform.d.ts +2 -0
- package/dist/esm/platform.js +37 -0
- package/package.json +30 -30
- package/dist/analytics.d.ts +0 -24
- package/dist/analytics.js +0 -114
- package/dist/apple-auth.d.ts +0 -22
- package/dist/apple-auth.js +0 -74
- package/dist/auth-errors.d.ts +0 -117
- package/dist/auth-errors.js +0 -250
- package/dist/auth.d.ts +0 -213
- package/dist/auth.js +0 -810
- package/dist/cache-store.d.ts +0 -50
- package/dist/cache-store.js +0 -197
- package/dist/code-push.d.ts +0 -59
- package/dist/code-push.js +0 -255
- package/dist/conflict.d.ts +0 -80
- package/dist/conflict.js +0 -84
- package/dist/database-errors.d.ts +0 -101
- package/dist/database-errors.js +0 -200
- package/dist/database.d.ts +0 -298
- package/dist/database.js +0 -852
- package/dist/device-id.d.ts +0 -1
- package/dist/device-id.js +0 -60
- package/dist/device-metadata.d.ts +0 -36
- package/dist/device-metadata.js +0 -102
- package/dist/errors.d.ts +0 -64
- package/dist/errors.js +0 -85
- package/dist/flags.d.ts +0 -15
- package/dist/flags.js +0 -76
- package/dist/function-errors.d.ts +0 -51
- package/dist/function-errors.js +0 -103
- package/dist/functions.d.ts +0 -15
- package/dist/functions.js +0 -83
- package/dist/index.d.ts +0 -49
- package/dist/index.js +0 -204
- package/dist/logic-engine.d.ts +0 -17
- package/dist/logic-engine.js +0 -193
- package/dist/messaging.d.ts +0 -13
- package/dist/messaging.js +0 -36
- package/dist/offline-state.d.ts +0 -97
- package/dist/offline-state.js +0 -200
- package/dist/pending-write.d.ts +0 -47
- package/dist/pending-write.js +0 -22
- package/dist/realtime.d.ts +0 -44
- package/dist/realtime.js +0 -195
- package/dist/record.d.ts +0 -2
- package/dist/record.js +0 -23
- package/dist/storage-errors.d.ts +0 -163
- package/dist/storage-errors.js +0 -253
- package/dist/storage.d.ts +0 -198
- package/dist/storage.js +0 -451
- package/dist/sync-engine.d.ts +0 -30
- package/dist/sync-engine.js +0 -290
- package/dist/types.d.ts +0 -487
- package/dist/types.js +0 -40
- /package/dist/{auth-storage.js → cjs/auth-storage.js} +0 -0
package/README.md
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@koolbase/react-native)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
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,
|
|
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.
|
|
19
|
+
3. Install the SDK and its peer dependencies:
|
|
17
20
|
|
|
18
21
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
33
|
+
import { Koolbase } from '@koolbase/react-native';
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
53
|
-
> identity. `owner
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
180
|
-
const { records:
|
|
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
|
-
|
|
204
|
+
Insert a record, or update the existing one matching a filter.
|
|
192
205
|
|
|
193
|
-
|
|
206
|
+
```typescript
|
|
207
|
+
const result = await Koolbase.db.upsert(
|
|
208
|
+
'profiles',
|
|
209
|
+
{ user_id: userId },
|
|
210
|
+
{ weightKg: 70 },
|
|
211
|
+
);
|
|
194
212
|
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
###
|
|
220
|
+
### Delete where
|
|
208
221
|
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
+
### Atomic batch writes
|
|
251
236
|
|
|
252
|
-
|
|
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
|
-
|
|
268
|
-
|
|
239
|
+
```typescript
|
|
240
|
+
import { Koolbase, BatchOp } from '@koolbase/react-native';
|
|
269
241
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
//
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
261
|
+
`insert`, `update`, and `upsert` are online-first: when the server is reachable
|
|
262
|
+
they throw on rejection.
|
|
314
263
|
|
|
315
|
-
|
|
264
|
+
```typescript
|
|
265
|
+
import { KoolbaseConflictError } from '@koolbase/react-native';
|
|
316
266
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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();
|
|
392
|
-
await c.resolveWithServer();
|
|
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();
|
|
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
|
-
>
|
|
410
|
-
>
|
|
411
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
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) —
|
|
482
|
-
//
|
|
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 —
|
|
491
|
-
//
|
|
492
|
-
|
|
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 —
|
|
501
|
-
//
|
|
502
|
-
|
|
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
|
-
|
|
429
|
+
### Filtering weak matches
|
|
513
430
|
|
|
514
|
-
For `
|
|
515
|
-
|
|
516
|
-
on weak matches:
|
|
431
|
+
For `semantic` and `hybrid`, `minSimilarity` (0–100) drops results below a
|
|
432
|
+
threshold server-side:
|
|
517
433
|
|
|
518
434
|
```typescript
|
|
519
|
-
|
|
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,
|
|
440
|
+
minSimilarity: 70,
|
|
525
441
|
limit: 10,
|
|
526
442
|
});
|
|
527
443
|
```
|
|
528
444
|
|
|
529
|
-
`minSimilarity`
|
|
530
|
-
|
|
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
|
-
|
|
448
|
+
### Server-side embedding (recommended)
|
|
534
449
|
|
|
535
|
-
Configure an AI provider on the project once (Gemini's free tier works;
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
//
|
|
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
|
|
465
|
+
recordId: article.id,
|
|
561
466
|
vectorField: 'content_embedding',
|
|
562
467
|
});
|
|
563
468
|
|
|
564
|
-
// Or override the source
|
|
469
|
+
// Or override the source, to combine fields
|
|
565
470
|
await Koolbase.db.embedText({
|
|
566
471
|
collection: 'articles',
|
|
567
|
-
recordId: article
|
|
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
|
-
|
|
478
|
+
### Client-side embedding
|
|
574
479
|
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
504
|
+
### Behaviours worth knowing
|
|
605
505
|
|
|
606
|
-
- **Pass exactly one of `queryVector` or `queryText`.**
|
|
607
|
-
|
|
608
|
-
-
|
|
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
|
-
-
|
|
614
|
-
|
|
615
|
-
- **
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
//
|
|
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
|
|
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)
|
|
679
|
-
if (e instanceof KoolbaseStorageConflictError) {
|
|
680
|
-
const ok = await confirm(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
742
|
-
|
|
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
|
|
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
|
|
761
|
-
|
|
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
|
|
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
|
|
704
|
+
// Wipe the whole timeline for a path
|
|
776
705
|
await Koolbase.storage.delete('documents', 'contract.pdf', true);
|
|
777
706
|
```
|
|
778
707
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
- **
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
801
|
-
collections whose read rule is `public` or `authenticated`.
|
|
722
|
+
subscribe after sign-in.
|
|
802
723
|
|
|
803
|
-
```
|
|
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);
|
|
727
|
+
console.log('deleted', event.recordId); // recordId on deletes
|
|
808
728
|
} else {
|
|
809
|
-
console.log(event.type, event.record!.data);
|
|
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
|
|
817
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
857
|
-
const dark
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
931
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
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) —
|
|
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.
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1025
|
-
anything the SDK raises.
|
|
875
|
+
### Database
|
|
1026
876
|
|
|
1027
877
|
| Error | When |
|
|
1028
878
|
|---|---|
|
|
1029
|
-
| `KoolbaseConflictError` | A write violates a unique constraint (409).
|
|
1030
|
-
| `KoolbaseNotFoundError` | The record or collection
|
|
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
|
|
884
|
+
| `KoolbaseVectorDimensionMismatchError` | A vector's length does not match the field's declared dimension. |
|
|
1035
885
|
|
|
1036
|
-
```
|
|
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
|
-
//
|
|
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
|
|
1059
|
-
>
|
|
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` |
|
|
1077
|
-
| `KoolbaseStorageNotFoundError` | The bucket or object
|
|
1078
|
-
| `KoolbaseStorageValidationError` |
|
|
1079
|
-
| `KoolbaseStoragePermissionError` | The caller
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
|
1114
|
-
|
|
1115
|
-
-
|
|
1116
|
-
|
|
1117
|
-
-
|
|
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
|
-
-
|
|
1121
|
-
-
|
|
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/
|
|
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
|
|