MySQL

Switch the project from PostgreSQL to MySQL.

For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

TurboStarter ships with PostgreSQL by default, but you can also run it on MySQL if that better matches your infrastructure.

MySQL is a reasonable choice when your team already runs MySQL-compatible infrastructure, you want a familiar managed database offering, or you prefer staying closer to the default PostgreSQL setup than a move to SQLite would allow. The important nuance is that MySQL is still a customization rather than a built-in preset.

PostgreSQL is still the default

At the time of writing, the starter is wired to PostgreSQL in packages/db/src/server.ts, packages/db/drizzle.config.ts, packages/auth/src/server.ts, packages/db/src/schema/*, and parts of packages/db/src/utils/index.ts.

When MySQL makes sense

MySQL is a good fit when you want:

  • compatibility with existing MySQL infrastructure
  • a traditional client/server SQL database instead of a local file database
  • a dialect that is operationally familiar to your team

You should usually stay on PostgreSQL if you want:

  • the exact default path used by the starter
  • the fewest code changes in the database layer
  • direct reuse of the current PostgreSQL-oriented schema helpers and upsert patterns

What you need to change

Moving to MySQL usually means updating these areas:

  • packages/db/package.json - add a MySQL driver
  • packages/db/src/server.ts - initialize Drizzle with a MySQL client
  • packages/db/drizzle.config.ts - switch Drizzle Kit from postgresql to mysql
  • packages/auth/src/server.ts - change Better Auth from provider: "pg" to provider: "mysql"
  • packages/db/src/schema/* - replace pg-core schema utilities with mysql-core
  • packages/db/src/utils/index.ts - remove PostgreSQL-specific types and SQL fragments
  • packages/billing/shared/src/server/* and packages/auth/src/scripts/seed.ts - replace PostgreSQL-style upserts and returning() calls

MySQL is closer to PostgreSQL than SQLite is, but there are still a few important Drizzle differences that affect the current codebase directly.

Install the MySQL driver

For Drizzle, the standard driver is mysql2.

pnpm --filter @workspace/db add mysql2
pnpm --filter @workspace/db remove postgres

Update environment variables

The project already validates DATABASE_URL, so the main change is using a MySQL connection string instead of a PostgreSQL one.

.env.local
DATABASE_URL="mysql://root:password@127.0.0.1:3306/turbostarter"

If you run a managed MySQL provider, use that provider's connection string instead.

Replace the database client

The current database package uses postgres-js. Replace it with a mysql2 connection or pool in packages/db/src/server.ts.

packages/db/src/server.ts
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";

import { env } from "./env";
import * as schema from "./schema";

const client = mysql.createPool(env.DATABASE_URL);

export const db = drizzle({
  client,
  schema,
  casing: "snake_case",
});

The rest of the package structure can stay the same.

Switch Drizzle Kit to MySQL

Update packages/db/drizzle.config.ts to use the Drizzle Kit MySQL dialect:

packages/db/drizzle.config.ts
import { defineConfig } from "drizzle-kit";

import { env } from "./src/env";

export default defineConfig({
  out: "./migrations",
  schema: "./src/schema/index.ts",
  dialect: "mysql",
  casing: "snake_case",
  dbCredentials: {
    url: env.DATABASE_URL,
  },
});

Update Better Auth

The auth package is PostgreSQL-first today. In packages/auth/src/server.ts, switch the Better Auth Drizzle adapter provider to MySQL:

packages/auth/src/server.ts
database: drizzleAdapter(db, {
  provider: "mysql",
  schema,
}),

Better Auth's Drizzle adapter supports MySQL directly, so this part is straightforward.

Convert the schema to MySQL

The schema files currently use drizzle-orm/pg-core in:

  • packages/db/src/schema/auth.ts
  • packages/db/src/schema/billing.ts

Move those files to drizzle-orm/mysql-core and replace the PostgreSQL-only schema helpers.

The most common replacements are:

  • pgTable -> mysqlTable
  • pgEnum(...) -> mysqlEnum(...)
  • PostgreSQL timestamp definitions -> MySQL datetime(...) or timestamp(...) equivalents
  • PostgreSQL-specific helper imports -> MySQL or dialect-neutral types

For example, the billing enums in packages/db/src/schema/billing.ts should become MySQL enums instead of PostgreSQL enums:

packages/db/src/schema/billing.ts
import { mysqlEnum, mysqlTable, text } from "drizzle-orm/mysql-core";

export const subscriptionStatusEnum = mysqlEnum("subscription_status", [
  "active",
  "canceled",
  "incomplete",
  "incomplete_expired",
  "past_due",
  "paused",
  "trialing",
  "unpaid",
]);

packages/db/src/utils/index.ts also needs attention. Right now it imports PgTable and PgTableWithColumns, and its buildConflictUpdateColumns helper emits raw SQL using the PostgreSQL/SQLite excluded.column_name pattern.

That helper is not directly reusable for MySQL upserts.

Review utility types and raw SQL carefully

Schema conversion is not limited to the table files. packages/db/src/utils/index.ts contains PostgreSQL-specific typing and conflict-update SQL, so it should be reviewed as part of the dialect switch.

Update upserts and returning queries

This is the biggest MySQL-specific difference in the current setup.

The current codebase uses PostgreSQL-style upserts with .onConflictDoUpdate(...) and frequently calls .returning() after inserts and updates. MySQL does not follow the same pattern.

In practice, you need to review files such as:

  • packages/billing/shared/src/server/customer.ts
  • packages/billing/shared/src/server/subscription.ts
  • packages/billing/shared/src/server/order.ts
  • packages/auth/src/scripts/seed.ts

The usual query changes are:

  • .onConflictDoUpdate(...) -> .onDuplicateKeyUpdate(...)
  • .returning() after inserts -> $returningId() only when you need inserted primary keys, or a follow-up select
  • .returning() after updates -> a follow-up select if you need the updated row data back

For example, this current PostgreSQL-style upsert:

packages/billing/shared/src/server/customer.ts
return db
  .insert(customer)
  .values(data)
  .onConflictDoUpdate({
    target: [customer.externalId, customer.provider],
    set: data,
  })
  .returning();

needs to be reworked for MySQL around onDuplicateKeyUpdate, without relying on PostgreSQL conflict targets or returning().

Regenerate migrations from scratch

Once the schema, utilities, and query layer are updated, generate a fresh MySQL migration set.

The existing migration history in packages/db/migrations was generated for PostgreSQL, so don't reuse it as-is for MySQL.

rm -rf packages/db/migrations
pnpm with-env turbo db:generate
pnpm with-env pnpm --filter @workspace/db db:migrate

After that, you can keep using the same Drizzle workflows:

  • pnpm with-env turbo db:generate
  • pnpm with-env pnpm --filter @workspace/db db:migrate
  • pnpm with-env pnpm --filter @workspace/db db:studio

If you're converting an existing project, this is the safest order:

  1. Commit the PostgreSQL version first.
  2. Switch the driver, Drizzle config, and Better Auth provider.
  3. Convert packages/db/src/schema/* from pg-core to mysql-core.
  4. Update packages/db/src/utils/index.ts for MySQL-compatible typing and upsert helpers.
  5. Replace PostgreSQL-only .onConflictDoUpdate(...) and .returning() usage in service and seed files.
  6. Delete old PostgreSQL migrations and generate fresh MySQL migrations.
  7. Smoke test auth, organizations, billing sync, and seed scripts.

Final notes

MySQL is a viable option, but it is not a connection-string-only switch. The biggest changes are not the schema files themselves, but the PostgreSQL-style query patterns already used across billing and auth scripts.

If you want the smoothest path away from PostgreSQL, review query semantics early, not just schema declarations.

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter