# Importing data (CSV, SQL dumps)

> Load data into a SupaHost project from CSV files, SQL scripts or pg_dump archives, using Studio, a supabase-js script, psql and pg_restore over the Business SSH tunnel, or the Supabase Cloud import.

Source: https://supahost.app/docs/database/import-data · Updated 2026-11-11

How you load data into a project depends on its format, its size, and your plan. This page covers each route and the clean-up steps that apply to all of them.

## Pick a method

| You have | Size | Method | Plans |
|---|---|---|---|
| A Supabase Cloud project | Any | [One-click import](#from-supabase-cloud) | All |
| A CSV file | Small to medium | [Studio table editor](#csv-in-studio) | All |
| A `.sql` script | Small to medium | [Studio SQL editor](#sql-scripts-in-studio) | All |
| CSV or JSON from another system | Any | [Script through the Data API](#scripted-inserts-through-the-data-api) | All |
| A `pg_dump` archive or large SQL file | Large | [pg_restore or psql over the SSH tunnel](#pg_restore-and-psql-over-the-ssh-tunnel) | Business, Enterprise |

Direct Postgres connections from your computer are only possible on Business and Enterprise, through the SSH tunnel. On other plans, use Studio, the Data API, or the MCP server's `execute_sql` for small batches.

## From Supabase Cloud

If the data is in a Supabase Cloud project, use the built-in import instead of moving it by hand. It copies the database in one transaction, Auth users with their passwords and identities, RLS policies and roles, and Storage buckets and files into an empty SupaHost project. Start it from Dashboard → your project → **Import** tab. Auth settings, Edge Functions, cron jobs and Vault secrets aren't copied; the import lists them so you can set them up again.

Full steps: [Importing from Supabase Cloud](/docs/platform/import-from-supabase).

## CSV in Studio

Studio's table editor can read a CSV with a header row and insert it into a table.

1. Dashboard → your project → **Database** tab → **Open Studio**.
2. Open the **Table Editor**.
3. To create a new table from the file, start a new table and choose to import data from a CSV. Studio suggests column names and types from the header and the first rows. Check the types before saving; it's easy to end up with `text` where you wanted `numeric` or `timestamptz`.
4. To add rows to an existing table, open the table and use **Insert** → import from CSV. The CSV header must match the column names.

Studio parses the file in your browser, so very large files are slow or fail. For big imports, split the file or use a script.

## SQL scripts in Studio

For a `.sql` file of `create table` and `insert` statements, paste it into Studio's **SQL Editor** and run it. Statements in one run execute together, so a failure part-way through rolls back the whole run.

For a bookings app seed:

```sql
insert into public.venues (name, city, capacity) values
  ('Riverside Hall', 'Leeds', 120),
  ('The Glasshouse', 'Bristol', 60),
  ('North Pier Studio', 'Brighton', 35);
```

Keep each run to a manageable size: a few thousand rows of inserts is fine; a multi-gigabyte dump belongs on the tunnel route below.

## Scripted inserts through the Data API

On any plan, a small Node.js script can read a file and insert it in batches with the `service_role` key, which bypasses RLS. Run it on your own machine or server, never in a browser.

```ts
import { readFileSync } from 'node:fs'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://app-one.acme.supahost.space',
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
  { auth: { persistSession: false } }
)

type Booking = { external_id: string; venue_id: number; starts_at: string; guests: number }
const rows: Booking[] = JSON.parse(readFileSync('bookings.json', 'utf8'))

const BATCH = 500
for (let i = 0; i < rows.length; i += BATCH) {
  const chunk = rows.slice(i, i + BATCH)
  const { error } = await supabase
    .from('bookings')
    .upsert(chunk, { onConflict: 'external_id' })
  if (error) throw new Error(`Batch at row ${i}: ${error.message}`)
  console.log(`Imported ${Math.min(i + BATCH, rows.length)} / ${rows.length}`)
}
```

Upserting on a unique column (`external_id` here) makes the script safe to re-run after a failure. Batches of a few hundred rows keep each request small.

## pg_restore and psql over the SSH tunnel

On Business and Enterprise you can connect standard Postgres tools to your project's database. Set up the tunnel first, as described in [SSH access and direct Postgres](/docs/platform/ssh):

1. Add your public key under Dashboard → your project → **Connect** tab → **SSH & CLI**.
2. Open the tunnel and leave it running:

   ```bash
   ssh -N -L 5432:127.0.0.1:5432 supahost@acme.supahost.space
   ```

3. Copy the connection string from the **Connect** tab → **Database** → **Show connection string** (owner only). It looks like `postgresql://postgres:<password>@127.0.0.1:5432/p_app_one`.

Restore a custom-format dump (`pg_dump -Fc`) into the project:

```bash
export DATABASE_URL='postgresql://postgres:<password>@127.0.0.1:5432/p_app_one'

pg_restore --no-owner --no-privileges --schema=public \
  --dbname "$DATABASE_URL" invoices.dump
```

`--no-owner` and `--no-privileges` skip ownership and grants from the source server, whose roles don't exist here. `--schema=public` keeps the restore away from the `auth` and `storage` schemas, which Supabase services own.

Load a plain SQL file or a large CSV with psql:

```bash
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f schema_and_data.sql

psql "$DATABASE_URL" -c "\copy public.invoices (number, customer_id, total, issued_on) from 'invoices.csv' with (format csv, header true)"
```

`\copy` streams the file from your computer, so it handles files of any size.

If you prefer not to run a tunnel, pipe a SQL file through the restricted SSH command, which runs psql as `postgres` on the server:

```bash
ssh supahost@acme.supahost.space sudo supahost-psql app-one < schema_and_data.sql
```

## After any import

1. **Reset sequences.** Rows inserted with explicit ids don't move the sequence forward, so the next insert fails with a duplicate key. Fix it per table:

   ```sql
   select setval(
     pg_get_serial_sequence('public.invoices', 'id'),
     coalesce((select max(id) from public.invoices), 0) + 1,
     false
   );
   ```

   More in [duplicate key value violates unique constraint after importing data](/kb/duplicate-key-sequence).

2. **Enable RLS** on every imported table in `public` and add policies, or the tables are readable through the Data API. See [Row Level Security](/docs/database/row-level-security).
3. **Update statistics** so the planner has accurate row counts: `analyze;`
4. **Reload the Data API schema cache** if new tables don't appear: `notify pgrst, 'reload schema';` See [PostgREST not seeing new tables](/kb/postgrest-schema-cache).

## Common import errors

| Error | Cause |
|---|---|
| `invalid byte sequence for encoding "UTF8": 0x00` | Null bytes in text. See [the KB article](/kb/invalid-byte-sequence-utf8). |
| `role "..." does not exist` | Dump references source roles; use `--no-owner --no-privileges`. |
| `extension "..." is not available` | The extension isn't in this Postgres image. Check `pg_available_extensions`. |
| `permission denied for schema auth` | The dump touches Supabase-owned schemas; restore `public` only. |

Your project's database counts toward the plan's shared database size, so check headroom under Dashboard → **Usage & billing** before a very large import.

## Related

- [Exporting your data](/docs/platform/exports)
- [Seeding data](/docs/local-development/seeding)
- [Migrate from Heroku Postgres](/kb/migrate-from-heroku)
- [Migrate from MySQL](/kb/migrate-from-mysql)
