# Export

Download one table, or every table in a workspace, as CSV or JSON. An export holds exactly the rows and fields you can read, the same ones a query in ADoor, the API, or MCP returns to you.

## In ADoor

- One table: open the table, choose the three-dot menu, then Export CSV or Export JSON.
- Everything: on Spaces, choose Export. Whole workspace downloads every table you can read. The same sheet has CSV and JSON for each table.

ADoor checks the export first, so a table you cannot read or a busy export shows a message. Then your browser saves the file while ADoor writes it. A large table does not have to fit in the page or in memory.

## CSV

CSV follows [RFC 4180](https://www.rfc-editor.org/rfc/rfc4180):

- UTF-8 without a byte order mark. Fields are separated by commas and rows end with CRLF.
- The first row is the header: the field names, in table order. An empty table is just the header.
- A field that contains a comma, a double quote, a carriage return or a line break is wrapped in double quotes, and a double quote inside it is doubled. A line break inside a value stays inside its quotes.
- No value (SQL `NULL`) is an empty field: `a,,c`. Empty text is two quotes: `a,"",c`.
- Text, ids, dates, timestamps, `bigint` and `numeric` are written as PostgreSQL stores them, for example `2026-10-28 18:40:02.316688+00` and `1200.50`. Booleans are `true` and `false`. `json` and `jsonb` are their JSON text. Arrays are JSON arrays such as `["a","b,c",null]`, which `COPY ... FROM` reads back.
- A value whose first character is `=`, `+`, `-`, `@`, a tab or a carriage return gets one leading apostrophe, so `=SUM(A1)` is written `'=SUM(A1)`. Excel, LibreOffice and Google Sheets then show it as text instead of running it as a formula. A plain number such as `-5` or `+1.5` is left as it is. This changes the text of those cells. Use JSON when you need an exact copy.

To open a CSV with accented or non-Latin text in Excel, use Data, From Text/CSV, and choose UTF-8.

A whole-workspace CSV export is one ZIP file with one CSV per table, named after the table, for example `contacts.csv`.

## JSON

A JSON export is one document with each table's schema and rows. A table export holds one table; a workspace export holds every table you can read.

```json
{
  "format": "adoor-export-v1",
  "exportedAt": "2026-10-28T18:40:02.316Z",
  "scope": "table",
  "workspace": { "id": "3e46…", "name": "Field Studio" },
  "access": "Rows and fields this account could read when the export ran.",
  "tables": [
    {
      "id": "f00f…",
      "name": "contacts",
      "space": { "id": "7510…", "name": "General" },
      "columns": [
        { "name": "id", "type": "uuid", "nullable": false, "primaryKey": true },
        { "name": "name", "type": "text", "nullable": true, "primaryKey": false },
        { "name": "company_id", "type": "uuid", "nullable": true, "primaryKey": false }
      ],
      "references": [{ "column": "company_id", "table": "companies", "targetColumn": "id" }],
      "rows": [
        { "id": "07a8…", "name": "Sam Okonkwo", "company_id": "5c95…" }
      ],
      "rowCount": 1
    }
  ]
}
```

| Field type | In JSON |
| --- | --- |
| `text`, `uuid` | string |
| `date`, `timestamptz` | string, as PostgreSQL prints it |
| `integer` | number |
| `bigint`, `numeric` | string, so no digit or trailing zero is lost |
| `boolean` | `true` or `false` |
| `json`, `jsonb` | the JSON value itself, with large numbers kept digit for digit |
| arrays | a JSON array of the same forms |
| no value | `null` |

Each row is an object keyed by field name. A relation is a `references` entry: the row stores the referenced row's key, and the entry names the table and field it points to. JSON has no formula guard; text is exactly as stored.

## What is included

Anyone who can read a table can export it. There is no separate export permission: an export is a read, checked exactly like every query, and never wider than what you can read. It is recorded the same way as any other read, with no Activity entry of its own.

- Viewer, editor and owner can each read every table, so each can export every table. A viewer still cannot change anything.
- A custom grant exports only its tables. A field the grant cannot read is left out of the file, not blanked. A row rule, such as `team = "red"`, limits the rows.
- A table you cannot read is not found, the same answer as for a table that does not exist. Someone who is not a member of the workspace cannot export from it.
- A connection key exports within that key's grant.
- A workspace export holds every table you can read and leaves out the ones you cannot. ADoor's system tables, such as `adoor_users`, and undo history are not included. A system table you can read can still be exported on its own.

Access is checked again for every page of rows. If access is removed, a connection is revoked, or the sign-in ends while an export runs, the export stops at the next page. The download then fails instead of ending early, so a cut-off file is not mistaken for a complete one.

An export is not a point-in-time snapshot. Each row is written at most once, but a row added or removed while a long export runs may or may not be in the file.

## Limits

- Two exports run at once for one person or key, two for one workspace, and four on one server. One more gets `429 EXPORT_BUSY` with `Retry-After: 15`. Ordinary queries keep working meanwhile.
- Rows are read in pages of up to 1000, fewer when rows are wide. Each page is an ordinary read: it waits its turn behind other queries in the workspace, holds one database connection only while it runs, and has the normal statement time limit.
- A single row larger than 1 MiB cannot be read, here or in a query, and stops the export.
- There is no size cap on the file. A client that reads nothing for two minutes is disconnected.
- A table is paged by its key. When your access does not include the table's key field, rows are paged by position instead, which is slower on very large tables.

## API

The website uses the same HTTP endpoints. Send a connection key as `Authorization: Bearer <key>`.

| Request | Returns |
| --- | --- |
| `GET /api/export/tables/<table>?format=csv` | One table as CSV |
| `GET /api/export/tables/<table>?format=json` | One table as a JSON document |
| `GET /api/export/workspace?format=csv` | A ZIP of one CSV per table |
| `GET /api/export/workspace?format=json` | Every table in one JSON document |

Optional parameters: `workspace` (id or slug, when the key can open that workspace), `space` and `id` (pin the table's space and id; a different table under that name is `409 TABLE_IDENTITY_CHANGED`), and `preflight=1`, which answers with the file name and table list without reading rows. Responses set `Content-Disposition: attachment` with a safe file name and `Cache-Control: no-store`.

```sh
curl -fSL -o contacts.csv \
  -H "Authorization: Bearer $ADOOR_KEY" \
  'https://app.adoor.run/api/export/tables/contacts?format=csv'
```

`-f` makes curl fail on a refusal. A download cut off by revoked access also makes curl exit with an error.

## MCP

MCP tool results come back inline, so there is no MCP tool that returns these files. An assistant has two ways to export:

- Run `COPY contacts TO 'adoor://new' WITH (FORMAT csv, HEADER true)` with the `query` tool. The result has a `download_url` for that file. The file is deleted once it has been downloaded, or six hours after it was made. Include `HEADER true` for a header row: PostgreSQL's `COPY` writes none by default. `COPY (SELECT ...) TO` exports the rows a query returns. That file has no formula guard.
- Call the HTTP endpoints above with a connection key.

The MCP `export` tool is a separate whole-workspace snapshot that includes undo history. Its own requirements do not apply to the CSV and JSON export on this page, which needs only read access.
