MySQL
Switch the project from PostgreSQL to MySQL.
For the complete documentation index, see llms.txt. Prefer markdown by appending.mdto documentation URLs or sendingAccept: 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 driverpackages/db/src/server.ts- initialize Drizzle with a MySQL clientpackages/db/drizzle.config.ts- switch Drizzle Kit frompostgresqltomysqlpackages/auth/src/server.ts- change Better Auth fromprovider: "pg"toprovider: "mysql"packages/db/src/schema/*- replacepg-coreschema utilities withmysql-corepackages/db/src/utils/index.ts- remove PostgreSQL-specific types and SQL fragmentspackages/billing/shared/src/server/*andpackages/auth/src/scripts/seed.ts- replace PostgreSQL-style upserts andreturning()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 postgresUpdate environment variables
The project already validates DATABASE_URL, so the main change is using a MySQL connection string instead of a PostgreSQL one.
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.
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:
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:
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.tspackages/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->mysqlTablepgEnum(...)->mysqlEnum(...)- PostgreSQL timestamp definitions -> MySQL
datetime(...)ortimestamp(...)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:
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.tspackages/billing/shared/src/server/subscription.tspackages/billing/shared/src/server/order.tspackages/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-upselect.returning()after updates -> a follow-upselectif you need the updated row data back
For example, this current PostgreSQL-style upsert:
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:migrateAfter that, you can keep using the same Drizzle workflows:
pnpm with-env turbo db:generatepnpm with-env pnpm --filter @workspace/db db:migratepnpm with-env pnpm --filter @workspace/db db:studio
Recommended migration order
If you're converting an existing project, this is the safest order:
- Commit the PostgreSQL version first.
- Switch the driver, Drizzle config, and Better Auth provider.
- Convert
packages/db/src/schema/*frompg-coretomysql-core. - Update
packages/db/src/utils/index.tsfor MySQL-compatible typing and upsert helpers. - Replace PostgreSQL-only
.onConflictDoUpdate(...)and.returning()usage in service and seed files. - Delete old PostgreSQL migrations and generate fresh MySQL migrations.
- 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