Inside the export zip: manifest, CSVs, attachments
The reference for Tabla's export format: manifest.json carries the exact structure and every saved view, one CSV per table carries the records, comments come as their own CSV, attachments arrive as real files.
Updated August 2026
Export a database and you get one zip. This page documents what's in it, from an actual export of the demo database — enough detail to rebuild the thing anywhere, which is the standard an export format should be held to.
The layout:
Sakkara Plants-export.zip
├── manifest.json the structure, exactly
├── Plants.csv one CSV per table, named after it
├── Orders.csv
├── Customers.csv
├── Suppliers.csv
├── نباتات.csv table names survive, Arabic included
├── comments.csv every comment on every record
├── attachments.csv the file index
├── attachments/ the actual bytes
│ └── att_tfhj23qqkanc-monstera.png
└── README.txt the layout, explained in prose
Two tables sharing a name get numbered files ("Plants (2).csv"); characters no filesystem tolerates become dashes.
manifest.json is the structure
The manifest is the machine-readable half: every table, every field, every setting. From the real file, trimmed to one table and five fields:
{
"exported_at": "2026-08-09T23:30:41.989Z",
"database": { "id": "db_rci9p7iggin5", "name": "Sakkara Plants" },
"tables": [
{
"id": "tbl_jukzu86nxtcj",
"name": "Plants",
"display_field_id": "fld_rume7j84a473",
"fields": [
{ "id": "fld_rume7j84a473", "name": "Name",
"storage_kind": "text", "costume": "single_line_text",
"options": {} },
{ "id": "fld_t8y7aqpv8ykt", "name": "Price",
"storage_kind": "number", "costume": "currency",
"options": { "symbol": "$", "decimals": 0 } },
{ "id": "fld_txz7eiyxd8r2", "name": "Light",
"storage_kind": "single_select", "costume": "single_select",
"options": { "choices": [
{ "id": "rh4ve39e", "name": "Full sun", "color": "saffron" },
{ "id": "nntthbzz", "name": "Bright indirect", "color": "nile" }
] } },
{ "id": "fld_pj5bpeebk8a5", "name": "Supplier",
"storage_kind": "link", "costume": "link",
"options": { "direction": "forward",
"pair_field_id": "fld_d4d98hfhed8b",
"allow_multiple": false,
"target_table_id": "tbl_6fn9jer8qrwu" } },
{ "id": "fld_pzd6ggm4fxa5", "name": "Supplier email",
"storage_kind": "text", "costume": "lookup_text",
"options": { "link_field_id": "fld_pj5bpeebk8a5",
"target_field_id": "fld_6pnusgxgqmg7",
"source_presentation": { "costume": "email", "options": {} } } }
]
}
]
}
Each table also carries a views array beside its fields, one entry per
saved view:
"views": [
{ "id": "viw_…", "name": "Board · by status", "type": "board",
"config": { "type": "board", "filters": { … }, "sorts": [ … ],
"hidden_fields": [ … ], "field_order": [ … ],
"group_field_id": null } }
]
type is lifted out of the config for convenience and reads grid when the
config doesn't say otherwise; config is the view's own settings written
whole, which is why it holds the layout's extras — a board's stacking field,
a calendar's date field, row height, column widths and color rules — without
this page having to keep a list of them in step with the app.
Reading it: storage_kind is how a value is stored, and costume is the
field type as the app presents it — the pair is why currency and percent
can both live on plain number storage. options holds each type's
settings by their real names: select choices with stable ids and colors,
a link's target_table_id and its pair_field_id (the mirror on the far
side), a lookup's link_field_id and target_field_id, a formula's
expression. The display field is named per
table. Every id here — tbl_, fld_, choice ids — matches what the
API uses.
One CSV per table
UTF-8 with a byte-order mark, so Excel opens it with accents and
Arabic intact instead of mojibake; CRLF line endings; first column id,
then one column per field in grid order. A real row from Orders.csv:
id,Name,Customer,Plants,Status,Placed,Deliver by,Assigned to,Total,Discount,Paid,Delivery window,Notes,To collect,Customer email,Items,Send invoice
rec_ff374pj8sbhe,ORD-1041,Nour El-Sayed,"Monstera deliciosa,Golden pothos",Delivered,2026-07-28T10:24:00.000Z,2026-07-31,laila@sakkaraplants.com,63,10,true,2:00,Ribbon wrap on the monstera — housewarming gift.,56.7,nour.elsayed@example.com,2,
Cell shapes, column by column:
- Linked records (
Customer,Plants) are written as the linked records' display values, comma-joined —"Monstera deliciosa,Golden pothos". Readable anywhere; the exact wiring lives in the manifest. The one ambiguity: two linked records sharing a display value are indistinguishable in the CSV cell. Every row still carries its own stablerec_id in the first column. - Attachments are comma-joined attachment ids (
att_tfhj23qqkanc) — join them toattachments.csvfor filenames and paths. - Dates with time are UTC instants, ISO 8601 with
Z— the same value the API returns, unshifted by the database timezone. Date-only columns are bare2026-07-31. - Multi select joins with comma-space (
Air-purifying, Beginner-friendly) — the same delimiter the importer splits on. - Checkbox is
true/false; percent is the plain number (10means 10%); duration is2:00as the grid shows it; member cells carry the member's email. - Formula, lookup and rollup columns (
To collect,Customer email,Items) hold their computed values as of export time —56.7— since the manifest already carries the live wiring. Button columns export empty.
One safety detail: a cell that a spreadsheet would execute as a formula —
starting with =, @, or a sign that isn't a number — is prefixed with an
apostrophe on the way out. Tabla's own importer
strips it back off, so +20 100 123 4567 round-trips unchanged.
comments.csv
One row per comment, oldest first, across every table in the database:
id,table,record_id,author,author_id,created_at,edited,body
table is the table's name and record_id the record it hangs off, so a
join back to that table's CSV on id puts every note beside its row.
author is the person's name as the record panel shows it, falling back to
Former member for somebody since removed, and author_id is their usr_
— which is what resolves an @[usr_…] mention inside a body. edited is
true/false.
Two things are deliberately absent. A deleted comment isn't here: the delete has a ten-second undo and then the row is gone from Tabla, so this file doesn't put it back. And a viewer's zip has no comments.csv at all, because a viewer can't read comments in the app either; the README stops mentioning the file rather than promising one that isn't there.
attachments.csv and the bytes
id,record_id,filename,size_bytes,path
att_tfhj23qqkanc,rec_y4crk8w9hc3e,monstera.png,31176,attachments/att_tfhj23qqkanc-monstera.png
The files in attachments/ are the original bytes, named
<id>-<filename> so collisions can't happen. Two honest edges. One export
inlines up to the account's whole storage ceiling — 100 GB, per
limits — so an account inside its limit gets every file;
past it, files are written oldest first until the budget is spent and the
rest keep an empty path, counted by name in the README. And a file missing
from storage (possible only after a hand-restore) exports as a named gap
rather than a broken zip.
Export the same unchanged database twice and you get the same zip: the file order is fixed, so the same attachments come out inline both times.
Rebuilding elsewhere
The zip is deliberately sufficient: create tables from the manifest, create
fields from each entry's type and options, load the CSVs keying on id,
and re-wire links using target_table_id plus the display values (or your
own join on the id columns). Computed columns come back to life by
re-creating them from the manifest's expressions rather than importing
their frozen values. The README in the zip closes with the export's own
contract, and it is the right sentence to end on here: "Nothing is lost
between what's in Tabla and what's in this zip."