This stack supports two ORMs (Drizzle and Prisma) and two databases (Turso and PostgreSQL). During setup, you choose your preferred ORM and database.
Drizzle ORM is the default for its SQL-like syntax, zero runtime overhead, and full Turso migration support.
Turso provides edge-optimized SQLite with embedded replicas, generous free tiers, and built-in backups.
See Why Drizzle Over Prisma? for our reasoning.
During pnpm run setup, you'll choose:
- ORM: Drizzle (default) or Prisma
- Database: Turso (default) or PostgreSQL
You can switch anytime using:
pnpm turbo gen scaffold-databaseSchemas are defined in packages/infrastructure/drizzle/schema.ts:
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("User", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").unique(),
emailVerified: text("emailVerified"),
});
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;No code generation needed - edit the schema and immediately use it.
import { eq } from "drizzle-orm";
import { users } from "@react-router-gospel-stack/infrastructure";
import { db } from "~/db.server";
export const loader = async () => {
// Select all users
const allUsers = await db.select().from(users);
// Find by email
const user = await db
.select()
.from(users)
.where(eq(users.email, "test@example.com"));
// Insert
await db.insert(users).values({ name: "John", email: "john@example.com" });
// Update
await db.update(users).set({ name: "Jane" }).where(eq(users.id, userId));
// Delete
await db.delete(users).where(eq(users.id, userId));
return { allUsers };
};Drizzle doesn't require a client generation step - edit the schema and use it directly.
After modifying drizzle/schema.ts, create a new migration:
pnpm run db:migrate:newThis is an interactive command that will:
- Compare your schema with the database
- Generate migration SQL files in
drizzle/migrations/ - Prompt you to name the migration
Local (SQLite or PostgreSQL):
pnpm run db:migrate:applyProduction (reads from .env.production):
pnpm run db:migrate:apply:productionThis works with both local and remote Turso databases, as well as PostgreSQL.
View and edit your database with Drizzle Studio:
pnpm --filter @react-router-gospel-stack/infrastructure db:studioOpens at https://local.drizzle.studio
Schemas are defined in packages/infrastructure/prisma/schema.prisma:
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
}After schema changes, regenerate the TypeScript client:
pnpm run db:generateNote: With Prisma,
db:generateonly generates the TypeScript client. It does not create migrations.
import { db } from "~/db.server";
export const loader = async () => {
const allUsers = await db.user.findMany();
const user = await db.user.findUnique({
where: { email: "test@example.com" },
});
await db.user.create({ data: { name: "John", email: "john@example.com" } });
return { allUsers };
};After modifying the Prisma schema, create a new migration:
pnpm run db:migrate:newThis will:
- Compare your schema with the database
- Generate migration SQL files in
prisma/migrations/ - Prompt you to name the migration
PostgreSQL:
pnpm run db:migrate:applyTurso: Prisma cannot apply migrations directly to Turso. Generate the migration first, then apply manually:
# 1. Generate the migration (creates SQL file)
pnpm run db:migrate:new
# 2. Apply manually to local Turso
sqlite3 local.db < packages/infrastructure/prisma/migrations/<folder>/migration.sql
# 3. Or apply to remote Turso
turso db shell <database-name> < packages/infrastructure/prisma/migrations/<folder>/migration.sqlpnpm --filter @react-router-gospel-stack/infrastructure prisma:studioConfigure .env:
DATABASE_URL="file:../../local.db"
# No sync URL or auth token needed for local devApply your initial migration:
# With Drizzle
pnpm db:migrate:apply
# With Prisma
pnpm db:migrate:apply
sqlite3 local.db < packages/infrastructure/prisma/migrations/<folder>/migration.sql-
Install Turso CLI:
curl -sSfL https://get.tur.so/install.sh | bash turso auth login -
Create Database:
turso group create <group-name> turso db create <database-name> --group <group-name>
-
Get Credentials:
turso db show --url <database-name> turso db tokens create <database-name>
-
Configure Environment:
DATABASE_URL="file:/data/libsql/local.db" DATABASE_SYNC_URL="libsql://[db].turso.io" DATABASE_AUTH_TOKEN="<token>"
-
Apply Migrations:
# With Drizzle - direct push to remote pnpm db:migrate:apply:production # With Prisma - manual application pnpm db:migrate:apply:production turso db shell <database-name> < packages/infrastructure/prisma/migrations/<folder>/migration.sql
Turso's embedded replicas provide local-first resilience:
- Local SQLite file serves reads (fast)
- Automatic sync with remote database (every 60s)
- Writes go to remote first, then sync locally
- Offline resilience - reads work even if remote is unavailable
The client automatically handles this when you provide syncUrl.
-
Start PostgreSQL:
The stack includes a Docker Compose configuration that automatically creates the database:
pnpm run docker:db
This starts a PostgreSQL container with:
- Username:
postgres - Password:
postgres - Database:
remix_gospel(automatically created) - Port:
5432
Note: If you're using an existing PostgreSQL installation, you'll need to create the database manually:
psql -U postgres -c "CREATE DATABASE remix_gospel;" - Username:
-
Configure
.env:DATABASE_URL="postgresql://postgres:postgres@localhost:5432/remix_gospel" -
Run Migrations:
# With Drizzle or Prisma pnpm db:migrate:apply
For production on Fly.io, see Deployment Guide.
packages/infrastructure/
├── drizzle/
│ ├── schema.ts # Drizzle schema (default)
│ └── migrations/ # Drizzle migrations
├── prisma/
│ ├── schema.prisma # Prisma schema (alternative)
│ └── migrations/ # Prisma migrations
├── src/
│ ├── client.ts # Client factory (Drizzle or Prisma)
│ ├── seed.ts # Seed script
│ └── index.ts # Package exports
├── drizzle.config.ts # Drizzle configuration (optional)
├── prisma.config.ts # Prisma configuration (optional)
└── package.json
The seed script adapts to your chosen ORM:
pnpm --filter @react-router-gospel-stack/infrastructure db:seedEdit packages/infrastructure/src/database/seed.ts to customize seed data.
Drizzle:
pnpm --filter @react-router-gospel-stack/infrastructure db:studioPrisma:
pnpm --filter @react-router-gospel-stack/infrastructure prisma:studioLocal SQLite:
rm local.db
pnpm db:migrate:apply # Reapply migrationsPostgreSQL:
docker compose down -v
pnpm run docker:db
pnpm db:migrate:applyRemote Turso:
turso db destroy <database-name>
turso db create <database-name> --group <group-name>
pnpm db:migrate:apply:production # or apply migrations manuallyTo switch between Drizzle and Prisma:
pnpm turbo gen scaffold-databaseSelect your preferred ORM. The generator updates src/index.ts to export the correct client as default.
With Prisma: Regenerate the TypeScript client:
pnpm run db:generateWith Drizzle: No client generation needed. Check that your schema is properly imported.
- Backup your data
- Delete the migrations folder for your ORM
- Create a fresh initial migration
- Verify credentials:
turso db show --url <database-name> - Check token:
turso db tokens create <database-name> - Ensure correct org:
turso org list
- Why Drizzle Over Prisma? - Understanding our ORM choice
- Deployment - Deploy with your chosen stack
- Architecture - How the database package fits in
- Development - Workflow tips