@@ -69,7 +69,7 @@ GET /api/collections/:owner/:slug/versions/latest → latest version with
6969GET /api/collections/:owner/:slug/versions/:n → specific version by number
7070
7171### Read records and files
72- GET /api/collections/:owner/:slug/versions/:n/records → records for a version (?type=TypeName&limit=100&offset=0 )
72+ GET /api/collections/:owner/:slug/versions/:n/records → records for a version (?type=TypeName&limit=100&after=recordId )
7373GET /api/collections/:owner/:slug/versions/:n/manifest → lightweight manifest: record ids/types + file hashes
7474GET /api/collections/:owner/:slug/files/:hash → download a file by hash
7575HEAD /api/collections/:owner/:slug/files/:hash → check if a file exists (returns Content-Length, Content-Type)
@@ -185,6 +185,119 @@ Set base_version to null. Put all records in changes.added. Include schemas for
185185
186186---
187187
188+ ## Chunked Push: Large Uploads
189+
190+ For pushes exceeding 100MB (or millions of records), use the chunked upload protocol. This avoids
191+ body size limits and memory pressure by streaming changes in batches.
192+
193+ ### Step 1: Start an upload session
194+ POST /api/collections/:owner/:slug/versions/upload
195+ Authorization: Bearer ul_<key>
196+
197+ {
198+ "base_version": 3,
199+ "message": "Bulk import 2M records",
200+ "app_id": "my-app",
201+ "schemas": { ... }
202+ }
203+
204+ Response (201):
205+ {
206+ "sessionId": "uuid-of-session",
207+ "expiresAt": "2026-04-30T01:00:00.000Z"
208+ }
209+
210+ Sessions expire after 1 hour.
211+
212+ ### Step 2: Append batches (repeat as needed)
213+ PUT /api/collections/:owner/:slug/versions/upload/:sessionId
214+ Authorization: Bearer ul_<key>
215+
216+ {
217+ "changes": {
218+ "added": [ ...up to 10,000 records... ],
219+ "updated": [ ... ],
220+ "removed": ["id-1", "id-2"]
221+ }
222+ }
223+
224+ Response:
225+ {
226+ "received": { "added": 5000, "updated": 0, "removed": 0 },
227+ "totalStaged": 15000
228+ }
229+
230+ Each batch can contain up to 10,000 records. Call this endpoint as many times as needed.
231+ If a record ID is sent more than once, the last write wins (upsert semantics).
232+
233+ ### Step 3: Finalize the version
234+ POST /api/collections/:owner/:slug/versions/upload/:sessionId/finalize
235+ Authorization: Bearer ul_<key>
236+
237+ Response (201): same as regular push
238+ {
239+ "version": 4,
240+ "semver": "v1.3.0",
241+ "hash": "...",
242+ "recordCount": 2000000,
243+ "fileCount": 5
244+ }
245+
246+ Finalize applies all staged changes to the base version, validates against schemas,
247+ computes hashes, and creates the new immutable version.
248+
249+ ### Errors during finalize
250+ - 409 Version conflict: someone pushed since your base_version. Start a new session.
251+ - 422 Schema validation failed: one or more records don't match. Fix and re-upload.
252+ - 422 Missing files: records reference files not yet uploaded. Upload them and retry finalize.
253+ - 410 Session expired: the 1-hour window elapsed. Start a new session.
254+
255+ ### Cancel a session
256+ DELETE /api/collections/:owner/:slug/versions/upload/:sessionId
257+ Response: 204 (staged records are discarded)
258+
259+ ### Check session status
260+ GET /api/collections/:owner/:slug/versions/upload/:sessionId
261+ Response: { sessionId, status, recordCount, baseVersion, expiresAt, createdAt }
262+
263+ Status values: open, finalizing, completed, failed, expired.
264+
265+ Note: On successful finalize, the session and all staged records are deleted from the database.
266+ There is no need to manually clean up completed sessions.
267+
268+ ---
269+
270+ ## Pagination (Records Endpoint)
271+
272+ The records endpoint uses cursor-based pagination for efficient traversal of large collections.
273+
274+ GET /api/collections/:owner/:slug/versions/:n/records?limit=100&after=record-id-42
275+
276+ Response:
277+ {
278+ "records": [ ...up to `limit` records... ],
279+ "pagination": {
280+ "limit": 100,
281+ "hasMore": true,
282+ "nextCursor": "record-id-142",
283+ "total": 2000000
284+ }
285+ }
286+
287+ Parameters:
288+ - limit: max records per page (default 100, max 1000)
289+ - after: cursor (record ID) — return records with IDs lexicographically after this value
290+ - offset: legacy offset-based pagination (still supported, but cursor is preferred for large sets)
291+ - type: filter by record type
292+
293+ To paginate through all records:
294+ 1. First request: GET .../records?limit=1000
295+ 2. If pagination.hasMore is true, use pagination.nextCursor for the next request:
296+ GET .../records?limit=1000&after=<nextCursor>
297+ 3. Repeat until hasMore is false.
298+
299+ ---
300+
188301## Record Format
189302
190303{
0 commit comments