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

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.

## Key takeaways

- We publish the source Airtable base and the full SQL output so you can inspect fidelity before you pay.
- Import the seed into Supabase, Neon, or local Postgres — any standard PostgreSQL host works.
- Validate row counts, junction tables, attachment metadata, and spot-check linked records on both sides.
- Formulas and rollups live in `vt_[table]` views — query those to compare computed values against Airtable.
- Attachments are copied to your S3-compatible bucket; Postgres stores filename, size, and URL in a jsonb column.

## 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 base: https://airtable.com/appnJn1Yw5mtBg1GF/shrR6TA2TIHemjjZ6

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: /samples/at-migrator-sample-migration.sql

The seed is a full migrated dump (~85k lines, COPY blocks — not INSERT statements). It is sanitized for any Postgres host — no Neon-specific role required. Use `psql -f` for Neon and local Postgres. For Supabase, connect with `psql` via the pooler string — pasting the entire file into the browser SQL editor is not reliable at this size. Objects are owned by whichever user runs the import.

## 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, 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)

```bash
# 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

### Row counts

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

```sql
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:

```sql
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`

**Base tables** (`companies`, `contacts`, etc.) hold migrated source data only. **`vt_` views** add formulas (e.g. `line_total`, `weighted_value`), rollups (e.g. `of_contacts`, `line_item_total`), lookups (e.g. `deal_stage`), and linked-record display fields.

Sync tools write the last computed value Airtable had at sync time. AT Migrator generates SQL that derives the same result from source columns and joins — so updating a linked record or input field changes the formula on the next query, just like in Airtable.

#### Verify a formula field

Pick a record in Airtable's **Order Line Items** table and note its record ID and `Line Total` value:

```sql
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 — 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:

```sql
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 — e.g. `of_contacts` and `total_deal_value` on `vt_companies`, or `company_industry` (lookup) on `vt_contacts`.

### Attachments — files in your bucket, metadata in Postgres

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

1. **Transfer the file** — download from Airtable's CDN, upload to your S3-compatible bucket (you connect storage during setup).
2. **Store metadata in Postgres** — the column becomes `jsonb`. Each file is an object with `key`, `url`, `filename`, `size`, and `mimetype`.

In this CRM sample, **Companies** has a `logo` attachment field on `companies.logo` (also visible on `vt_companies`). The seed dump's `url` values point at the bucket from that migration run — your migration will use your own URLs; verify the `jsonb` shape and metadata.

#### Verify attachment metadata

Pick a company with a **Logo** in Airtable and compare filename and size:

```sql
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>';
```

#### Verify attachment counts

```sql
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;
```

Compare the count to companies with logos in Airtable. Optionally open `file_url` if you have bucket access.

## Ready for your own base?

The sample proves the engine. See the full migration guide at https://www.atmigrator.com/blog/how-to-migrate-airtable-to-postgresql or view pricing at https://www.atmigrator.com/pricing

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