Skip to content
Open beta — everything’s free right now, and your rate is locked when it ends.

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 stable rec_ id in the first column.
  • Attachments are comma-joined attachment ids (att_tfhj23qqkanc) — join them to attachments.csv for 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 bare 2026-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 (10 means 10%); duration is 2:00 as 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."

Tabla is the database we build these on.

A no-code database with real Postgres underneath: every feature on every plan, a million records per database, and your whole database back out in one file, any day.

More guides