This guide describes how to migrate your Turso database to Neon Postgres using pgloader.

pgloader is an open-source data loading and migration tool that efficiently transfers data from various sources (like CSV, MySQL, SQLite, MS SQL, etc.) into Postgres, handling schema and data transformations on the fly. Since Turso databases are SQLite-compatible, you can dump them to a local SQLite file and then use pgloader to migrate that file to Neon Postgres.

Prerequisites

Before you begin, ensure you have the following:

  • A Neon account and a project. If you don't have one, see Sign up.
  • A database created in your Neon project. For instructions, see Create a database.
  • The Turso CLI installed. You'll use it to export your database.
  • The sqlite3 command-line tool, typically pre-installed on macOS and Linux.
  • Neon's Free plan supports 0.5 GB of data. If your data size is more than 0.5 GB, you'll need to upgrade to one of Neon's paid plans. See Neon plans for more information.

A review of the pgloader SQLite to Postgres Guide is also recommended. It provides a comprehensive overview of pgloader's capabilities and type mappings, which will be helpful for understanding the migration process.

Understanding SQLite and Postgres data types

Before migrating from Turso to Postgres, it's helpful to understand a key difference in how they handle data types. Turso, built on SQLite, uses a flexible typing system called "type affinity". You can store any type of data in any column, regardless of its declared type. Postgres uses a strict, static typing system. Data inserted into a column must precisely match the column's declared data type.

When converting a database, SQLite's type affinities are mapped to appropriate Postgres types. Here is a summary of the common mappings:

Data Type CategorySQLitePostgreSQLKey Differences & Notes
IntegerINTEGERSMALLINT (2 bytes)
INTEGER (4 bytes)
BIGINT (8 bytes)
SQLite's INTEGER is a flexible-size signed integer, storing values in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude of the value. PostgreSQL offers fixed-size integers for more granular control over storage and performance.
Auto-incrementing IntegerINTEGER PRIMARY KEYSMALLSERIAL (2 bytes)
SERIAL (4 bytes)
BIGSERIAL (8 bytes)
In SQLite, declaring a column as INTEGER PRIMARY KEY automatically makes it an alias for the rowid and thus auto-incrementing. PostgreSQL provides the SERIAL pseudo-types, which create a sequence object to generate unique identifiers.
Floating-PointREALREAL (4 bytes)
DOUBLE PRECISION (8 bytes)
SQLite's REAL is an 8-byte IEEE floating-point number. PostgreSQL offers both single-precision (REAL) and double-precision (DOUBLE PRECISION) floating-point numbers.
Arbitrary Precision NumericNUMERICNUMERIC(precision, scale)
DECIMAL(precision, scale)
SQLite's NUMERIC affinity will attempt to store data as INTEGER or REAL, or as TEXT if it cannot be losslessly converted. PostgreSQL's NUMERIC and DECIMAL types are for exact decimal arithmetic, crucial for financial and scientific applications, allowing for user-defined precision and scale.
StringTEXT
VARCHAR(n)
CHAR(n)
TEXT
VARCHAR(n)
CHAR(n)
While both databases accept these type names, in SQLite, they all have a TEXT affinity. The length (n) is not enforced in SQLite. In PostgreSQL, VARCHAR(n) enforces a maximum length, and CHAR(n) is a fixed-length, blank-padded string. TEXT in PostgreSQL has no predefined length limit.
Binary DataBLOBBYTEABoth are used for storing raw binary data.
Date & TimeTEXT
REAL
INTEGER
DATE
TIME
TIMESTAMP
TIMESTAMPTZ (with time zone)
INTERVAL
SQLite has no dedicated date/time storage class; they are typically stored as TEXT (ISO-8601 strings), REAL (Julian day numbers), or INTEGER (Unix timestamps). PostgreSQL provides a rich set of specific date and time types with built-in functions for complex date and time arithmetic and time zone handling.
BooleanINTEGER (0 for false, 1 for true)BOOLEANSQLite does not have a native boolean type and commonly uses INTEGER with values 0 and 1. PostgreSQL has a dedicated BOOLEAN type that stores true or false.
JSONTEXTJSON
JSONB
In SQLite, JSON data is stored as TEXT. PostgreSQL offers two dedicated JSON types: JSON for storing the raw JSON text and JSONB for a decomposed binary format that is more efficient for indexing and querying.
Unique Identifier-UUIDPostgreSQL has a dedicated UUID data type for storing Universally Unique Identifiers, which is not present in SQLite.
Array-data_type[]PostgreSQL supports arrays of any built-in or user-defined data type, a powerful feature for storing lists of values in a single column. SQLite does not have a native array type.
  1. Authenticate Turso CLI

    If you haven't authenticated the Turso CLI yet, run:

    turso auth login

    Follow the prompts to log in with your Turso account credentials. This will allow you to access your Turso databases and export them for migration.

  2. Export your Turso database

    Dump your Turso database to a local sql file using the turso db shell command with the .dump option:

    turso db shell <database-name> .dump > dump.sql

    Replace <database-name> with the name of your Turso database. This command connects to your Turso database and runs the .dump command, which outputs the full schema and data as SQL statements.

    Next, generate a local SQLite database file from the dump:

    sqlite3 turso_export.db < dump.sql

    You now have a turso_export.db file containing your data, ready for migration.

  3. Retrieve your Neon database connection string

    Log in to the Neon Console. Find the connection string for your database by clicking the Connect button on your Project Dashboard. Make sure the Connection pooling toggle is disabled:

    Connection details modal with connection pooling disabled

    Your connection string should look similar to this:

    postgresql://alex:AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require

    important

    You will need to remove &channel_binding=require from the connection string, as pgloader does not support channel binding and throws an error when it is present.

    Now, modify this connection string to pass your endpoint ID (ep-cool-darkness-123456 in this example) to Neon with your password using the endpoint keyword, as shown here:

    postgresql://alex:endpoint=ep-cool-darkness-123456;AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require

    note

    Passing the endpoint ID with your password is a required workaround for some Postgres drivers, including the one used by pgloader. For more information, see Connect with an endpoint ID.

    Keep your modified Neon connection string handy.

  4. Install pgloader

    Install the pgloader utility using your preferred method:

    • For macOS with Homebrew: brew install pgloader
    • For Debian/Ubuntu: sudo apt-get install pgloader
    • For Docker: Pull the latest image with docker pull dimitri/pgloader:latest

    For other systems, see Installing pgloader.

  5. Run a basic migration

    For a straightforward migration, run pgloader directly from the command line:

    pgloader sqlite://turso_export.db "postgresql://alex:endpoint=ep-cool-darkness-123456;AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require"

    Make sure to enclose the Postgres connection string in quotes to prevent shell interpretation issues.

    The command output will look similar to this:

    2025-06-27T08:26:19.941000Z LOG report summary reset
                 table name     errors       rows      bytes      total time
    -----------------------  ---------  ---------  ---------  --------------
                      fetch          0          0                     0.000s
            fetch meta data          0          5                     0.204s
             Create Schemas          0          0                     0.108s
           Create SQL Types          0          0                     0.222s
              Create tables          0          4                     1.307s
             Set Table OIDs          0          2                     0.121s
    -----------------------  ---------  ---------  ---------  --------------
                    authors          0          3     0.1 kB          1.082s
                      books          0          5     0.2 kB          0.993s
    -----------------------  ---------  ---------  ---------  --------------
    COPY Threads Completion          0          4                     1.080s
     Index Build Completion          0          2                     2.342s
             Create Indexes          0          2                     0.662s
            Reset Sequences          0          0                     1.297s
               Primary Keys          0          2                     0.650s
        Create Foreign Keys          0          1                     0.339s
            Create Triggers          0          0                     0.211s
           Install Comments          0          0                     0.000s
    -----------------------  ---------  ---------  ---------  --------------
          Total import time          8     0.3 kB          6.581s

    pgloader will automatically create the necessary tables and indexes in your Neon Postgres database, and transfer all data from the turso_export.db file. The summary report at the end confirms that the migration completed successfully without errors.

Advanced migration with custom casting

For better control over the destination schema, create a pgloader load file. This lets you cast columns to specific Postgres types.

For example, if you have an authors table with an id column that is an INTEGER PRIMARY KEY in Turso (SQLite), you can cast it to SERIAL in Postgres. Similarly, if you have a published_date column stored as TEXT, you can cast it to DATE

Create a file named turso.load with the following content, replacing the connection strings as needed:

LOAD DATABASE
    FROM sqlite://turso_export.db
    INTO postgresql://alex:endpoint=ep-cool-darkness-123456;AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require

WITH
    include drop,
    create tables,
    create indexes,
    reset sequences,
    downcase identifiers

CAST
    -- Cast integer primary keys to SERIAL
    column authors.id to serial,
    column books.id to serial,

    -- Cast text date columns to Postgres DATE
    column books.published_date to date;

Run the migration using the load file:

pgloader turso.load

The migration will produce a cleaner Postgres schema with SERIAL primary keys and proper Postgres date types.

Post-migration verification

After migrating, always verify your data.

Verify sequences

The reset sequences option in the load file ensures that auto-incrementing columns start from the correct value. Connect to your Neon database using psql or the Neon SQL Editor and check the next value for a table's sequence:

SELECT nextval(pg_get_serial_sequence('books', 'id'));

This should return a value one higher than the max id in the books table. If it doesn't, you can reset it manually:

SELECT setval(
    pg_get_serial_sequence('books', 'id'),
    (SELECT MAX(id) FROM books) + 1
);

Verify row counts

Compare row counts between your Turso source and Neon destination:

SELECT count(*) FROM books;
SELECT count(*) FROM authors;

Update your application

After migrating your database schema and data to Neon, you must update your application code to connect to Neon and execute queries using a Postgres driver instead of the Turso SQLite client.

Connection changes

First, replace your Turso database client (such as @tursodatabase/serverless or @libsql/client) with a Postgres-compatible driver. For serverless or edge environments, Neon's serverless driver is a great choice.

npm uninstall @tursodatabase/serverless
npm install @neondatabase/serverless # or npm install pg

Next, update your environment variables with your Neon connection string retrieved from the Neon Console:

DATABASE_URL="postgresql://alex:AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require"

Then, update your database connection initialization.

Before (Turso):

import { connect } from "@tursodatabase/serverless";

const db = connect({
  url: process.env.TURSO_DATABASE_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
});

After (Neon):

import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);

Query translation

When converting your Turso queries to run against Postgres, you must adapt how statements are prepared and executed.

You need to transition from SQLite's parameter binding (often using ? placeholders) and driver-specific methods (.run(), .all(), .get()) to standard Postgres driver syntax. For example, with the Neon serverless driver, you can use tagged template literals for queries, while with node-postgres, you use parameterized queries with $1, $2, etc.

Before (Turso):

// Inserting data
const insertUser = db.prepare("INSERT INTO users (username) VALUES (?)");
await insertUser.run("alice");

// Querying data
const stmt = db.prepare("SELECT * FROM users");
const users = await stmt.all();

After (Neon):

// Inserting data
await sql`INSERT INTO users (username) VALUES (${'alice'})`;

// Querying data
const users = await sql`SELECT * FROM users`;

SQL dialect differences

Turso (SQLite) and Postgres use different SQL dialects. Here are the key differences you'll need to address when converting your application queries.

Boolean handling

SQLite has no native boolean type -- it stores true/false as integers 1/0. Postgres has a dedicated BOOLEAN type with true/false values.

Before (Turso/SQLite):

SELECT * FROM users WHERE active = 1;
INSERT INTO users (username, active) VALUES ('alice', 1);

After (Neon/Postgres):

SELECT * FROM users WHERE active = true;
INSERT INTO users (username, active) VALUES ('alice', true);

Case-insensitive string matching

SQLite's LIKE operator is case-insensitive for ASCII characters by default. Postgres's LIKE is case-sensitive; use ILIKE for case-insensitive matching, or LOWER() for comparisons.

Before (Turso/SQLite):

SELECT * FROM users WHERE username LIKE '%alice%';

After (Neon/Postgres):

SELECT * FROM users WHERE username ILIKE '%alice%';

Alternatively, normalize both sides:

SELECT * FROM users WHERE LOWER(username) LIKE LOWER('%alice%');

Date and time functions

SQLite and Postgres use different built-in functions for date and time operations.

Before (Turso/SQLite):

-- Current timestamp
INSERT INTO logs (message, created_at) VALUES ('startup', datetime('now'));

-- Formatting a timestamp
SELECT strftime('%Y-%m-%d', created_at) FROM logs;

After (Neon/Postgres):

-- Current timestamp
INSERT INTO logs (message, created_at) VALUES ('startup', NOW());

-- Formatting a timestamp
SELECT to_char(created_at, 'YYYY-MM-DD') FROM logs;
OperationSQLitePostgres
Current timestampdatetime('now')NOW() or CURRENT_TIMESTAMP
Current datedate('now')CURRENT_DATE
Format timestampstrftime(format, ts)to_char(ts, format)
Date arithmetic (add 7 days)datetime(ts, '+7 days')ts + INTERVAL '7 days'
Extract part of a date (year)strftime('%Y', ts)EXTRACT(YEAR FROM ts)

Troubleshooting

SSL verify error with Docker

If you run pgloader from a Docker container and encounter an SSL verify error: 20 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, you may need to disable SSL certificate verification.

Modify your load file to set sslmode=allow in the Postgres connection string.

LOAD DATABASE
    FROM sqlite:///data/turso_export.db
    INTO postgresql://.../dbname?sslmode=allow;
...

Then, run the Docker command with the --no-ssl-cert-verification flag. Mount your database and load files into the container's /data directory.

docker run --rm -v /path/to/your/files:/data \
  dimitri/pgloader:latest \
  pgloader --no-ssl-cert-verification /data/turso.load

References