hydrousdb 1.1.0 → 2.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/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  - [Features](#features)
15
15
  - [Installation](#installation)
16
+ - [Key Concepts](#key-concepts)
16
17
  - [Quick Start](#quick-start)
17
18
  - [Framework Guides](#framework-guides)
18
19
  - [Next.js (App Router)](#nextjs-app-router)
@@ -28,6 +29,7 @@
28
29
  - [Error Handling](#error-handling)
29
30
  - [Pagination](#pagination)
30
31
  - [TypeScript](#typescript)
32
+ - [Migration from v1](#migration-from-v1)
31
33
  - [Contributing](#contributing)
32
34
  - [License](#license)
33
35
 
@@ -36,6 +38,7 @@
36
38
  ## Features
37
39
 
38
40
  - ✅ **Full TypeScript** — every method and response is fully typed
41
+ - ✅ **Multi-bucket** — one client, any number of buckets; pass `bucketKey` per call
39
42
  - ✅ **Modular** — import only what you need (`hydrousdb/records`, `hydrousdb/auth`, `hydrousdb/analytics`)
40
43
  - ✅ **Tree-shakeable** — ships ESM + CJS with dual exports
41
44
  - ✅ **Auto-retry** — retries on transient 5xx / network errors with linear back-off
@@ -59,34 +62,66 @@ yarn add hydrousdb
59
62
 
60
63
  ---
61
64
 
65
+ ## Key Concepts
66
+
67
+ ### Two keys, one client
68
+
69
+ The SDK separates two distinct credentials:
70
+
71
+ | Key | Config field | Used for |
72
+ |-----|-------------|----------|
73
+ | Auth key | `authKey` | All `/auth` routes — sign up, sign in, session management |
74
+ | Security key | `securityKey` | All records and analytics routes — data access credential |
75
+
76
+ ### Bucket per call
77
+
78
+ Unlike v1, `bucketKey` is **not** set on the client — it's passed as an option on every records and analytics call. This means a **single client instance can read and write to any number of buckets** without re-initialising.
79
+
80
+ ```ts
81
+ const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
82
+
83
+ // Same client, different buckets
84
+ await db.records.insert({ values: { name: 'Alice' } }, { bucketKey: 'users' });
85
+ await db.records.insert({ values: { order: '#1001' } }, { bucketKey: 'orders' });
86
+ await db.analytics.count({ bucketKey: 'users' });
87
+ await db.analytics.count({ bucketKey: 'orders' });
88
+ ```
89
+
90
+ ---
91
+
62
92
  ## Quick Start
63
93
 
64
94
  ```ts
65
95
  import { createClient } from 'hydrousdb';
66
96
 
67
97
  const db = createClient({
68
- apiKey: 'your-api-key',
69
- bucketKey: 'your-bucket-key',
98
+ authKey: 'your-auth-key',
99
+ securityKey: 'your-security-key',
70
100
  });
71
101
 
72
- // Insert a record
73
- const { data, meta } = await db.records.insert({
74
- values: { name: 'Alice', score: 99 },
75
- queryableFields: ['name'],
76
- });
102
+ // Insert a record into the 'users' bucket
103
+ const { data, meta } = await db.records.insert(
104
+ { values: { name: 'Alice', score: 99 }, queryableFields: ['name'] },
105
+ { bucketKey: 'users' }
106
+ );
77
107
  console.log('Created record:', meta.id);
78
108
 
79
- // Query records
109
+ // Query records from the 'users' bucket
80
110
  const { data: records } = await db.records.query({
111
+ bucketKey: 'users',
81
112
  filters: [{ field: 'score', op: '>=', value: 90 }],
82
113
  limit: 20,
83
114
  });
84
115
 
85
- // Auth — sign a user in
116
+ // Auth — no bucketKey needed
86
117
  const { data: user, session } = await db.auth.signIn({
87
118
  email: 'alice@example.com',
88
119
  password: 'Str0ng@Pass!',
89
120
  });
121
+
122
+ // Analytics
123
+ const { data: stats } = await db.analytics.count({ bucketKey: 'users' });
124
+ console.log(stats.count);
90
125
  ```
91
126
 
92
127
  ---
@@ -101,10 +136,9 @@ const { data: user, session } = await db.auth.signIn({
101
136
  // lib/db.ts
102
137
  import { createClient } from 'hydrousdb';
103
138
 
104
- // The client is re-used across all server components in the same process
105
139
  export const db = createClient({
106
- apiKey: process.env.HYDROUS_API_KEY!,
107
- bucketKey: process.env.HYDROUS_BUCKET_KEY!,
140
+ authKey: process.env.HYDROUS_AUTH_KEY!,
141
+ securityKey: process.env.HYDROUS_SECURITY_KEY!,
108
142
  });
109
143
  ```
110
144
 
@@ -116,6 +150,7 @@ import { db } from '@/lib/db';
116
150
 
117
151
  export default async function ProductsPage() {
118
152
  const { data: products } = await db.records.query({
153
+ bucketKey: 'products',
119
154
  filters: [{ field: 'category', op: '==', value: 'shoes' }],
120
155
  limit: 24,
121
156
  sortOrder: 'desc',
@@ -142,10 +177,10 @@ import { HydrousError } from 'hydrousdb';
142
177
  export async function POST(req: NextRequest) {
143
178
  try {
144
179
  const body = await req.json();
145
- const result = await db.records.insert({
146
- values: body,
147
- queryableFields: ['email', 'name'],
148
- });
180
+ const result = await db.records.insert(
181
+ { values: body, queryableFields: ['email', 'name'] },
182
+ { bucketKey: 'users' }
183
+ );
149
184
  return NextResponse.json(result, { status: 201 });
150
185
  } catch (err) {
151
186
  if (err instanceof HydrousError) {
@@ -182,8 +217,8 @@ export const config = { matcher: ['/dashboard/:path*'] };
182
217
 
183
218
  ```env
184
219
  # .env.local
185
- HYDROUS_API_KEY=your-api-key
186
- HYDROUS_BUCKET_KEY=your-bucket-key
220
+ HYDROUS_AUTH_KEY=your-auth-key
221
+ HYDROUS_SECURITY_KEY=your-security-key
187
222
  ```
188
223
 
189
224
  ---
@@ -196,14 +231,14 @@ import type { NextApiRequest, NextApiResponse } from 'next';
196
231
  import { createClient, HydrousError } from 'hydrousdb';
197
232
 
198
233
  const db = createClient({
199
- apiKey: process.env.HYDROUS_API_KEY!,
200
- bucketKey: process.env.HYDROUS_BUCKET_KEY!,
234
+ authKey: process.env.HYDROUS_AUTH_KEY!,
235
+ securityKey: process.env.HYDROUS_SECURITY_KEY!,
201
236
  });
202
237
 
203
238
  export default async function handler(req: NextApiRequest, res: NextApiResponse) {
204
239
  if (req.method === 'GET') {
205
240
  try {
206
- const result = await db.records.query({ limit: 50 });
241
+ const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
207
242
  return res.status(200).json(result);
208
243
  } catch (err) {
209
244
  if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
@@ -229,8 +264,8 @@ export default defineNuxtPlugin(() => {
229
264
  const config = useRuntimeConfig();
230
265
 
231
266
  const db = createClient({
232
- apiKey: config.hydrousApiKey as string,
233
- bucketKey: config.hydrousBucketKey as string,
267
+ authKey: config.hydrousAuthKey as string,
268
+ securityKey: config.hydrousSecurityKey as string,
234
269
  });
235
270
 
236
271
  return { provide: { db } };
@@ -241,8 +276,8 @@ export default defineNuxtPlugin(() => {
241
276
  // nuxt.config.ts
242
277
  export default defineNuxtConfig({
243
278
  runtimeConfig: {
244
- hydrousApiKey: '', // Set via NUXT_HYDROUS_API_KEY env var
245
- hydrousBucketKey: '', // Set via NUXT_HYDROUS_BUCKET_KEY env var
279
+ hydrousAuthKey: '', // NUXT_HYDROUS_AUTH_KEY
280
+ hydrousSecurityKey: '', // NUXT_HYDROUS_SECURITY_KEY
246
281
  },
247
282
  });
248
283
  ```
@@ -254,7 +289,7 @@ export default defineNuxtConfig({
254
289
  const { $db } = useNuxtApp();
255
290
 
256
291
  const { data: records, pending } = await useAsyncData('records', () =>
257
- $db.records.query({ limit: 20 })
292
+ $db.records.query({ bucketKey: 'products', limit: 20 })
258
293
  );
259
294
  </script>
260
295
 
@@ -273,8 +308,8 @@ const { data: records, pending } = await useAsyncData('records', () =>
273
308
  import { createClient } from 'hydrousdb';
274
309
 
275
310
  const db = createClient({
276
- apiKey: import.meta.env.VITE_HYDROUS_API_KEY,
277
- bucketKey: import.meta.env.VITE_HYDROUS_BUCKET_KEY,
311
+ authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
312
+ securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
278
313
  });
279
314
 
280
315
  export function useHydrous() {
@@ -292,11 +327,11 @@ import { useEffect, useState } from 'react';
292
327
  import { createClient, HydrousRecord, HydrousError } from 'hydrousdb';
293
328
 
294
329
  const db = createClient({
295
- apiKey: import.meta.env.VITE_HYDROUS_API_KEY,
296
- bucketKey: import.meta.env.VITE_HYDROUS_BUCKET_KEY,
330
+ authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
331
+ securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
297
332
  });
298
333
 
299
- export function useRecords(limit = 20) {
334
+ export function useRecords(bucketKey: string, limit = 20) {
300
335
  const [records, setRecords] = useState<HydrousRecord[]>([]);
301
336
  const [loading, setLoading] = useState(true);
302
337
  const [error, setError] = useState<string | null>(null);
@@ -304,13 +339,13 @@ export function useRecords(limit = 20) {
304
339
  useEffect(() => {
305
340
  const controller = new AbortController();
306
341
 
307
- db.records.query({ limit }, { signal: controller.signal })
342
+ db.records.query({ bucketKey, limit }, { signal: controller.signal })
308
343
  .then(r => setRecords(r.data))
309
344
  .catch(e => { if (!(e instanceof DOMException)) setError(String(e)); })
310
345
  .finally(() => setLoading(false));
311
346
 
312
347
  return () => controller.abort();
313
- }, [limit]);
348
+ }, [bucketKey, limit]);
314
349
 
315
350
  return { records, loading, error };
316
351
  }
@@ -326,15 +361,15 @@ import express from 'express';
326
361
  import { createClient, HydrousError } from 'hydrousdb';
327
362
 
328
363
  const db = createClient({
329
- apiKey: process.env.HYDROUS_API_KEY!,
330
- bucketKey: process.env.HYDROUS_BUCKET_KEY!,
364
+ authKey: process.env.HYDROUS_AUTH_KEY!,
365
+ securityKey: process.env.HYDROUS_SECURITY_KEY!,
331
366
  });
332
367
  const app = express();
333
368
  app.use(express.json());
334
369
 
335
- app.get('/records', async (req, res) => {
370
+ app.get('/orders', async (req, res) => {
336
371
  try {
337
- const result = await db.records.query({ limit: 50 });
372
+ const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
338
373
  res.json(result);
339
374
  } catch (err) {
340
375
  if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
@@ -355,89 +390,101 @@ app.listen(3000, () => console.log('Listening on :3000'));
355
390
  import { createClient } from 'hydrousdb';
356
391
 
357
392
  const db = createClient({
358
- apiKey: string, // required — your HydrousDB API key
359
- bucketKey: string, // required — your bucket identifier
360
- baseUrl?: string, // override API base URL
361
- timeout?: number, // request timeout in ms (default: 30_000)
362
- retries?: number, // retries on network/5xx errors (default: 2)
393
+ authKey: string, // required — your HydrousDB auth key (for /auth routes)
394
+ securityKey: string, // required — your HydrousDB security key (for records & analytics)
395
+ baseUrl?: string, // override API base URL
396
+ timeout?: number, // request timeout in ms (default: 30_000)
397
+ retries?: number, // retries on network/5xx errors (default: 2)
363
398
  });
364
399
  ```
365
400
 
401
+ > **Note:** `bucketKey` is no longer part of the client config. It is passed as an option on each individual records and analytics call.
402
+
366
403
  ---
367
404
 
368
405
  ### Records
369
406
 
370
- All methods live on `db.records`.
407
+ All methods live on `db.records`. Every method requires a `bucketKey` option.
371
408
 
372
409
  | Method | Description |
373
410
  |---|---|
374
- | `get(recordId, opts?)` | Fetch a single record |
375
- | `getSnapshot(recordId, generation, opts?)` | Fetch a historical version |
376
- | `query(opts?)` | Query a collection with filters / pagination |
377
- | `queryAll(opts?)` | Fetch all pages automatically |
378
- | `insert(payload, opts?)` | Create a new record |
379
- | `update(payload, opts?)` | Update an existing record |
380
- | `delete(recordId, opts?)` | Delete a record |
381
- | `exists(recordId, opts?)` | HEAD check — returns metadata or `null` |
382
- | `batchInsert(payload, opts?)` | Insert up to 500 records |
383
- | `batchUpdate(payload, opts?)` | Update up to 500 records |
384
- | `batchDelete(payload, opts?)` | Delete up to 500 records |
385
-
386
- #### `db.records.get(recordId, options?)`
411
+ | `get(recordId, { bucketKey, ...opts })` | Fetch a single record |
412
+ | `getSnapshot(recordId, generation, { bucketKey, ...opts })` | Fetch a historical version |
413
+ | `query({ bucketKey, ...opts })` | Query a collection with filters / pagination |
414
+ | `queryAll({ bucketKey, ...opts })` | Fetch all pages automatically |
415
+ | `insert(payload, { bucketKey, ...opts })` | Create a new record |
416
+ | `update(payload, { bucketKey, ...opts })` | Update an existing record |
417
+ | `delete(recordId, { bucketKey, ...opts })` | Delete a record |
418
+ | `exists(recordId, { bucketKey, ...opts })` | HEAD check — returns metadata or `null` |
419
+ | `batchInsert(payload, { bucketKey, ...opts })` | Insert up to 500 records |
420
+ | `batchUpdate(payload, { bucketKey, ...opts })` | Update up to 500 records |
421
+ | `batchDelete(payload, { bucketKey, ...opts })` | Delete up to 500 records |
422
+
423
+ #### `db.records.get(recordId, options)`
387
424
 
388
425
  ```ts
389
- const { data, history } = await db.records.get('rec_abc', { showHistory: true });
426
+ const { data, history } = await db.records.get('rec_abc', {
427
+ bucketKey: 'users',
428
+ showHistory: true,
429
+ });
390
430
  ```
391
431
 
392
- #### `db.records.query(options?)`
432
+ #### `db.records.query(options)`
393
433
 
394
434
  ```ts
395
435
  const { data, meta } = await db.records.query({
396
- filters: [{ field: 'status', op: '==', value: 'active' }],
397
- timeScope: '30d',
398
- sortOrder: 'desc',
399
- limit: 50,
400
- cursor: meta.nextCursor ?? undefined, // for pagination
401
- fields: 'id,name,status', // select specific fields
436
+ bucketKey: 'orders',
437
+ filters: [{ field: 'status', op: '==', value: 'active' }],
438
+ timeScope: '30d',
439
+ sortOrder: 'desc',
440
+ limit: 50,
441
+ cursor: meta.nextCursor ?? undefined,
442
+ fields: 'id,name,status',
402
443
  });
403
444
  ```
404
445
 
405
- **Filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
446
+ **Filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
406
447
  **Max 3 filters per query.**
407
448
 
408
- #### `db.records.insert(payload)`
449
+ #### `db.records.insert(payload, options)`
409
450
 
410
451
  ```ts
411
- const { data, meta } = await db.records.insert({
412
- values: { name: 'Alice', role: 'admin', score: 100 },
413
- queryableFields: ['name', 'role'], // fields you want to filter by later
414
- userEmail: 'system@example.com',
415
- });
452
+ const { data, meta } = await db.records.insert(
453
+ {
454
+ values: { name: 'Alice', role: 'admin', score: 100 },
455
+ queryableFields: ['name', 'role'],
456
+ userEmail: 'system@example.com',
457
+ },
458
+ { bucketKey: 'users' }
459
+ );
416
460
  // meta.id — the new record's ID
417
461
  ```
418
462
 
419
- #### `db.records.update(payload)`
463
+ #### `db.records.update(payload, options)`
420
464
 
421
465
  ```ts
422
- await db.records.update({
423
- recordId: 'rec_abc',
424
- values: { score: 150 },
425
- track_record_history: true, // snapshot the previous version
426
- reason: 'Manual score adjustment',
427
- });
466
+ await db.records.update(
467
+ {
468
+ recordId: 'rec_abc',
469
+ values: { score: 150 },
470
+ track_record_history: true,
471
+ reason: 'Manual score adjustment',
472
+ },
473
+ { bucketKey: 'users' }
474
+ );
428
475
  ```
429
476
 
430
- #### `db.records.batchInsert(payload)`
477
+ #### `db.records.batchInsert(payload, options)`
431
478
 
432
479
  ```ts
433
- const result = await db.records.batchInsert({
434
- records: [
435
- { name: 'Alice', score: 99 },
436
- { name: 'Bob', score: 82 },
437
- ],
438
- queryableFields: ['name'],
439
- userEmail: 'import@example.com',
440
- });
480
+ const result = await db.records.batchInsert(
481
+ {
482
+ records: [{ name: 'Alice', score: 99 }, { name: 'Bob', score: 82 }],
483
+ queryableFields: ['name'],
484
+ userEmail: 'import@example.com',
485
+ },
486
+ { bucketKey: 'users' }
487
+ );
441
488
  // result.meta.failed — number of records that failed
442
489
  ```
443
490
 
@@ -445,7 +492,7 @@ const result = await db.records.batchInsert({
445
492
 
446
493
  ### Auth
447
494
 
448
- All methods live on `db.auth`.
495
+ All methods live on `db.auth`. Auth routes use `authKey` and do **not** require a `bucketKey`.
449
496
 
450
497
  | Method | Description |
451
498
  |---|---|
@@ -477,7 +524,7 @@ const { data: user, session } = await db.auth.signUp({
477
524
  fullName: 'Alice Smith',
478
525
  });
479
526
 
480
- // 2. Store session tokens (e.g. in a cookie or localStorage)
527
+ // 2. Store session tokens
481
528
  const { sessionId, refreshToken, expiresAt } = session;
482
529
 
483
530
  // 3. On each request, validate the session
@@ -494,32 +541,30 @@ await db.auth.signOut({ sessionId });
494
541
 
495
542
  ### Analytics
496
543
 
497
- All methods live on `db.analytics`. Every method maps to a BigQuery-backed analytics query. The server `queryType` string each method sends is noted for reference.
544
+ All methods live on `db.analytics`. Every method requires a `bucketKey` option.
498
545
 
499
546
  | Method | Server `queryType` | Description |
500
547
  |---|---|---|
501
- | `count(opts?)` | `"count"` | Total record count |
502
- | `distribution(field, opts?)` | `"distribution"` | Value histogram for a field |
503
- | `sum(field, opts?)` | `"sum"` | Sum a numeric field, with optional group-by |
504
- | `timeSeries(opts?)` | `"timeSeries"` | Record count over time |
505
- | `fieldTimeSeries(field, opts?)` | `"fieldTimeSeries"` | Numeric field aggregated over time |
506
- | `topN(field, n, opts?)` | `"topN"` | Top N field values by frequency |
507
- | `stats(field, opts?)` | `"stats"` | min/max/avg/stddev/p50/p90/p99 |
508
- | `records(opts?)` | `"records"` | Filtered + paginated raw records |
509
- | `multiMetric(metrics, opts?)` | `"multiMetric"` | Multiple aggregations in one call |
510
- | `storageStats(opts?)` | `"storageStats"` | Bucket storage usage |
511
- | `crossBucket(opts)` | `"crossBucket"` | Compare metric across buckets |
512
- | `query(payload, opts?)` | any | Raw query — full control |
548
+ | `count({ bucketKey, ...opts })` | `"count"` | Total record count |
549
+ | `distribution(field, { bucketKey, ...opts })` | `"distribution"` | Value histogram for a field |
550
+ | `sum(field, { bucketKey, ...opts })` | `"sum"` | Sum a numeric field, with optional group-by |
551
+ | `timeSeries({ bucketKey, ...opts })` | `"timeSeries"` | Record count over time |
552
+ | `fieldTimeSeries(field, { bucketKey, ...opts })` | `"fieldTimeSeries"` | Numeric field aggregated over time |
553
+ | `topN(field, n, { bucketKey, ...opts })` | `"topN"` | Top N field values by frequency |
554
+ | `stats(field, { bucketKey, ...opts })` | `"stats"` | min/max/avg/stddev/p50/p90/p99 |
555
+ | `records({ bucketKey, ...opts })` | `"records"` | Filtered + paginated raw records |
556
+ | `multiMetric(metrics, { bucketKey, ...opts })` | `"multiMetric"` | Multiple aggregations in one call |
557
+ | `storageStats({ bucketKey, ...opts })` | `"storageStats"` | Bucket storage usage |
558
+ | `crossBucket({ bucketKey, bucketKeys, ...opts })` | `"crossBucket"` | Compare metric across buckets |
559
+ | `query(payload, { bucketKey, ...opts })` | any | Raw query — full control |
513
560
 
514
561
  #### Date ranges
515
562
 
516
563
  All analytics methods accept a `dateRange` option:
517
564
 
518
565
  ```ts
519
- // Scope to a specific date window
520
566
  const dateRange = { startDate: '2025-01-01', endDate: '2025-12-31' };
521
-
522
- // Or scope to a year / month / day
567
+ // Or scope to a year / month
523
568
  const dateRange = { year: '2025', month: '03' };
524
569
  ```
525
570
 
@@ -527,57 +572,63 @@ const dateRange = { year: '2025', month: '03' };
527
572
 
528
573
  ```ts
529
574
  // Total records
530
- const { data } = await db.analytics.count();
575
+ const { data } = await db.analytics.count({ bucketKey: 'orders' });
531
576
  console.log(data.count);
532
577
 
533
578
  // Status breakdown
534
- const { data } = await db.analytics.distribution('status');
579
+ const { data } = await db.analytics.distribution('status', { bucketKey: 'orders' });
535
580
  // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
536
581
 
537
582
  // Monthly revenue for Q1
538
583
  const { data } = await db.analytics.sum('amount', {
539
- groupBy: 'region',
540
- dateRange: { startDate: '2025-01-01', endDate: '2025-03-31' },
584
+ bucketKey: 'orders',
585
+ groupBy: 'region',
586
+ dateRange: { startDate: '2025-01-01', endDate: '2025-03-31' },
541
587
  });
542
588
 
543
589
  // Daily signups this week
544
590
  const { data } = await db.analytics.timeSeries({
591
+ bucketKey: 'users',
545
592
  granularity: 'day',
546
- dateRange: { startDate: '2025-03-01' },
593
+ dateRange: { startDate: '2025-03-01' },
547
594
  });
548
595
 
549
596
  // Score percentiles
550
- const { data } = await db.analytics.stats('score');
597
+ const { data } = await db.analytics.stats('score', { bucketKey: 'users' });
551
598
  console.log(data.avg, data.p90, data.p99);
552
599
 
553
600
  // Top 5 countries
554
- const { data } = await db.analytics.topN('country', 5);
601
+ const { data } = await db.analytics.topN('country', 5, { bucketKey: 'users' });
555
602
 
556
603
  // Dashboard stats — one round-trip for multiple metrics
557
- const { data } = await db.analytics.multiMetric([
558
- { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
559
- { name: 'avgScore', field: 'score', aggregation: 'avg' },
560
- { name: 'userCount', field: 'userId', aggregation: 'count' },
561
- ]);
604
+ const { data } = await db.analytics.multiMetric(
605
+ [
606
+ { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
607
+ { name: 'avgScore', field: 'score', aggregation: 'avg' },
608
+ { name: 'userCount', field: 'userId', aggregation: 'count' },
609
+ ],
610
+ { bucketKey: 'orders' }
611
+ );
562
612
  console.log(data.totalRevenue, data.avgScore);
563
613
 
564
614
  // Storage usage
565
- const { data } = await db.analytics.storageStats();
615
+ const { data } = await db.analytics.storageStats({ bucketKey: 'orders' });
566
616
  console.log(data.totalRecords, data.totalBytes);
567
617
 
568
618
  // Cross-bucket revenue comparison
569
619
  const { data } = await db.analytics.crossBucket({
620
+ bucketKey: 'sales-2025',
570
621
  bucketKeys: ['sales-2024', 'sales-2025'],
571
622
  field: 'amount',
572
623
  aggregation: 'sum',
573
624
  });
574
625
 
575
- // Raw records with filters (supports: == != > < >= <= CONTAINS)
626
+ // Raw records with filters
576
627
  const { data } = await db.analytics.records({
577
- filters: [{ field: 'role', op: '==', value: 'admin' }],
628
+ bucketKey: 'users',
629
+ filters: [{ field: 'role', op: '==', value: 'admin' }],
578
630
  selectFields: ['email', 'createdAt'],
579
- limit: 50,
580
- offset: 0,
631
+ limit: 50,
581
632
  });
582
633
  console.log(data.data, data.hasMore);
583
634
  ```
@@ -586,13 +637,11 @@ console.log(data.data, data.hasMore);
586
637
 
587
638
  ## Error Handling
588
639
 
589
- The SDK throws typed errors you can `instanceof` check:
590
-
591
640
  ```ts
592
641
  import { HydrousError, HydrousNetworkError, HydrousTimeoutError } from 'hydrousdb';
593
642
 
594
643
  try {
595
- await db.records.get('rec_does_not_exist');
644
+ await db.records.get('rec_does_not_exist', { bucketKey: 'users' });
596
645
  } catch (err) {
597
646
  if (err instanceof HydrousError) {
598
647
  console.error(err.message); // human-readable message
@@ -620,17 +669,19 @@ let cursor: string | null = null;
620
669
 
621
670
  do {
622
671
  const { data, meta } = await db.records.query({
623
- limit: 100,
624
- cursor: cursor ?? undefined,
672
+ bucketKey: 'orders',
673
+ limit: 100,
674
+ cursor: cursor ?? undefined,
625
675
  });
626
676
 
627
677
  processBatch(data);
628
678
  cursor = meta.nextCursor;
629
679
  } while (cursor);
630
680
 
631
- // — OR — let the SDK handle it for you
681
+ // — OR — let the SDK handle it
632
682
  const allRecords = await db.records.queryAll({
633
- filters: [{ field: 'status', op: '==', value: 'active' }],
683
+ bucketKey: 'orders',
684
+ filters: [{ field: 'status', op: '==', value: 'active' }],
634
685
  });
635
686
  ```
636
687
 
@@ -648,21 +699,23 @@ import type {
648
699
  AuthUser,
649
700
  SessionInfo,
650
701
  HydrousConfig,
702
+ BucketOptions,
651
703
  Filter,
652
704
  } from 'hydrousdb';
653
705
  ```
654
706
 
655
- You can also use the generic `query<T>` overload to type your records:
707
+ Use the generic `query<T>` overload to type your records:
656
708
 
657
709
  ```ts
658
710
  interface Product {
659
- name: string;
660
- price: number;
711
+ name: string;
712
+ price: number;
661
713
  category: string;
662
714
  }
663
715
 
664
716
  const { data } = await db.records.query<Product>({
665
- filters: [{ field: 'category', op: '==', value: 'shoes' }],
717
+ bucketKey: 'products',
718
+ filters: [{ field: 'category', op: '==', value: 'shoes' }],
666
719
  });
667
720
 
668
721
  // data is (Product & HydrousRecord)[]
@@ -671,6 +724,44 @@ console.log(data[0].name, data[0].price);
671
724
 
672
725
  ---
673
726
 
727
+ ## Migration from v1
728
+
729
+ v2 introduces two breaking changes:
730
+
731
+ ### 1. `bucketKey` moves from `createClient` to per-call options
732
+
733
+ ```ts
734
+ // v1
735
+ const db = createClient({ authKey: 'ak_...', bucketKey: 'users' });
736
+ await db.records.query({ limit: 50 });
737
+
738
+ // v2
739
+ const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
740
+ await db.records.query({ bucketKey: 'users', limit: 50 });
741
+ ```
742
+
743
+ ### 2. `securityKey` replaces the implicit credential
744
+
745
+ In v1 the SDK incorrectly used `bucketKey` as both the bucket identifier and the security credential in the URL. v2 adds an explicit `securityKey` that maps to the server's `:key` parameter.
746
+
747
+ ```ts
748
+ // v1 (broken — used bucketKey as both segments)
749
+ createClient({ authKey: 'ak_...', bucketKey: 'users' });
750
+ // → /api/users/users ❌
751
+
752
+ // v2 (correct)
753
+ createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
754
+ // → /api/users/sk_... ✅ (bucketKey passed per-call)
755
+ ```
756
+
757
+ **Quick migration checklist:**
758
+ 1. Add `securityKey` to your `createClient` call
759
+ 2. Remove `bucketKey` from `createClient`
760
+ 3. Add `{ bucketKey: '...' }` to every `db.records.*` and `db.analytics.*` call
761
+ 4. Auth calls (`db.auth.*`) are unchanged
762
+
763
+ ---
764
+
674
765
  ## Contributing
675
766
 
676
767
  1. Fork the repo and clone it