Skip to content

CSV import / export

Bulk-load records into a base collection from a CSV, or export every row as CSV — one click in the admin or one HTTP call.

For schema portability use migration snapshots. For data portability — moving records between environments, importing seed data, exporting a snapshot to a spreadsheet — use CSV.

Available on base collections only (auth and view collections aren’t exposed). Open the Records page for a collection and look at the toolbar.

GET /api/v1/admin/export/<collection> ← admin auth
→ text/csv response, served as a download

Or click Export on the Records page. The downloaded file:

  • Includes every column except password fields (always stripped — they’re Argon2 hashes, never exportable).
  • JSON-encodes object/array values (e.g. multi-select ["a","b"], file fields with multiple: true, geoPoint, json fields) into a single cell — round-trippable on import.
  • Starts with the system columns id, created and updated (Unix seconds), then the collection’s fields in schema order.
id,created,updated,title,body,published,tags
abc123,1730000000,1730000123,Hello,World,true,"[""a"",""b""]"

Excel / Google Sheets / xsv / pandas.read_csv all open this without issue. CSV escaping follows RFC 4180 (double-quote everything containing commas, quotes, or newlines; double-up quote chars inside).

POST /api/v1/admin/import/<collection> ← admin auth
Content-Type: text/csv
<csv body>
→ { "data": { "created": 42, "failed": 1, "total": 43,
"errors": [ { "row": 7, "details": { "title": "..." } } ] } }

row is the line in the file, counting the header as row 1. errors lists at most 50 rows.

Or use Import on the Records page: pick a file and it uploads straight away. A toast shows how many rows were imported; failed rows are logged to the browser console.

  1. Parse the CSV row into a record-shaped object using the header line.
  2. Run it through the standard validateRecord() pipeline — same validation rules as the records API.
  3. Create a new record with a fresh id. Import only creates records; it never updates existing ones.
  4. On error, push { row: <line>, details: <message> } into errors and continue with the next row.

Rows are inserted one by one, not in one transaction: a failing row is reported in errors and the rows around it still import.

HeaderEffect
id, created, updatedIgnored. Every row gets a new id and fresh timestamps.
Any field nameMapped to the field by exact match.
Unknown columnIgnored.

For multi-value or JSON columns, supply the JSON-encoded form just like export produces:

id,tags,metadata
,"[""a"",""b""]","{""color"":""blue""}"
{ "row": 3, "details": { "title": "title must be at least 3 characters" } }
{ "row": 7, "details": { "email": "email must be unique" } }

Validation errors are the same per-field details object the records API returns on 422; other errors are a plain message string. Fix the rows in the spreadsheet and re-upload only the failed ones.

  • No row limit — but very large imports lock the SQLite writer; chunk to ~1k rows per request if you’re loading hundreds of thousands.
  • Export is streamed: the response body is a ReadableStream that pages through listRecords in batches of 500. Memory stays bounded regardless of collection size; client disconnects stop paging via cancel().
  • Import is buffered: POST /api/v1/admin/import/:collection parses the whole file into memory before processing. Chunk huge imports manually if you’re loading hundreds of thousands of rows.
  • Files aren’t included: file-type fields hold the filename only; the actual bytes need to be uploaded separately via the Files API.

A common use case — export from prod, edit in a spreadsheet, re-import:

Terminal window
# 1. Export
curl -H "Authorization: Bearer $ADMIN" \
http://prod.example.com/api/v1/admin/export/posts > posts.csv
# 2. Edit posts.csv in Excel / Sheets / vim
# 3. Re-import (always creates new records with new ids)
curl -X POST -H "Authorization: Bearer $ADMIN" \
-H "Content-Type: text/csv" \
--data-binary @posts.csv \
http://staging.example.com/api/v1/admin/import/posts

Import can’t update existing records: re-importing an export creates a copy of every row, and rows that hit a unique field fail. To change existing records in bulk, use the Batch API with PATCH operations.