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