# How to Migrate Airtable to PostgreSQL: A Step-by-Step Guide

Moving an Airtable base to PostgreSQL is far less scary than it sounds — provided you do it in the right order and treat the relationships, not the rows, as the hard part. This guide walks through the whole thing: mapping your schema, turning linked records into foreign keys, handling rollups and attachments, validating the result, and cutting over without taking your team offline.

## Key takeaways
- The data is the easy part. The real work is rebuilding relationships, rollups, and automations correctly.
- Linked records become foreign keys (one-to-many) or junction tables (many-to-many).
- Rollups and lookups become SQL joins, aggregates, or materialised views — computed on demand, not stored.
- Attachments are files: copy them to object storage and store the URL in Postgres.
- Validate row counts and spot-check values on both sides before you cut over.

## Before you start: map the territory
Resist the urge to export a CSV and start importing. First, inventory what you actually have. For each table, list the field types, note every linked-record relationship, and flag the rollups, lookups, and formulas that depend on those links. This map is what turns a chaotic migration into a checklist. It also surfaces the question worth answering up front: which fields hold *real* data and which are just derived values you can recompute later?

## Step 1 — Design the relational schema
Each Airtable table becomes a Postgres table. Each field becomes a column with a real type: `text`, `numeric`, `boolean`, `date`, `timestamptz`, and so on. Single selects map naturally to a text column (or a Postgres `enum` if the options are fixed); multi-selects map to a `text[]` array or a related lookup table. Give every table a real primary key — keep the Airtable record ID in a column so you can trace records back during validation.

## Step 2 — Convert linked records into foreign keys
This is the heart of the migration and the thing CSV exports get wrong. Airtable's linked records are relationships, and a relational database models relationships explicitly:

- **One-to-many** (e.g. a Project links to many Tasks): add a foreign key column on the "many" side — `tasks.project_id` referencing `projects.id`.
- **Many-to-many** (e.g. Authors link to many Books and vice versa): create a junction table — `authors_books` — holding one row per relationship with both foreign keys.

Done right, this is a strict upgrade: foreign keys enforce referential integrity, so you can no longer end up with a task pointing at a project that does not exist.

> Watch out for: Airtable lets a single linked-record field point at multiple records without declaring a direction. Decide explicitly whether each link is one-to-many or many-to-many before you build the schema — getting this wrong is the most common source of rework.

## Step 3 — Rebuild rollups, lookups, and formulas
In Airtable these fields store a recalculated value. In Postgres you usually do not store them at all — you derive them when you query, which keeps them always-correct and removes the recalculation cost:

- **Lookups** become a `JOIN` back to the related table.
- **Rollups** (sum, count, max of linked records) become an aggregate query — `SUM(...)`, `COUNT(...)` with a `GROUP BY`.
- **Formulas** become a `GENERATED` column for simple per-row math, or a computed expression in the query for anything involving other rows.

If a derived value is read constantly and expensive to compute, wrap it in a **materialised view** so it is precomputed and refreshed on a schedule — the best of both worlds.

## Step 4 — Move attachments to object storage
Attachments are not database values; they are files that happen to be referenced from a cell. The standard pattern is to copy each file into object storage (such as Amazon S3 or a compatible bucket) and store its URL in a Postgres `text` column. That keeps the database lean and serves files efficiently. AT Migrator moves attachments as part of the migration, or writes them to storage you control with the self-hosted option.

## Step 5 — Migrate the data and validate
With the schema in place, load the data table by table, parents before children so foreign keys resolve. Then — before anyone trusts the new database — validate:

- **Row counts match** on every table, source versus target.
- **Relationships resolve** — no orphaned foreign keys, junction rows line up with both parents.
- **Spot-check values**, especially dates, numbers with precision, and anything that passed through a formula.

This validation step is where hand-rolled scripts tend to fall down and where dedicated tooling earns its keep — AT Migrator runs these checks automatically and reports any mismatch before you rely on the data.

## Step 6 — Cut over without downtime
Two patterns cover almost every team:

| | Freeze & cutover | Dual-write |
| --- | --- | --- |
| How it works | Pause edits, run final migration, validate, switch apps to Postgres | Write to Airtable and Postgres simultaneously, then cut over once at parity |
| Downtime | A short, planned freeze window | None |
| Complexity | Low — best for most teams | Higher — needs sync code |
| Best for | Internal tools, business hours edits | Always-on production systems |

For the majority of bases, a short freeze over a quiet evening is more than enough. Only reach for dual-write when the system genuinely cannot pause.

## How long does it take?
The data and schema move in minutes with the right tooling. The *project* timeline depends on everything attached to the base. A clean single base can be done and validated in a day. A multi-base workspace with heavy automations and integrations is usually a one-to-four week effort — and most of that is rebuilding automations and testing, not moving rows.

## FAQ
**How long does an Airtable to PostgreSQL migration take?** A single base with clean schema and a few thousand rows can be migrated and validated in a day. A multi-base workspace with linked records, attachments, and downstream automations is usually a one-to-four week project, most of which is rebuilding automations and testing — not moving the data itself.

**How are Airtable linked records converted in PostgreSQL?** Each linked-record field becomes a foreign key relationship. A one-to-many link maps to a foreign key column on the child table; a many-to-many link maps to a junction (join) table holding both record IDs.

**What happens to rollups, lookups, and formula fields?** Lookups and rollups become SQL joins and aggregate queries (or materialised views if you want them precomputed). Formula fields become generated columns or are computed at query time.

**Can I migrate Airtable attachments to PostgreSQL?** Yes. Attachments are files, so the standard pattern is to copy each file into object storage (such as S3) and store its URL in a Postgres column.

**How do I migrate without downtime?** Use a dual-write or freeze-and-cutover approach. For most teams a short freeze works: pause edits, run the final migration, validate row counts, then point applications at Postgres.

---
*Considering a migration? Book a free discovery call with AT Migrator: https://www.atmigrator.com*
