Migration guides

How to verify an Airtable to PostgreSQL migration with our sample base

8 min read

Discuss this article with AI

Opens your AI tool with a prompt about this article. It can read the live page and answer your questions.

The fastest way to trust an Airtable → PostgreSQL migration is not a sales deck — it is opening the source base, loading the migrated SQL, and running the same checks you would on your own data. This guide walks through exactly that using AT Migrator's public sample base and the PostgreSQL seed we generated from it.

Why verify with a real sample?

CSV exports and sync demos hide the hard parts: linked-record cardinality, junction tables, formula recreation, and row-level fidelity at scale. A purpose-built migration should preserve relationships as foreign keys and rebuild computed fields as live SQL. The sample base (48,000+ records) lets you confirm that before committing to your own base.

Step 1 — Open the sample Airtable base

The public base is read-only. Browse the tables, linked records, and views the same way you would in your own workspace.

Open the sample Airtable base →

Note table names, record counts per table, and a few linked-record pairs you can spot-check later in Postgres. The CRM base includes companies, contacts, deals, order_line_items, with many-to-many links stored in junction tables named __lnk_*.

Step 2 — Download the migrated SQL seed

The seed file contains the PostgreSQL schema and data AT Migrator generated from that base — tables, foreign keys, junction tables where needed, and COPY data in standard pg_dump format.

Download at-migrator-sample-migration.sql

Step 3 — Import into Supabase

  1. Create a new Supabase project (or use an existing dev project).
  2. Connect with psql using your project's connection string (Direct or pooler), then run: psql <connection-string> -f at-migrator-sample-migration.sql
  3. Alternatively, paste sections in the SQL Editor for spot checks — not the full ~85k-line file.
  4. Confirm tables appear under Table Editor: companies, contacts, deals, order_line_items.

Step 4 — Import into Neon

  1. Create a Neon project and copy the connection string.
  2. Open the Neon SQL Editor, or connect with psql.
  3. Run the seed file: psql <connection-string> -f at-migrator-sample-migration.sql

Step 5 — Import locally (Supabase CLI or plain Postgres)

For a local Postgres instance or Supabase local dev:

# Plain Postgres
createdb sample_migration
psql sample_migration -f at-migrator-sample-migration.sql

# Supabase local (after supabase start)
psql postgresql://postgres:postgres@localhost:54322/postgres \
  -f at-migrator-sample-migration.sql

Step 6 — Compare Airtable and Postgres

Run these checks before you trust any migration — sample or production:

Row counts

For each main table, confirm the record count in Airtable matches Postgres:

SELECT 'companies' AS table_name, COUNT(*) FROM public.companies
UNION ALL SELECT 'contacts', COUNT(*) FROM public.contacts
UNION ALL SELECT 'deals', COUNT(*) FROM public.deals
UNION ALL SELECT 'order_line_items', COUNT(*) FROM public.order_line_items;

Junction table integrity

Many-to-many links land in __lnk_* junction tables. Every link row should resolve to both sides:

SELECT COUNT(*) AS orphaned_company_links
FROM public.__lnk_companies__contacts l
LEFT JOIN public.companies c ON c._at_id = l.companies_id
WHERE c._at_id IS NULL;

A result of 0 means no orphaned company links in the companies ↔ contacts junction table.

Spot-check linked records

Pick one Airtable record with linked children. Find the same record in Postgres via the _at_id column (the original Airtable record ID — not the integer id primary key) and confirm related rows in __lnk_companies__contacts match.

Formulas, rollups, and lookups — the vt_ views

In Airtable, formula, rollup, and lookup fields recalculate whenever underlying data changes. AT Migrator does not copy those values as static columns into the base tables — they would go stale the moment a linked record or source field changed. Instead, each table gets a companion view named vt_[table] (view table) that mirrors the Airtable grid: every stored column from the base table, plus computed fields rebuilt as SQL.

For this CRM sample, query these views — not the raw tables — when comparing computed fields to Airtable:

  • vt_companies
  • vt_contacts
  • vt_deals
  • vt_order_line_items

What lives where:

  • Base tables (companies, contacts, etc.) — migrated source data only: text, numbers, dates, attachments (jsonb), and foreign-key-style link columns.
  • vt_ views — base columns plus formulas (e.g. line_total, weighted_value, health_label), rollups (e.g. of_contacts, line_item_total), lookups (e.g. deal_stage), and linked-record display fields (e.g. linked_contacts).

Verify a formula field

Pick a record in Airtable's Order Line Items table and note its record ID and Line Total value. In Postgres, query the view — the formula is exposed as line_total:

SELECT
  _at_id,
  quantity,
  unit_price,
  discount,
  line_total          -- formula: qty × price × (1 − discount)
FROM public.vt_order_line_items
WHERE _at_id = '<airtable-record-id>'
LIMIT 1;

Replace <airtable-record-id> with the Airtable ID (e.g. rec0…). The value should match what you see in the base — it is computed as quantity × unit_price × (1 − discount).

Verify a rollup field

Pick a Deals record with linked order line items. Compare Airtable's Line Items count and Line Item Total rollup to the view:

SELECT
  _at_id,
  deal_name,
  line_items,          -- rollup: count of linked order line items
  line_item_total,     -- rollup: sum of line_total on linked items
  weighted_value       -- formula: amount × probability
FROM public.vt_deals
WHERE _at_id = '<airtable-record-id>'
LIMIT 1;

line_items is a count of linked rows; line_item_total sums their line_total values. weighted_value is a separate formula ( amount × probability) you can check on the same row.

Repeat for other tables as needed — e.g. of_contacts and total_deal_value on vt_companies, or company_industry (lookup) on vt_contacts. If computed values match on a handful of spot-checked records, the migration logic is working; static sync tools would pass row-count checks but fail when you change a source field and re-query.

Attachments — files in your bucket, metadata in Postgres

Airtable attachment fields are files, not database values. AT Migrator handles them in two steps during migration:

  1. Transfer the file — each attachment is downloaded from Airtable's CDN and uploaded to your S3-compatible bucket (you connect storage during setup; we do not host the files).
  2. Store metadata in Postgres — the attachment column becomes jsonb on the base table. Each file is one object in a JSON array with key (path in your bucket), url (full fetch URL), filename, size (bytes), and mimetype.

In this CRM sample, the Companies table has a logo attachment field. After import, inspect it on the base table — attachments are not recomputed in vt_ views; they are stored data on companies.logo (and passed through unchanged on vt_companies).

Verify attachment metadata

Pick a company in Airtable that has a Logo attachment. Note the filename and file size. In Postgres, query the same record by _at_id:

SELECT
  _at_id,
  name,
  logo->0->>'filename'   AS filename,
  (logo->0->>'size')::int AS size_bytes,
  logo->0->>'mimetype'   AS mimetype,
  logo->0->>'key'        AS storage_key,
  logo->0->>'url'        AS file_url
FROM public.companies
WHERE _at_id = '<airtable-record-id>';

filename and size_bytes should match Airtable. Multi-file attachment fields use a longer array — use logo->1, logo->2, etc. for additional files.

Verify attachment counts

Compare how many companies have a logo in Airtable vs Postgres:

SELECT COUNT(*) AS companies_with_logo
FROM public.companies
WHERE logo IS NOT NULL
  AND jsonb_typeof(logo) = 'array'
  AND jsonb_array_length(logo) > 0;

Verify the files themselves (optional)

If you have access to the bucket used during the sample migration, open the file_url from the query above and confirm the image loads. On your own migration, spot-check a few URLs against your S3 bucket — the database should never contain file bytes, only pointers to where AT Migrator uploaded them.

Ready for your own base?

The sample proves the engine — including links, formulas, and attachments. Your migration uses the same pipeline with your bucket and schema. See how to migrate Airtable to PostgreSQL for the full playbook, or view pricing when you are ready to run it on your base.

Not sure if you should migrate yet?

Get a free 30-minute discovery call with a migration engineer. We will look at your bases, estimate the work, and tell you honestly whether Postgres is the right move — no pressure, no obligation.

Keep reading

FAQ

Frequently asked questions

Quick answers to the questions teams ask most about this topic.

Yes. The sample base is public and read-only. You can inspect tables, linked records, and views without creating an AT Migrator account.

Any standard PostgreSQL host works. The seed is a full pg_dump (~85k lines) — use psql -f for Neon, local Postgres, or Supabase via the connection string. Pasting the entire file into a browser SQL editor is not reliable at this size.

The full migrated dump from the public CRM sample base — all tables, junction links, and COPY data for 48,000+ records. It is the same output AT Migrator generates for a real migration.

Yes, when you run the comparison checklist: row counts, foreign-key integrity, spot-checked links, and computed fields recreated as SQL. That is the same validation we recommend before production cutover.

No. The sample lets you verify our output on published data. Migrating your own base is a paid plan starting at $199, with a free discovery call to scope it first.