Base de Datos
PostgreSQL con Prisma ORM para tu SaaS
Base de Datos
EmpiezaTuSaaS usa PostgreSQL + Prisma ORM. Tipos TypeScript automaticos, migraciones versionadas y control total sobre tu base de datos.
Setup
Instala las dependencias e inicializa Prisma:
npm install @prisma/client
npm install -D prisma
npx prisma initEsto crea prisma/schema.prisma y agrega DATABASE_URL a tu .env.
Configura la conexión en .env:
# Local con Docker
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/empiezatusaas"
# O con Neon (recomendado para producción)
DATABASE_URL="postgresql://user:password@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require"Para desarrollo local con Docker:
docker run --name postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=empiezatusaas \
-p 5432:5432 \
-d postgres:15Schema
El boilerplate incluye todos los modelos necesarios para autenticación (Better Auth), usuarios y suscripciones Stripe:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String
email String @unique
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role String? @default("user")
banned Boolean? @default(false)
banReason String?
banExpires DateTime?
sessions Session[]
accounts Account[]
subscription Subscription?
@@map("users")
}
model Session {
id String @id @default(cuid())
expiresAt DateTime
token String @unique
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("sessions")
}
model Account {
id String @id @default(cuid())
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("accounts")
}
model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
@@map("verifications")
}
model Subscription {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
plan String?
status String @default("inactive")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("subscriptions")
}| Modelo | Tabla | Descripción |
|---|---|---|
User | users | Usuarios con roles y campos de baneo |
Session | sessions | Sesiónes activas con token, IP y user agent |
Account | accounts | Cuentas de proveedor (credenciales, Google, GitHub) |
Verification | verifications | Tokens de verificación (email, reset password) |
Subscription | subscriptions | Suscripciones Stripe vinculadas al usuario |
Aplica el schema a tu base de datos:
npx prisma migrate dev --name initCliente de base de datos
El boilerplate incluye un cliente singleton que evita multiples instancias en desarrollo con hot reload:
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;Este patron singleton evita crear multiples conexiónes durante hot reload en desarrollo. Importa db desde @/lib/db en cualquier parte del proyecto.
Consultas con Prisma
Crear
import { db } from "@/lib/db";
const user = await db.user.create({
data: {
name: "Juan Garcia",
email: "juan@example.com",
emailVerified: false,
},
});Leer
// Por ID o campo unico
const user = await db.user.findUnique({
where: { email: "juan@example.com" },
});
// Con relaciones
const userWithSub = await db.user.findUnique({
where: { id: "user-id" },
include: { subscription: true, sessions: true },
});
// Listado con filtros
const activeUsers = await db.user.findMany({
where: { banned: false, emailVerified: true },
orderBy: { createdAt: "desc" },
take: 10,
});Actualizar
const user = await db.user.update({
where: { id: "user-id" },
data: { name: "Juan Garcia Lopez" },
});
// Upsert: crear o actualizar
const subscription = await db.subscription.upsert({
where: { userId: "user-id" },
update: { status: "active", plan: "pro" },
create: { userId: "user-id", status: "active", plan: "pro" },
});Eliminar
// Eliminar sesiónes expiradas
await db.session.deleteMany({
where: { expiresAt: { lt: new Date() } },
});Transacciones
const result = await db.$transaction(async (tx) => {
const user = await tx.user.create({
data: { name: "Maria", email: "maria@example.com", emailVerified: true },
});
const subscription = await tx.subscription.create({
data: { userId: user.id, status: "active", plan: "starter" },
});
return { user, subscription };
});Uso en Next.js
import { db } from "@/lib/db";
export default async function UsersPage() {
const users = await db.user.findMany({
include: { subscription: true },
orderBy: { createdAt: "desc" },
});
return (
<div>
{users.map((user) => (
<div key={user.id}>
<h2>{user.name}</h2>
<p>{user.subscription?.plan ?? "Sin suscripcion"}</p>
</div>
))}
</div>
);
}"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
export async function banUser(userId: string, reason: string) {
await db.user.update({
where: { id: userId },
data: { banned: true, banReason: reason },
});
revalidatePath("/admin/users");
}Migraciones
Desarrollo
# Crear y aplicar migracion
npx prisma migrate dev --name add_user_phone
# Resetear BD y re-aplicar todo
npx prisma migrate reset
# Sincronizar schema sin crear migracion (prototipado rapido)
npx prisma db pushProducción
# Solo aplicar migraciones pendientes
npx prisma migrate deployNunca uses migrate dev o migrate reset en producción. Estos comandos pueden eliminar datos.
Comandos utiles
| Comando | Descripción |
|---|---|
prisma migrate dev | Crear y aplicar migracion (desarrollo) |
prisma migrate deploy | Aplicar migraciones pendientes (produccion) |
prisma migrate reset | Resetear base de datos y re-aplicar todo |
prisma migrate status | Ver estado de migraciones |
prisma db push | Sincronizar schema sin crear migracion |
prisma generate | Regenerar cliente Prisma |
prisma studio | UI visual para tu base de datos |
Seed
Crea datos iniciales:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const admin = await prisma.user.upsert({
where: { email: "admin@example.com" },
update: {},
create: {
email: "admin@example.com",
name: "Administrador",
emailVerified: true,
role: "admin",
},
});
await prisma.subscription.upsert({
where: { userId: admin.id },
update: {},
create: {
userId: admin.id,
plan: "pro",
status: "active",
stripeCustomerId: "cus_seed_admin",
stripeSubscriptionId: "sub_seed_admin",
},
});
console.log("Seed completado:", { admin });
}
main()
.catch((e) => { console.error(e); process.exit(1); })
.finally(async () => { await prisma.$disconnect(); });Agrega al package.json:
{
"prisma": {
"seed": "npx tsx prisma/seed.ts"
}
}npx prisma db seedHosting
| Proveedor | Tier gratuito | Mejor para |
|---|---|---|
| Neon | Si (generoso) | Serverless, recomendado |
| Railway | Trial | Side projects |
| Vercel Postgres | Si | Apps en Vercel |
Recomendamos Neon para producción: tier gratuito generoso, serverless, y funciona perfecto con Prisma en Vercel.