Disclosure: this article was written with the help of AI and checked against Airtable's docs and API before publishing.
Airtable keeps your data safe on its side, but that is not the same as having your own copy. A deleted table, a bad bulk edit or a lost workspace login can still ruin your week. Snapshots and the trash help for a limited time. A copy you hold yourself does not expire.
A complete backup of a base has three parts, and most people only save the first:
- Records: the data in every table, not just one view.
- Schema: the table structure, field types, select options, formulas and linked tables. Without it, a pile of CSVs is hard to rebuild.
- Attachments: the actual files. A CSV only contains links to them.
Here are three ways to do this for free, from quick to automated.
1. The manual way: download CSV per view
In Airtable, open a view, click the view name, then Download CSV. It works, but has limits:
- It exports one view at a time. If the view has filters or hidden fields, the CSV has them too, so you can silently miss records or columns.
- You have to repeat it for every table and every time you want a fresh copy.
- Attachments come out as URLs, not files.
- Nothing is saved about the structure: no field types, no formulas, no select options.
For a one-table base you check once a month, this is fine. For anything bigger, use one of the next options.
2. A script you control
If you prefer to own the whole process, the Airtable REST API is simple. First, create a read-only personal access token at airtable.com/create/tokens. Give it the scopes data.records:read and schema.bases:read, and add only the base you want to back up. Delete the token when you are done. Three facts matter:
- A list request returns at most 100 records per page. If the response contains an
offset, send it back to get the next page. - Airtable allows about 5 requests per second per base, so pause between pages.
- Attachment URLs expire after about two hours. Download the files in the same run that fetched the records. A URL saved in last month's CSV will not work anymore.
This Node.js script (version 18 or newer, no dependencies) saves the schema, every table as JSON, and every attachment:
// backup.mjs: AIRTABLE_TOKEN=pat... BASE_ID=app... node backup.mjs
import { mkdir, writeFile } from "node:fs/promises";
const { AIRTABLE_TOKEN: token, BASE_ID: base } = process.env;
const api = "https://api.airtable.com/v0";
const headers = { Authorization: `Bearer ${token}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function get(url) {
const res = await fetch(url, { headers });
if (res.status === 429) { await sleep(30000); return get(url); }
if (!res.ok) throw new Error(`${res.status} ${url}`);
return res.json();
}
const stamp = new Date().toISOString().slice(0, 10);
const dir = `backup-${stamp}`;
await mkdir(`${dir}/files`, { recursive: true });
// 1. Schema
const schema = await get(`${api}/meta/bases/${base}/tables`);
await writeFile(`${dir}/schema.json`, JSON.stringify(schema, null, 2));
// 2. Records, page by page
for (const table of schema.tables) {
const records = [];
let offset;
do {
const qs = new URLSearchParams({ pageSize: "100" });
if (offset) qs.set("offset", offset);
const page = await get(`${api}/${base}/${table.id}?${qs}`);
records.push(...page.records);
offset = page.offset;
await sleep(250);
} while (offset);
await writeFile(`${dir}/${table.name}.json`, JSON.stringify(records, null, 2));
// 3. Attachments, right away while the URLs are valid
for (const rec of records) {
for (const value of Object.values(rec.fields)) {
if (!Array.isArray(value)) continue;
for (const f of value) {
if (!f?.url || !f?.filename) continue;
const file = await fetch(f.url);
if (!file.ok) { console.warn("skipped", f.filename); continue; }
await writeFile(`${dir}/files/${rec.id}-${f.id}-${f.filename}`,
Buffer.from(await file.arrayBuffer()));
}
}
}
console.log(`${table.name}: ${records.length} records`);
}
Two notes. The JSON keeps linked records, lookups and multi-selects exactly as Airtable returns them, which is more faithful than CSV. If you want CSV for spreadsheets, convert the JSON afterwards. And the script lives on your machine, so the token never leaves it. Keep the token in an environment variable, not in the file.
3. Make it happen on a schedule
A backup you have to remember is a backup that stops. Some options for automating it:
- Cron or a scheduled GitHub Action running the script above, committing or uploading the output to storage you control. The most flexible option, and you maintain it.
- An Airtable automation with a scheduled trigger that runs a script or sends a webhook. It works inside Airtable's own limits, and the free plan has few automation runs.
- Make or Zapier with a schedule trigger that lists records and writes them to Drive or Dropbox. Fine for small bases, but watch the operation counts.
- Paid add-ons and third-party export or backup services can run scheduled exports for you, usually with a free tier or a subscription. Check what they cover: many export views or single tables rather than a full base with attachments, so a script like the one above is still the most complete option.
A sensible routine
- Once now: run the backup script and store the output somewhere that is not Airtable.
- Weekly or monthly: a scheduled run of the script for the tables that change.
- Before big changes: run the script again. A fresh copy takes minutes.
- Test a restore once. Open a few of the JSON files, check that linked records and dates look right, and check that the schema file tells you what each field was.
The one habit that matters is keeping the copy outside the system it is backing up.
Top comments (0)