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
sqlite3command-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 Category | SQLite | PostgreSQL | Key Differences & Notes |
|---|---|---|---|
| Integer | INTEGER | SMALLINT (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 Integer | INTEGER PRIMARY KEY | SMALLSERIAL (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-Point | REAL | REAL (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 Numeric | NUMERIC | NUMERIC(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. |
| String | TEXTVARCHAR(n)CHAR(n) | TEXTVARCHAR(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 Data | BLOB | BYTEA | Both are used for storing raw binary data. |
| Date & Time | TEXTREALINTEGER | DATETIMETIMESTAMPTIMESTAMPTZ (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. |
| Boolean | INTEGER (0 for false, 1 for true) | BOOLEAN | SQLite 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. |
| JSON | TEXT | JSONJSONB | 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 | - | UUID | PostgreSQL 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. |
Export your Turso database
Dump your Turso database to a local sql file using the
turso db shellcommand with the.dumpoption:turso db shell <database-name> .dump > dump.sqlReplace
<database-name>with the name of your Turso database. This command connects to your Turso database and runs the.dumpcommand, 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.sqlYou now have a
turso_export.dbfile containing your data, ready for migration.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:

Your connection string should look similar to this:
postgresql://alex:AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=requireimportant
You will need to remove
&channel_binding=requirefrom the connection string, aspgloaderdoes 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-123456in this example) to Neon with your password using theendpointkeyword, as shown here:postgresql://alex:endpoint=ep-cool-darkness-123456;AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=requirenote
Passing the
endpoint IDwith your password is a required workaround for some Postgres drivers, including the one used bypgloader. For more information, see Connect with an endpoint ID.Keep your modified Neon connection string handy.
Install pgloader
Install the
pgloaderutility 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.
- For macOS with Homebrew:
Run a basic migration
For a straightforward migration, run
pgloaderdirectly 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.581spgloader will automatically create the necessary tables and indexes in your Neon Postgres database, and transfer all data from the
turso_export.dbfile. 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.loadThe 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 pgNext, 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;| Operation | SQLite | Postgres |
|---|---|---|
| Current timestamp | datetime('now') | NOW() or CURRENT_TIMESTAMP |
| Current date | date('now') | CURRENT_DATE |
| Format timestamp | strftime(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







