You're reading docs for Nextly Alpha. APIs may change between releases.

Database

Database

Choose and configure a database for your Nextly application. PostgreSQL is the recommended production database; MySQL is supported; SQLite is for local demos only.

Nextly stores data through a small set of dialect-specific adapter packages built on a shared Drizzle ORM core. Pick the adapter that matches your database, install it as a regular dependency, and Nextly handles dialect differences (placeholders, identifier escaping, RETURNING fallbacks, error classification) automatically.

Supported databases

DatabasePackageDriverUse it for
PostgreSQL@nextlyhq/adapter-postgrespgRecommended for production. Full feature set: JSONB, arrays, full-text search, ILIKE, savepoints, RETURNING.
MySQL@nextlyhq/adapter-mysqlmysql2Supported. Use it when you already run MySQL or a MySQL-compatible cloud database. Some features are emulated -- see the page.
SQLite@nextlyhq/adapter-sqlitebetter-sqlite3Local demos only. Single-writer, file-based, no SSL. Not recommended for serious local development -- run Postgres in Docker instead.

For minimum versions, cloud-provider compatibility, and the policy on MySQL-compatible variants (MariaDB, TiDB, Aurora MySQL, PlanetScale, Vitess), see database support.

Quick recommendation

Use PostgreSQL. It is the recommended production database, the only adapter with the full feature set, and the easiest to run locally with Docker (see Local development with Docker). Pick MySQL only if you already run it. Pick SQLite only for one-off local demos.

Installation

Each adapter is a regular npm package. Install the adapter and its peer driver in your project.

# PostgreSQL (recommended)
pnpm add @nextlyhq/adapter-postgres pg

# MySQL
pnpm add @nextlyhq/adapter-mysql mysql2

# SQLite (local demos only)
pnpm add @nextlyhq/adapter-sqlite better-sqlite3
# PostgreSQL (recommended)
npm install @nextlyhq/adapter-postgres pg

# MySQL
npm install @nextlyhq/adapter-mysql mysql2

# SQLite (local demos only)
npm install @nextlyhq/adapter-sqlite better-sqlite3
# PostgreSQL (recommended)
yarn add @nextlyhq/adapter-postgres pg

# MySQL
yarn add @nextlyhq/adapter-mysql mysql2

# SQLite (local demos only)
yarn add @nextlyhq/adapter-sqlite better-sqlite3
# PostgreSQL (recommended)
bun add @nextlyhq/adapter-postgres pg

# MySQL
bun add @nextlyhq/adapter-mysql mysql2

# SQLite (local demos only)
bun add @nextlyhq/adapter-sqlite better-sqlite3

Selecting a dialect

Nextly's runtime picks the dialect from environment variables, in this order:

  1. DB_DIALECT -- explicit override. One of postgresql, mysql, sqlite.
  2. DATABASE_URL protocol -- postgres:// / postgresql:// -> PostgreSQL, mysql:// -> MySQL, file: (or *.db / *.sqlite) -> SQLite.
  3. Fallback -- PostgreSQL with a warning. Always set DB_DIALECT explicitly to avoid the warning.

Minimal .env files for each dialect:

.env (PostgreSQL)
DB_DIALECT=postgresql
DATABASE_URL=postgres://nextly:nextly@localhost:5432/nextly_dev
.env (MySQL)
DB_DIALECT=mysql
DATABASE_URL=mysql://nextly:nextly@localhost:3306/nextly_dev
.env (SQLite, local demo only)
DB_DIALECT=sqlite
DATABASE_URL=file:./data/nextly.db

Feature comparison

The adapters share a single CRUD API, but underlying dialect capabilities differ. The shared DrizzleAdapter base reports per-dialect capabilities at runtime; this table is a quick reference.

FeaturePostgreSQLMySQLSQLite
JSONB columnsYesNo (JSON only)No (JSON as TEXT)
Array columnsYesNoNo
RETURNING clauseYesNo (extra SELECT after writes)Yes (3.35+)
Case-insensitive text matching (ILIKE, ICONTAINS)LOWER(...) LIKE LOWER(...); ASCII letters only under the C or POSIX collationLOWER(...) LIKE LOWER(...), even under a case-sensitive collationLOWER(...) LIKE LOWER(...), ASCII letters only
Savepoints (nested transactions)YesDisabled by adapterYes
Full-text searchtsvectorFULLTEXT indexesFTS5 (extension)
Upsert syntaxON CONFLICT DO UPDATEON DUPLICATE KEY UPDATEON CONFLICT DO UPDATE
Connection poolingpg.Poolmysql2 poolNone (single file handle)
Generated columnsYesYesYes
Max query parameters65,53565,535999
SSL / TLSYesYesN/A (local file)
Deadlock retryOn 40001 / 40P01 (configurable)On error 1213 (configurable)N/A
Auto-detected providersNeon, Supabase, standardNoneNone

Text matching

The adapter's where clauses have four text operators, and they compile the same way on every dialect:

OperatorMatchesCase% and _ in the value
LIKEa patternas the column's collation compareswildcards
ILIKEa patternignoredwildcards
CONTAINSa substringas the column's collation comparesliteral characters
ICONTAINSa substringignoredliteral characters

A collection's contains and search filters, its search box in the admin and the API, and the admin's search of collections, Singles and field groups all match this way.

Use ICONTAINS for a search box: it finds the text the person typed, whatever its case, and a % or _ they type is searched for rather than read as a wildcard. CONTAINS and ICONTAINS escape %, _ and the escape character ! themselves; with LIKE and ILIKE the value is a pattern you build, so escape anything that should match literally.

import type { WhereClause } from "@nextlyhq/adapter-drizzle/types";

// Finds "Spring launch", "SPRING SALE" and "Last spring".
const where: WhereClause = {
  and: [{ column: "title", op: "ICONTAINS", value: "spring" }],
};

This matters on MySQL: a column under a case-sensitive collation such as utf8mb4_bin makes a plain LIKE (and CONTAINS) case-sensitive, while ILIKE and ICONTAINS lower-case both sides and ignore case there too. Which letters count as the same in another case is the database's own lower(), following the column's collation. On MySQL that is the Unicode case mapping of the collation's version, so a 0900 collation knows more letters than a legacy one such as utf8mb4_unicode_ci. On PostgreSQL a linguistic collation such as en_US.UTF-8 folds non-ASCII letters too. SQLite, and a PostgreSQL column under the C or POSIX collation, fold ASCII letters only, so there "É" and "é" still differ.

All four are for text columns. Matching through LOWER() cannot use an ordinary index on the column, so on a large table a substring search reads every row.

Next steps