@objy/server 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/API.md +1469 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +9 -0
  4. package/README.md +21 -2
  5. package/objydb.config.json +48 -0
  6. package/package.json +42 -4
  7. package/public/index.html +4134 -0
  8. package/public/objy-db-logo.png +0 -0
  9. package/src/aggregate.js +438 -0
  10. package/src/api.js +15 -0
  11. package/src/auth.js +472 -0
  12. package/src/clock.js +18 -0
  13. package/src/cluster-secret.js +25 -0
  14. package/src/cluster-status.js +562 -0
  15. package/src/compression-stats.js +32 -0
  16. package/src/config.js +360 -0
  17. package/src/crdt.js +1076 -0
  18. package/src/derived-state-scheduler.js +128 -0
  19. package/src/files.js +324 -0
  20. package/src/hnsw.js +269 -0
  21. package/src/http-cors.js +31 -0
  22. package/src/http-router.js +162 -0
  23. package/src/index.js +137 -0
  24. package/src/interfaces/auth.js +123 -0
  25. package/src/interfaces/cluster.js +296 -0
  26. package/src/interfaces/public-object.js +2452 -0
  27. package/src/interfaces/ui.js +38 -0
  28. package/src/lib/crypto.cjs +54 -0
  29. package/src/lib/jwt.cjs +231 -0
  30. package/src/local-wal.js +122 -0
  31. package/src/memory-guard.js +152 -0
  32. package/src/ndjson.js +117 -0
  33. package/src/node-runtime.js +29 -0
  34. package/src/object-hooks.js +282 -0
  35. package/src/object-id.js +7 -0
  36. package/src/object-storage.js +263 -0
  37. package/src/object-store.js +288 -0
  38. package/src/patch.js +87 -0
  39. package/src/permissions.js +178 -0
  40. package/src/persistence.js +1918 -0
  41. package/src/plugin-loader.js +21 -0
  42. package/src/query-planner.js +266 -0
  43. package/src/query.js +416 -0
  44. package/src/references.js +349 -0
  45. package/src/replication-scheduler.js +303 -0
  46. package/src/restore-status.js +77 -0
  47. package/src/runtime.js +47 -0
  48. package/src/server.js +242 -0
  49. package/src/shape.js +286 -0
  50. package/src/shard-map-store.js +85 -0
  51. package/src/shard-map.js +333 -0
  52. package/src/shard-router.js +737 -0
  53. package/src/store.js +3410 -0
  54. package/src/system-secrets.js +107 -0
  55. package/src/vector.js +83 -0
package/API.md ADDED
@@ -0,0 +1,1469 @@
1
+ # OBJY DB HTTP API
2
+
3
+ <p align="center"><strong>Plain objects. Bulk-first operations. One public API.</strong></p>
4
+
5
+ This is the public HTTP contract for OBJY DB. The standalone [JavaScript client](https://github.com/objy-org/objy-client) exposes the same operations for browsers and Node.
6
+
7
+ [Start here](#start-here) | [JavaScript client](https://github.com/objy-org/objy-client) | [Database README](README.md)
8
+
9
+ ## Start Here
10
+
11
+ Examples use `{URL}` as the server base URL. Public requests may reach any cluster node; OBJY DB routes them to the owning object shard.
12
+
13
+ ```text
14
+ database
15
+ collection
16
+ object _id=...
17
+ ```
18
+
19
+ | Placeholder | Meaning |
20
+ | --- | --- |
21
+ | `{URL}` | Public node or gateway URL, without a trailing slash. |
22
+ | `{db}` | Database namespace. |
23
+ | `{collection}` | Collection inside the database. |
24
+ | `{_id}` | Caller-provided or generated object id. |
25
+
26
+ Most routes require a bearer token:
27
+
28
+ ```http
29
+ Authorization: Bearer <accessToken>
30
+ Content-Type: application/json
31
+ ```
32
+
33
+ Objects are dynamic JSON-style records. Collections require no schema, and objects in one collection may have different shapes and value types. Every object has an `_id`; inserts generate a compact URL-safe id when it is omitted.
34
+
35
+ `PUT` replaces a complete object, `PATCH` applies Mongo-like `$set` and `$unset`, and `DELETE` creates a replicated delete. Query, search, and aggregation responses are plain arrays rather than envelopes.
36
+
37
+ ## Endpoint Map
38
+
39
+ | Area | Routes |
40
+ | --- | --- |
41
+ | [Authentication](#authentication-and-permissions) | Login and renew against any auth-enabled collection. |
42
+ | [Databases](#databases) | List, create, bootstrap an administrator, and delete. |
43
+ | [Collections](#collections) | List, create, delete, and manage collection metadata. |
44
+ | [Objects](#objects) | Single and bulk create, read, replace, patch, and delete. |
45
+ | [Imports](#streamed-bulk-writes) | Bounded NDJSON insert, replace, patch, and delete. |
46
+ | [Search](#query) | Filters, sorting, paging, aggregation, vectors, and references. |
47
+ | [Files](#file-handling) | Object-backed multipart upload, replacement, download, and deletion. |
48
+ | [Operations](#health-and-cluster-status) | Health and authenticated public cluster status. |
49
+
50
+ ## Authentication and Permissions
51
+
52
+ Authentication is collection-based: any collection may contain user objects. On first boot, OBJY DB creates `admin` in `_global/users` and prints its generated password once. `OBJYDB_ADMIN_PASSWORD` may supply that initial password for unattended startup.
53
+
54
+ Passwords are hashed on write and redacted from responses. Tokens are scoped to the database and collection used for authentication.
55
+
56
+ ### Login
57
+
58
+ `POST /api/{db}/{collection}/auth`
59
+
60
+ **Request**
61
+
62
+ ```http
63
+ POST {URL}/api/_global/users/auth
64
+ Content-Type: application/json
65
+
66
+ {
67
+ "username": "admin",
68
+ "password": "<initial-password>"
69
+ }
70
+ ```
71
+
72
+ **Response**
73
+
74
+ ```json
75
+ {
76
+ "user": { "_id": "admin", "username": "admin", "role": "admin" },
77
+ "accessToken": "...",
78
+ "refreshToken": "...",
79
+ "expiresIn": 3600,
80
+ "expires_in": 3600
81
+ }
82
+ ```
83
+
84
+ ### Renew Token
85
+
86
+ `POST /api/{db}/{collection}/renew`
87
+
88
+ **Request**
89
+
90
+ ```http
91
+ POST {URL}/api/_global/users/renew
92
+ Content-Type: application/json
93
+
94
+ {
95
+ "refreshToken": "..."
96
+ }
97
+ ```
98
+
99
+ **Response**
100
+
101
+ ```json
102
+ {
103
+ "user": { "_id": "admin", "username": "admin", "role": "admin" },
104
+ "accessToken": "...",
105
+ "refreshToken": "...",
106
+ "expiresIn": 3600,
107
+ "expires_in": 3600
108
+ }
109
+ ```
110
+
111
+ Refresh tokens must be renewed against the same database and collection that issued them.
112
+
113
+ <details>
114
+ <summary><strong>Safe renewal and replay</strong></summary>
115
+
116
+ Access-token expiration rejected before an operation begins returns HTTP 401 with `code: "ACCESS_TOKEN_EXPIRED"` and `operationStarted: false`. A client may renew and replay that request once.
117
+
118
+ These markers do not authorize replay when `operationStarted` is true or `writeMayHaveSucceeded` is true. Other authentication failures and permission errors are not expiration signals.
119
+
120
+ </details>
121
+
122
+ ### Permission Model
123
+
124
+ Database users can access only the database where they authenticated. A global user can address other databases, but database and collection administration requires `admin: true` or `role: "admin"`. Creating or deleting databases and reading `/api/config` specifically require a global admin token.
125
+
126
+ The built-in `_global` database is read-only through the public API after bootstrap. Object writes, metadata changes, collection changes, and database deletion are rejected even for a global administrator.
127
+
128
+ Collection grants provide broad read or write access by collection name:
129
+
130
+ ```json
131
+ {
132
+ "_id": "editor",
133
+ "username": "editor",
134
+ "password": "secret",
135
+ "collectionPermissions": {
136
+ "read": ["articles", "media"],
137
+ "write": ["articles"]
138
+ }
139
+ }
140
+ ```
141
+
142
+ `read` allows object reads and queries. `write` allows create, update, and delete, but does not imply read access. Create users through `POST /api/{db}/users/object`; permission changes apply after login or renewal issues a new token.
143
+
144
+ Object-level rules combine a query with `c`, `r`, `u`, and `d` permissions:
145
+
146
+ ```json
147
+ {
148
+ "_id": "editor",
149
+ "username": "editor",
150
+ "password": "secret",
151
+ "permissions": [
152
+ {
153
+ "name": "own articles",
154
+ "query": { "ownerId": "editor" },
155
+ "perm": "crud"
156
+ }
157
+ ]
158
+ }
159
+ ```
160
+
161
+ Read rules are merged into the requested query. Updates must permit both the stored object and its resulting value. A non-admin user without a matching rule or collection grant is denied by default.
162
+
163
+ ## Databases
164
+
165
+ ### List Databases
166
+
167
+ `GET /api/databases`
168
+
169
+ **Request**
170
+
171
+ ```http
172
+ GET {URL}/api/databases
173
+ Authorization: Bearer <accessToken>
174
+ ```
175
+
176
+ **Response**
177
+
178
+ ```json
179
+ {
180
+ "databases": ["_global", "{db}"]
181
+ }
182
+ ```
183
+
184
+ ### Create Database
185
+
186
+ `POST /api/databases`
187
+
188
+ **Request**
189
+
190
+ ```http
191
+ POST {URL}/api/databases
192
+ Authorization: Bearer <accessToken>
193
+ Content-Type: application/json
194
+
195
+ {
196
+ "name": "{db}",
197
+ "meta": { "owner": "team-a" }
198
+ }
199
+ ```
200
+
201
+ **Response**
202
+
203
+ ```json
204
+ {
205
+ "name": "{db}",
206
+ "created": true,
207
+ "meta": { "owner": "team-a" }
208
+ }
209
+ ```
210
+
211
+ ### Create Database With First Admin User
212
+
213
+ `POST /api/databases`
214
+
215
+ **Request**
216
+
217
+ ```http
218
+ POST {URL}/api/databases
219
+ Authorization: Bearer <accessToken>
220
+ Content-Type: application/json
221
+
222
+ {
223
+ "name": "{db}",
224
+ "adminUser": {
225
+ "collection": "users",
226
+ "username": "owner",
227
+ "password": "secret"
228
+ }
229
+ }
230
+ ```
231
+
232
+ **Response**
233
+
234
+ ```json
235
+ {
236
+ "name": "{db}",
237
+ "created": true,
238
+ "meta": {},
239
+ "adminUser": {
240
+ "collection": "users",
241
+ "user": {
242
+ "_id": "mHzQzLE6k9P9s6b2",
243
+ "username": "owner",
244
+ "admin": true
245
+ }
246
+ }
247
+ }
248
+ ```
249
+
250
+ If `adminUser.collection` is omitted, `users` is used. The users collection receives a `username` index.
251
+
252
+ ### Delete Database
253
+
254
+ `DELETE /api/databases/{db}`
255
+
256
+ This requires a global admin token. Deletion removes the database metadata and objects across the cluster and attempts to remove associated file blobs. `_global` cannot be deleted.
257
+
258
+ **Request**
259
+
260
+ ```http
261
+ DELETE {URL}/api/databases/{db}
262
+ Authorization: Bearer <accessToken>
263
+ ```
264
+
265
+ **Response**
266
+
267
+ ```http
268
+ HTTP/1.1 204 No Content
269
+ ```
270
+
271
+ ## Collections
272
+
273
+ A collection is a dynamic group of objects plus a small metadata document. Metadata is kept in memory, persisted separately from objects, and may contain custom application keys.
274
+
275
+ These built-in keys change runtime behavior:
276
+
277
+ | Key | Purpose |
278
+ | --- | --- |
279
+ | `indexes` | Field indexes for equality filters, candidate narrowing, grouped values, and sorting. |
280
+ | `columns` | RID-addressed typed values for compact storage and numeric aggregation. |
281
+ | `tiering` | Compressed-memory offloading for selected cold object fields. |
282
+ | `hooks` | Names and activation settings for server-registered object middleware. |
283
+
284
+ ```json
285
+ {
286
+ "indexes": ["status", "tenantId"],
287
+ "columns": ["score"],
288
+ "tiering": {
289
+ "mode": "compressedMemory",
290
+ "keep": ["name", "status", "tenantId"],
291
+ "minBytes": 4096
292
+ },
293
+ "hooks": {
294
+ "enabled": true,
295
+ "use": ["timestamps", "audit"]
296
+ }
297
+ }
298
+ ```
299
+
300
+ Field indexes accelerate supported filter and sort paths. Columns use typed arrays for numbers and booleans, dictionary codes for strings, and an overflow map for unsupported values. They support `$sum`, `$avg`, `$min`, `$max`, `$count`, and grouped aggregation without changing hydrated API objects.
301
+
302
+ Tiering accepts either `keep` or `offload`, never both. For example, `"offload": ["history", "rawPayload"]` compresses only those paths. Indexed and column fields remain hot automatically. Queries over non-indexed compressed fields remain correct but require scan-time hydration.
303
+
304
+ Hooks store configuration only; executable code is registered on the server. `use` enables hooks for their registered operations, operation-specific arrays may target `create`, `read`, `update`, or `delete`, and `enabled: false` bypasses the chain.
305
+
306
+ Changing indexes, columns, or tiering rebuilds derived in-memory state through cooperative bounded tasks. Changing hooks only invalidates the cached hook plan.
307
+
308
+ ### List Collections
309
+
310
+ `GET /api/{db}/collections`
311
+
312
+ **Request**
313
+
314
+ ```http
315
+ GET {URL}/api/{db}/collections
316
+ Authorization: Bearer <accessToken>
317
+ ```
318
+
319
+ **Response**
320
+
321
+ ```json
322
+ {
323
+ "database": "{db}",
324
+ "collections": ["{collection}", "users"]
325
+ }
326
+ ```
327
+
328
+ ### Create Collection
329
+
330
+ `POST /api/{db}/collections`
331
+
332
+ **Request**
333
+
334
+ ```http
335
+ POST {URL}/api/{db}/collections
336
+ Authorization: Bearer <accessToken>
337
+ Content-Type: application/json
338
+
339
+ {
340
+ "name": "{collection}",
341
+ "indexes": ["status", "tenantId"],
342
+ "columns": ["score"],
343
+ "hooks": {
344
+ "enabled": true,
345
+ "use": ["timestamps", "audit"]
346
+ },
347
+ "tiering": {
348
+ "mode": "compressedMemory",
349
+ "keep": ["name", "status", "tenantId"],
350
+ "minBytes": 4096
351
+ }
352
+ }
353
+ ```
354
+
355
+ **Response**
356
+
357
+ ```json
358
+ {
359
+ "name": "{collection}",
360
+ "created": true,
361
+ "meta": {
362
+ "indexes": ["status", "tenantId"],
363
+ "columns": ["score"],
364
+ "hooks": {
365
+ "enabled": true,
366
+ "use": ["timestamps", "audit"]
367
+ },
368
+ "tiering": {
369
+ "mode": "compressedMemory",
370
+ "keep": ["name", "status", "tenantId"],
371
+ "minBytes": 4096
372
+ }
373
+ }
374
+ }
375
+ ```
376
+
377
+ You may also send `{ "name": "{collection}", "meta": { ... } }`.
378
+
379
+ ### Read Collection Meta
380
+
381
+ `GET /api/{db}/{collection}/meta`
382
+
383
+ **Request**
384
+
385
+ ```http
386
+ GET {URL}/api/{db}/{collection}/meta
387
+ Authorization: Bearer <accessToken>
388
+ ```
389
+
390
+ **Response**
391
+
392
+ ```json
393
+ {
394
+ "indexes": ["status", "tenantId"],
395
+ "columns": ["score"],
396
+ "hooks": {
397
+ "enabled": true,
398
+ "use": ["timestamps", "audit"]
399
+ },
400
+ "tiering": {
401
+ "mode": "compressedMemory",
402
+ "keep": ["name", "status", "tenantId"],
403
+ "minBytes": 4096
404
+ }
405
+ }
406
+ ```
407
+
408
+ ### Replace Collection Meta
409
+
410
+ `PUT /api/{db}/{collection}/meta`
411
+
412
+ **Request**
413
+
414
+ ```http
415
+ PUT {URL}/api/{db}/{collection}/meta
416
+ Authorization: Bearer <accessToken>
417
+ Content-Type: application/json
418
+
419
+ {
420
+ "indexes": ["status", "tenantId"],
421
+ "columns": ["score"],
422
+ "hooks": {
423
+ "enabled": true,
424
+ "use": ["timestamps", "audit"]
425
+ },
426
+ "tiering": {
427
+ "mode": "compressedMemory",
428
+ "keep": ["name", "status", "tenantId"],
429
+ "minBytes": 4096
430
+ }
431
+ }
432
+ ```
433
+
434
+ **Response**
435
+
436
+ ```json
437
+ {
438
+ "indexes": ["status", "tenantId"],
439
+ "columns": ["score"],
440
+ "hooks": {
441
+ "enabled": true,
442
+ "use": ["timestamps", "audit"]
443
+ },
444
+ "tiering": {
445
+ "mode": "compressedMemory",
446
+ "keep": ["name", "status", "tenantId"],
447
+ "minBytes": 4096
448
+ }
449
+ }
450
+ ```
451
+
452
+ ### Delete Collection
453
+
454
+ `DELETE /api/{db}/collections/{collection}`
455
+
456
+ **Request**
457
+
458
+ ```http
459
+ DELETE {URL}/api/{db}/collections/{collection}
460
+ Authorization: Bearer <accessToken>
461
+ ```
462
+
463
+ **Response**
464
+
465
+ ```http
466
+ HTTP/1.1 204 No Content
467
+ ```
468
+
469
+ ## Objects
470
+
471
+ Objects are plain dynamic JSON-style records. Strings, numbers, booleans, arrays, nested objects, and `null` may coexist across different shapes in one collection.
472
+
473
+ Every object has an `_id`. If `_id` is omitted on insert, the server generates a compact 16-character URL-safe id. Single and bulk operations use the same base route. Special fields such as `_vec` are handled by storage features and hidden from ordinary read responses.
474
+
475
+ ### Insert One
476
+
477
+ `POST /api/{db}/{collection}/object`
478
+
479
+ **Request**
480
+
481
+ ```http
482
+ POST {URL}/api/{db}/{collection}/object
483
+ Authorization: Bearer <accessToken>
484
+ Content-Type: application/json
485
+
486
+ {
487
+ "_id": "object-1",
488
+ "status": "active",
489
+ "score": 42,
490
+ "tags": ["new", "paid"]
491
+ }
492
+ ```
493
+
494
+ **Response**
495
+
496
+ ```json
497
+ {
498
+ "_id": "object-1",
499
+ "status": "active",
500
+ "score": 42,
501
+ "tags": ["new", "paid"]
502
+ }
503
+ ```
504
+
505
+ ### Get One
506
+
507
+ `GET /api/{db}/{collection}/object/{_id}`
508
+
509
+ **Request**
510
+
511
+ ```http
512
+ GET {URL}/api/{db}/{collection}/object/object-1
513
+ Authorization: Bearer <accessToken>
514
+ ```
515
+
516
+ **Response**
517
+
518
+ ```json
519
+ {
520
+ "_id": "object-1",
521
+ "status": "active",
522
+ "score": 42
523
+ }
524
+ ```
525
+
526
+ ### Replace One
527
+
528
+ `PUT /api/{db}/{collection}/object/{_id}`
529
+
530
+ `PUT` is a full object replacement. The URL object id is authoritative.
531
+
532
+ **Request**
533
+
534
+ ```http
535
+ PUT {URL}/api/{db}/{collection}/object/object-1
536
+ Authorization: Bearer <accessToken>
537
+ Content-Type: application/json
538
+
539
+ {
540
+ "status": "archived",
541
+ "score": 11
542
+ }
543
+ ```
544
+
545
+ **Response**
546
+
547
+ ```json
548
+ {
549
+ "_id": "object-1",
550
+ "status": "archived",
551
+ "score": 11
552
+ }
553
+ ```
554
+
555
+ ### Patch One
556
+
557
+ `PATCH /api/{db}/{collection}/object/{_id}`
558
+
559
+ `PATCH` is a Mongo-like partial update. Use `$set` for dot-path writes and `$unset` for dot-path removals. `_id` cannot be changed.
560
+
561
+ **Request**
562
+
563
+ ```http
564
+ PATCH {URL}/api/{db}/{collection}/object/object-1
565
+ Authorization: Bearer <accessToken>
566
+ Content-Type: application/json
567
+
568
+ {
569
+ "$set": {
570
+ "score": 12,
571
+ "profile.city": "Hamburg"
572
+ },
573
+ "$unset": ["legacyFlag"]
574
+ }
575
+ ```
576
+
577
+ **Response**
578
+
579
+ ```json
580
+ {
581
+ "_id": "object-1",
582
+ "status": "archived",
583
+ "score": 12,
584
+ "profile": {
585
+ "city": "Hamburg"
586
+ }
587
+ }
588
+ ```
589
+
590
+ ### Delete One
591
+
592
+ `DELETE /api/{db}/{collection}/object/{_id}`
593
+
594
+ **Request**
595
+
596
+ ```http
597
+ DELETE {URL}/api/{db}/{collection}/object/object-1
598
+ Authorization: Bearer <accessToken>
599
+ ```
600
+
601
+ **Response**
602
+
603
+ ```http
604
+ HTTP/1.1 204 No Content
605
+ ```
606
+
607
+ ## File Handling
608
+
609
+ File handling is object-backed. One object can wrap one S3-backed blob, while the database object stores only a small `_file` descriptor. The S3 bucket should stay private; blob reads go through the API and use the same read permissions as the wrapping object.
610
+
611
+ ```json
612
+ {
613
+ "_file": {
614
+ "storage": "s3",
615
+ "key": "objydb/files/db/{db}/collection/{collection}/object/object-1/blob/{fileId}",
616
+ "mimeType": "application/pdf"
617
+ }
618
+ }
619
+ ```
620
+
621
+ `_file.key` is derived by the server from `{db}`, `{collection}`, `_id`, and a generated file id. Clients should not invent or change it. Replacing a file writes a new blob key, commits the object pointer, then removes the old blob best-effort. Bulk file uploads are not supported; use one multipart request per file object.
622
+
623
+ ### Create Object With File
624
+
625
+ `POST /api/{db}/{collection}/object`
626
+
627
+ **Request**
628
+
629
+ ```http
630
+ POST {URL}/api/{db}/{collection}/object
631
+ Authorization: Bearer <accessToken>
632
+ Content-Type: multipart/form-data
633
+
634
+ part "object":
635
+ {
636
+ "_id": "object-1",
637
+ "name": "invoice.pdf",
638
+ "status": "received"
639
+ }
640
+
641
+ part "file":
642
+ <binary file content>
643
+ ```
644
+
645
+ **Response**
646
+
647
+ ```json
648
+ {
649
+ "_id": "object-1",
650
+ "name": "invoice.pdf",
651
+ "status": "received",
652
+ "_file": {
653
+ "storage": "s3",
654
+ "key": "objydb/files/db/{db}/collection/{collection}/object/object-1/blob/{fileId}",
655
+ "mimeType": "application/pdf"
656
+ }
657
+ }
658
+ ```
659
+
660
+ ### Read File Blob
661
+
662
+ `GET /api/{db}/{collection}/object/{_id}/blob`
663
+
664
+ **Request**
665
+
666
+ ```http
667
+ GET {URL}/api/{db}/{collection}/object/object-1/blob
668
+ Authorization: Bearer <accessToken>
669
+ ```
670
+
671
+ **Response**
672
+
673
+ ```http
674
+ HTTP/1.1 200 OK
675
+ Content-Type: application/pdf
676
+
677
+ <binary file content>
678
+ ```
679
+
680
+ ### Replace Object With File
681
+
682
+ `PUT /api/{db}/{collection}/object/{_id}`
683
+
684
+ `PUT` replaces the full wrapping object and stores the new file under a new server-derived blob key. No old file version is exposed by OBJY DB; old blob cleanup is best-effort after the object pointer is durable.
685
+
686
+ **Request**
687
+
688
+ ```http
689
+ PUT {URL}/api/{db}/{collection}/object/object-1
690
+ Authorization: Bearer <accessToken>
691
+ Content-Type: multipart/form-data
692
+
693
+ part "object":
694
+ {
695
+ "name": "invoice-final.pdf",
696
+ "status": "approved"
697
+ }
698
+
699
+ part "file":
700
+ <binary file content>
701
+ ```
702
+
703
+ **Response**
704
+
705
+ ```json
706
+ {
707
+ "_id": "object-1",
708
+ "name": "invoice-final.pdf",
709
+ "status": "approved",
710
+ "_file": {
711
+ "storage": "s3",
712
+ "key": "objydb/files/db/{db}/collection/{collection}/object/object-1/blob/{fileId}",
713
+ "mimeType": "application/pdf"
714
+ }
715
+ }
716
+ ```
717
+
718
+ ### Delete File Object
719
+
720
+ `DELETE /api/{db}/{collection}/object/{_id}`
721
+
722
+ Delete the wrapping object through normal object CRUD. The object delete is durable first; blob deletion is then attempted best-effort.
723
+
724
+ **Request**
725
+
726
+ ```http
727
+ DELETE {URL}/api/{db}/{collection}/object/object-1
728
+ Authorization: Bearer <accessToken>
729
+ ```
730
+
731
+ **Response**
732
+
733
+ ```http
734
+ HTTP/1.1 204 No Content
735
+ ```
736
+
737
+ ## Bulk Objects
738
+
739
+ Batches are split by object shard internally. Duplicate ids inside one bulk request are rejected.
740
+
741
+ ### Insert Many
742
+
743
+ `POST /api/{db}/{collection}/object`
744
+
745
+ **Request**
746
+
747
+ ```http
748
+ POST {URL}/api/{db}/{collection}/object
749
+ Authorization: Bearer <accessToken>
750
+ Content-Type: application/json
751
+
752
+ [
753
+ { "_id": "object-1", "status": "active", "score": 42 },
754
+ { "_id": "object-2", "status": "archived", "score": 10 }
755
+ ]
756
+ ```
757
+
758
+ **Response**
759
+
760
+ ```json
761
+ {
762
+ "inserted": 2,
763
+ "items": [
764
+ { "_id": "object-1", "status": "active", "score": 42 },
765
+ { "_id": "object-2", "status": "archived", "score": 10 }
766
+ ]
767
+ }
768
+ ```
769
+
770
+ ### Insert Many With Minimal Response
771
+
772
+ `POST /api/{db}/{collection}/object`
773
+
774
+ **Request**
775
+
776
+ ```http
777
+ POST {URL}/api/{db}/{collection}/object
778
+ Authorization: Bearer <accessToken>
779
+ Content-Type: application/json
780
+ Prefer: return=minimal
781
+
782
+ [
783
+ { "_id": "object-1", "status": "active" },
784
+ { "_id": "object-2", "status": "archived" }
785
+ ]
786
+ ```
787
+
788
+ **Response**
789
+
790
+ ```json
791
+ {
792
+ "inserted": 2
793
+ }
794
+ ```
795
+
796
+ ### Replace Many
797
+
798
+ `PUT /api/{db}/{collection}/object`
799
+
800
+ **Request**
801
+
802
+ ```http
803
+ PUT {URL}/api/{db}/{collection}/object
804
+ Authorization: Bearer <accessToken>
805
+ Content-Type: application/json
806
+
807
+ [
808
+ { "_id": "object-1", "status": "active", "score": 50 },
809
+ { "_id": "object-2", "status": "archived", "score": 12 }
810
+ ]
811
+ ```
812
+
813
+ **Response**
814
+
815
+ ```json
816
+ {
817
+ "replaced": 2,
818
+ "items": [
819
+ { "_id": "object-1", "status": "active", "score": 50 },
820
+ { "_id": "object-2", "status": "archived", "score": 12 }
821
+ ]
822
+ }
823
+ ```
824
+
825
+ ### Patch Many
826
+
827
+ `PATCH /api/{db}/{collection}/object`
828
+
829
+ **Request**
830
+
831
+ ```http
832
+ PATCH {URL}/api/{db}/{collection}/object
833
+ Authorization: Bearer <accessToken>
834
+ Content-Type: application/json
835
+
836
+ [
837
+ { "_id": "object-1", "$set": { "score": 51 }, "$unset": ["legacyFlag"] },
838
+ { "_id": "object-2", "$set": { "score": 13 } }
839
+ ]
840
+ ```
841
+
842
+ **Response**
843
+
844
+ ```json
845
+ {
846
+ "updated": 2,
847
+ "skipped": [],
848
+ "items": [
849
+ { "_id": "object-1", "status": "active", "score": 51 },
850
+ { "_id": "object-2", "status": "archived", "score": 13 }
851
+ ]
852
+ }
853
+ ```
854
+
855
+ ### Delete Many
856
+
857
+ `DELETE /api/{db}/{collection}/object`
858
+
859
+ **Request**
860
+
861
+ ```http
862
+ DELETE {URL}/api/{db}/{collection}/object
863
+ Authorization: Bearer <accessToken>
864
+ Content-Type: application/json
865
+
866
+ {
867
+ "ids": ["object-1", "object-2"]
868
+ }
869
+ ```
870
+
871
+ **Response**
872
+
873
+ ```json
874
+ {
875
+ "deleted": 2,
876
+ "ids": ["object-1", "object-2"],
877
+ "skipped": []
878
+ }
879
+ ```
880
+
881
+ The delete body may also be a plain array of ids.
882
+
883
+ ### Streamed Bulk Writes
884
+
885
+ Large imports can use NDJSON so the server does not need to hold one giant JSON array in memory. Each line is one JSON value. Internally, OBJY DB buffers lines into bounded chunks and then uses the same bulk storage, routing, replication, durability, permission, and index paths as normal bulk writes.
886
+
887
+ Streamed writes require minimal responses:
888
+
889
+ ```http
890
+ Content-Type: application/x-ndjson
891
+ Prefer: return=minimal
892
+ ```
893
+
894
+ If a streamed request fails after earlier chunks were written, those earlier chunks stay committed. Use stable `_id` values when clients retry large streams. Omitted `_id` values are allowed on insert, but generated ids are not returned by minimal streamed responses.
895
+
896
+ #### Stream Insert
897
+
898
+ `POST /api/{db}/{collection}/object`
899
+
900
+ **Request**
901
+
902
+ ```http
903
+ POST {URL}/api/{db}/{collection}/object
904
+ Authorization: Bearer <accessToken>
905
+ Content-Type: application/x-ndjson
906
+ Prefer: return=minimal
907
+
908
+ {"_id":"object-1","status":"active","score":42}
909
+ {"_id":"object-2","status":"archived","score":10}
910
+ {"_id":"object-3","status":"generated","score":3}
911
+ ```
912
+
913
+ **Response**
914
+
915
+ ```json
916
+ {
917
+ "inserted": 3
918
+ }
919
+ ```
920
+
921
+ #### Stream Replace
922
+
923
+ `PUT /api/{db}/{collection}/object`
924
+
925
+ **Request**
926
+
927
+ ```http
928
+ PUT {URL}/api/{db}/{collection}/object
929
+ Authorization: Bearer <accessToken>
930
+ Content-Type: application/x-ndjson
931
+ Prefer: return=minimal
932
+
933
+ {"_id":"object-1","status":"active","score":50}
934
+ {"_id":"object-2","status":"archived","score":12}
935
+ ```
936
+
937
+ **Response**
938
+
939
+ ```json
940
+ {
941
+ "replaced": 2
942
+ }
943
+ ```
944
+
945
+ #### Stream Patch
946
+
947
+ `PATCH /api/{db}/{collection}/object`
948
+
949
+ **Request**
950
+
951
+ ```http
952
+ PATCH {URL}/api/{db}/{collection}/object
953
+ Authorization: Bearer <accessToken>
954
+ Content-Type: application/x-ndjson
955
+ Prefer: return=minimal
956
+
957
+ {"_id":"object-1","$set":{"score":51},"$unset":["legacyFlag"]}
958
+ {"_id":"object-2","$set":{"score":13}}
959
+ {"_id":"missing","$set":{"score":99}}
960
+ ```
961
+
962
+ **Response**
963
+
964
+ ```json
965
+ {
966
+ "updated": 2,
967
+ "skipped": 1
968
+ }
969
+ ```
970
+
971
+ #### Stream Delete
972
+
973
+ `DELETE /api/{db}/{collection}/object`
974
+
975
+ **Request**
976
+
977
+ ```http
978
+ DELETE {URL}/api/{db}/{collection}/object
979
+ Authorization: Bearer <accessToken>
980
+ Content-Type: application/x-ndjson
981
+ Prefer: return=minimal
982
+
983
+ {"_id":"object-1"}
984
+ {"_id":"object-2"}
985
+ {"_id":"missing"}
986
+ ```
987
+
988
+ **Response**
989
+
990
+ ```json
991
+ {
992
+ "deleted": 2,
993
+ "skipped": 1
994
+ }
995
+ ```
996
+
997
+ ## Query
998
+
999
+ Query responses are plain arrays. Use GET for small URL-safe queries and POST `/search` for larger payloads. Normal object queries default to `250` rows and cap each page at `250`; use `offset` / `$skip` or `$page` to read more.
1000
+
1001
+ ### GET Query
1002
+
1003
+ `GET /api/{db}/{collection}/object`
1004
+
1005
+ `q` is URL-encoded JSON. The decoded query in this example is `{"status":"active","score":{"$gte":10}}`.
1006
+
1007
+ **Request**
1008
+
1009
+ ```http
1010
+ GET {URL}/api/{db}/{collection}/object?q=%7B%22status%22%3A%22active%22%2C%22score%22%3A%7B%22%24gte%22%3A10%7D%7D&sortBy=score&sort=desc&limit=50&offset=0
1011
+ Authorization: Bearer <accessToken>
1012
+ ```
1013
+
1014
+ **Response**
1015
+
1016
+ ```json
1017
+ [
1018
+ { "_id": "object-1", "status": "active", "score": 42 },
1019
+ { "_id": "object-9", "status": "active", "score": 31 }
1020
+ ]
1021
+ ```
1022
+
1023
+ ### POST Search
1024
+
1025
+ `POST /api/{db}/{collection}/search`
1026
+
1027
+ **Request**
1028
+
1029
+ ```http
1030
+ POST {URL}/api/{db}/{collection}/search
1031
+ Authorization: Bearer <accessToken>
1032
+ Content-Type: application/json
1033
+
1034
+ {
1035
+ "status": "active",
1036
+ "$sort": { "score": -1 },
1037
+ "$page": 1,
1038
+ "$pageSize": 50,
1039
+ "$refDepth": 1
1040
+ }
1041
+ ```
1042
+
1043
+ You may also use `$limit` and `$skip` in the POST body. URL `limit` / `offset` parameters take precedence when both are present. Page size is capped at `250`.
1044
+
1045
+ **Response**
1046
+
1047
+ ```json
1048
+ [
1049
+ { "_id": "object-1", "status": "active", "score": 42 }
1050
+ ]
1051
+ ```
1052
+
1053
+ ### Operator Query
1054
+
1055
+ `POST /api/{db}/{collection}/search`
1056
+
1057
+ **Request**
1058
+
1059
+ ```http
1060
+ POST {URL}/api/{db}/{collection}/search
1061
+ Authorization: Bearer <accessToken>
1062
+ Content-Type: application/json
1063
+
1064
+ {
1065
+ "status": { "$in": ["active", "queued"] },
1066
+ "score": { "$gte": 10, "$lt": 100 },
1067
+ "tags": { "$contains": "paid" },
1068
+ "$or": [
1069
+ { "status": "active" },
1070
+ { "score": { "$gt": 1000 } }
1071
+ ]
1072
+ }
1073
+ ```
1074
+
1075
+ **Response**
1076
+
1077
+ ```json
1078
+ [
1079
+ { "_id": "object-1", "status": "active", "score": 42, "tags": ["paid"] }
1080
+ ]
1081
+ ```
1082
+
1083
+ Also supported: `$eq`, `$equal`, `$deepEquals`, `$ne`, `$in`, `$nin`, `$regex`, `$like`, `$likeI`, `$startsWith`, `$endsWith`, `$contains`, `$elemMatch`, `$exists`, `$null`, `$type`, `$size`, `$mod`, `$between`, `$before`, `$after`, `$and`, `$or`, `$nor`, `$not`, `$all`, and `$any`.
1084
+
1085
+ ### Query Path
1086
+
1087
+ Query, vector-search, and aggregation responses include the planner decision in:
1088
+
1089
+ ```http
1090
+ x-objydb-query-path: index-full
1091
+ ```
1092
+
1093
+ <details>
1094
+ <summary><strong>Planner path values</strong></summary>
1095
+
1096
+ Read paths include `scan`, `index-full`, `index-partial`, and `ordered-index`. Vector paths are `vector-index` and `vector-filter`. Aggregate paths include `aggregate-column-summary`, `aggregate-column-scan`, `aggregate-group-index`, `aggregate-group-forward-index`, `aggregate-group-column-scan`, and `aggregate-scan`.
1097
+
1098
+ Permission-denied reads report `permission-denied`; reference queries add `reference-` or `aggregate-reference-`. A sharded response may contain multiple comma-separated paths when shards use different plans.
1099
+
1100
+ </details>
1101
+
1102
+ ## Aggregation
1103
+
1104
+ Aggregation is supplied as `$aggregate` inside the query object. Supported operators are `$sum`, `$avg`, `$min`, `$max`, and `$count`.
1105
+
1106
+ `POST /api/{db}/{collection}/search`
1107
+
1108
+ **Request**
1109
+
1110
+ ```http
1111
+ POST {URL}/api/{db}/{collection}/search
1112
+ Authorization: Bearer <accessToken>
1113
+ Content-Type: application/json
1114
+
1115
+ {
1116
+ "status": "active",
1117
+ "$aggregate": {
1118
+ "groupBy": "status",
1119
+ "fields": {
1120
+ "totalScore": { "$sum": "score" },
1121
+ "avgScore": { "$avg": "score" },
1122
+ "count": { "$count": true }
1123
+ },
1124
+ "$sort": { "totalScore": -1 },
1125
+ "$page": 1,
1126
+ "$pageSize": 20
1127
+ }
1128
+ }
1129
+ ```
1130
+
1131
+ **Response**
1132
+
1133
+ ```json
1134
+ [
1135
+ { "status": "active", "totalScore": 1234, "avgScore": 38.56, "count": 32 }
1136
+ ]
1137
+ ```
1138
+
1139
+ ## Vector Search
1140
+
1141
+ Store embeddings on `_vec`. The vector is indexed separately and hidden from normal read responses.
1142
+
1143
+ ### Insert Vector Object
1144
+
1145
+ `POST /api/{db}/{collection}/object`
1146
+
1147
+ **Request**
1148
+
1149
+ ```http
1150
+ POST {URL}/api/{db}/{collection}/object
1151
+ Authorization: Bearer <accessToken>
1152
+ Content-Type: application/json
1153
+
1154
+ {
1155
+ "_id": "doc-1",
1156
+ "title": "Hello",
1157
+ "_vec": [0.1, 0.2, 0.3]
1158
+ }
1159
+ ```
1160
+
1161
+ **Response**
1162
+
1163
+ ```json
1164
+ {
1165
+ "_id": "doc-1",
1166
+ "title": "Hello"
1167
+ }
1168
+ ```
1169
+
1170
+ ### Search Vectors
1171
+
1172
+ `POST /api/{db}/{collection}/search`
1173
+
1174
+ **Request**
1175
+
1176
+ ```http
1177
+ POST {URL}/api/{db}/{collection}/search
1178
+ Authorization: Bearer <accessToken>
1179
+ Content-Type: application/json
1180
+
1181
+ {
1182
+ "$vector": {
1183
+ "vector": [0.1, 0.2, 0.3],
1184
+ "limit": 10,
1185
+ "ef": 64
1186
+ }
1187
+ }
1188
+ ```
1189
+
1190
+ **Response**
1191
+
1192
+ ```json
1193
+ [
1194
+ { "_id": "doc-1", "title": "Hello", "_score": 0.98 }
1195
+ ]
1196
+ ```
1197
+
1198
+ If the needed vector index is still warming:
1199
+
1200
+ ```json
1201
+ {
1202
+ "error": "vector index warming",
1203
+ "indexKey": "all",
1204
+ "indexStatus": "pending"
1205
+ }
1206
+ ```
1207
+
1208
+ ## Live References
1209
+
1210
+ Reference descriptors point to another object without embedding a stored copy. `database` is optional; if omitted, the current database is used. Reads expand references by default with depth `1`; use `$refDepth: 0` to retain the descriptor only.
1211
+
1212
+ ### Store Reference
1213
+
1214
+ `POST /api/{db}/{collection}/object`
1215
+
1216
+ **Request**
1217
+
1218
+ ```http
1219
+ POST {URL}/api/{db}/{collection}/object?$refDepth=0
1220
+ Authorization: Bearer <accessToken>
1221
+ Content-Type: application/json
1222
+
1223
+ {
1224
+ "_id": "post-1",
1225
+ "title": "Hello",
1226
+ "author": {
1227
+ "type": "reference",
1228
+ "collection": "users",
1229
+ "value": "user-1"
1230
+ }
1231
+ }
1232
+ ```
1233
+
1234
+ **Response**
1235
+
1236
+ ```json
1237
+ {
1238
+ "_id": "post-1",
1239
+ "title": "Hello",
1240
+ "author": {
1241
+ "type": "reference",
1242
+ "collection": "users",
1243
+ "value": "user-1"
1244
+ }
1245
+ }
1246
+ ```
1247
+
1248
+ ### Read Expanded Reference
1249
+
1250
+ `GET /api/{db}/{collection}/object/{_id}`
1251
+
1252
+ Reads expand references by default with depth `1`.
1253
+
1254
+ **Request**
1255
+
1256
+ ```http
1257
+ GET {URL}/api/{db}/{collection}/object/post-1
1258
+ Authorization: Bearer <accessToken>
1259
+ ```
1260
+
1261
+ **Response**
1262
+
1263
+ ```json
1264
+ {
1265
+ "_id": "post-1",
1266
+ "title": "Hello",
1267
+ "author": {
1268
+ "type": "reference",
1269
+ "collection": "users",
1270
+ "value": "user-1",
1271
+ "object": { "_id": "user-1", "username": "alice" }
1272
+ }
1273
+ }
1274
+ ```
1275
+
1276
+ ### Read Unexpanded Reference
1277
+
1278
+ `GET /api/{db}/{collection}/object/{_id}`
1279
+
1280
+ **Request**
1281
+
1282
+ ```http
1283
+ GET {URL}/api/{db}/{collection}/object/post-1?$refDepth=0
1284
+ Authorization: Bearer <accessToken>
1285
+ ```
1286
+
1287
+ **Response**
1288
+
1289
+ ```json
1290
+ {
1291
+ "_id": "post-1",
1292
+ "title": "Hello",
1293
+ "author": {
1294
+ "type": "reference",
1295
+ "collection": "users",
1296
+ "value": "user-1"
1297
+ }
1298
+ }
1299
+ ```
1300
+
1301
+ ### Query Reference Path
1302
+
1303
+ `POST /api/{db}/{collection}/search`
1304
+
1305
+ **Request**
1306
+
1307
+ ```http
1308
+ POST {URL}/api/{db}/{collection}/search
1309
+ Authorization: Bearer <accessToken>
1310
+ Content-Type: application/json
1311
+
1312
+ {
1313
+ "author.object.username": "alice"
1314
+ }
1315
+ ```
1316
+
1317
+ **Response**
1318
+
1319
+ ```json
1320
+ [
1321
+ {
1322
+ "_id": "post-1",
1323
+ "title": "Hello",
1324
+ "author": {
1325
+ "type": "reference",
1326
+ "collection": "users",
1327
+ "value": "user-1",
1328
+ "object": { "_id": "user-1", "username": "alice" }
1329
+ }
1330
+ }
1331
+ ]
1332
+ ```
1333
+
1334
+ Vector search cannot currently be combined with reference object paths.
1335
+
1336
+ ## Health and Cluster Status
1337
+
1338
+ ### Health
1339
+
1340
+ The health route does not require authentication. During restore it remains available while data routes return `503` with `Retry-After: 5`.
1341
+
1342
+ `GET /health`
1343
+
1344
+ **Request**
1345
+
1346
+ ```http
1347
+ GET {URL}/health
1348
+ ```
1349
+
1350
+ **Response**
1351
+
1352
+ ```json
1353
+ {
1354
+ "ok": true,
1355
+ "node": "node-8080",
1356
+ "restore": null
1357
+ }
1358
+ ```
1359
+
1360
+ While restoring, the response also has `"restoring": true` and `restore` contains current progress.
1361
+
1362
+ ### Public Cluster Status
1363
+
1364
+ `/api/config` is the compact public status endpoint for the UI and operators. It requires a global admin token. Cluster collection is cached for one second so concurrent status requests reuse the same peer fan-out.
1365
+
1366
+ `GET /api/config`
1367
+
1368
+ **Request**
1369
+
1370
+ ```http
1371
+ GET {URL}/api/config
1372
+ Authorization: Bearer <accessToken>
1373
+ ```
1374
+
1375
+ <details>
1376
+ <summary><strong>Response example</strong></summary>
1377
+
1378
+ ```json
1379
+ {
1380
+ "ok": true,
1381
+ "generatedAt": "2026-09-18T08:41:22.000Z",
1382
+ "node": { "id": "node-8080", "url": "{URL}" },
1383
+ "rss": {
1384
+ "cluster": { "rss": 2190999552, "nodeCount": 1, "onlineNodeCount": 1 },
1385
+ "logical": { "rss": 2190999552, "nodeCount": 1, "onlineNodeCount": 1 },
1386
+ "physical": { "rss": 2190999552, "nodeCount": 1, "onlineNodeCount": 1 }
1387
+ },
1388
+ "heap": {
1389
+ "cluster": { "used": 1701040128, "total": 4294967296, "percent": 39.6 },
1390
+ "logical": { "used": 1701040128, "total": 4294967296, "percent": 39.6 },
1391
+ "physical": { "used": 1701040128, "total": 4294967296, "percent": 39.6 }
1392
+ },
1393
+ "indexes": {
1394
+ "ready": 18,
1395
+ "building": 2,
1396
+ "error": 0,
1397
+ "activeTasks": 1
1398
+ },
1399
+ "vectors": {
1400
+ "storedVectors": 4200,
1401
+ "activeIndexes": 2,
1402
+ "warmingIndexes": 0,
1403
+ "errors": 0
1404
+ },
1405
+ "compression": {
1406
+ "compressedObjects": 120000,
1407
+ "uncompressedBytes": 2899102924,
1408
+ "compressedBytes": 577136230,
1409
+ "savedPercent": 80.1
1410
+ },
1411
+ "nodes": [
1412
+ {
1413
+ "node": { "id": "node-8080", "url": "{URL}" },
1414
+ "ownedShards": ["shard-0", "shard-3"],
1415
+ "memory": { "rss": 2190999552, "heapUsed": 1701040128 },
1416
+ "replication": {
1417
+ "healthy": true,
1418
+ "lastSuccessfulSyncAt": "2026-09-18T08:41:22.000Z",
1419
+ "pendingHints": 0,
1420
+ "inflightPulls": 0,
1421
+ "error": null
1422
+ }
1423
+ }
1424
+ ],
1425
+ "shards": [
1426
+ {
1427
+ "name": "shard-0",
1428
+ "peers": [
1429
+ { "url": "{URL}", "online": true }
1430
+ ]
1431
+ }
1432
+ ]
1433
+ }
1434
+ ```
1435
+
1436
+ </details>
1437
+
1438
+ The normal response contains one shard topology, lightweight node health, logical and physical heap/RSS totals, per-node memory-guard state, and aggregate index, vector, and logical compression information. Logical totals select one reachable replica per shard; physical totals count every process. The example abbreviates fields inside the memory summaries; the live response also includes byte/MiB totals, capacity percentages, and online/offline counts.
1439
+
1440
+ `indexes.activeTasks` counts active cooperative index, column, and vector maintenance tasks in the node process.
1441
+
1442
+ `replication.healthy` means the node has no known replication failure or queued/in-flight catch-up work. It is an operational health signal, not a full data-checksum comparison between replicas.
1443
+
1444
+ Detailed request:
1445
+
1446
+ ```http
1447
+ GET {URL}/api/config?details=1
1448
+ Authorization: Bearer <accessToken>
1449
+ ```
1450
+
1451
+ The detailed response adds per-collection index, vector, and compression state plus full replication diagnostics. The admin UI requests it only when the cluster-status modal is opened. Restore progress is included only while a peer is restoring.
1452
+
1453
+ ## Responses, Errors, and Limits
1454
+
1455
+ OBJY DB keeps public responses close to the requested operation:
1456
+
1457
+ | Operation | Response shape |
1458
+ | --- | --- |
1459
+ | Single create, read, replace, or patch | One object. |
1460
+ | Query, search, vector search, or aggregation | An array. |
1461
+ | Bulk write | Count fields plus returned items unless minimal response was requested. |
1462
+ | Bulk delete | `{ deleted, ids, skipped }`. |
1463
+ | Successful delete | `204 No Content`. |
1464
+
1465
+ Errors use a JSON body containing at least `error`. Additional fields may include `code`, `operationStarted`, `writeMayHaveSucceeded`, committed import counts, restore progress, or index readiness.
1466
+
1467
+ Normal object queries default to 250 rows and cap a page at 250. JSON bodies, individual objects, bulk counts, bulk bytes, file uploads, and pending persistence pressure are bounded by server configuration. Oversized requests return `413`; temporary restore or pressure responses may include `Retry-After`.
1468
+
1469
+ NDJSON streams commit bounded batches as they are processed. A later malformed line or failed batch does not roll back earlier committed batches.