Skip to content

Database — Prisma Schema & PostgreSQL

What This Does

Vexlyx uses Prisma ORM with PostgreSQL 16 for all database operations. The schema defines 8 core models (User, Project, Deployment, EnvVar, Domain, DnsRecord, Database, Mailbox) and 7 enums covering every entity in the hosting control panel. PostgreSQL runs locally via Docker Compose.

Architecture

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   Fastify Route  │────▶│  app.prisma.*    │────▶│   PostgreSQL 16  │
│  (route handler) │     │  (Prisma Client) │     │  (Docker)        │
└──────────────────┘     └──────────────────┘     └──────────────────┘
  • Prisma Client is registered as a Fastify plugin (app.prisma)
  • All routes access the database through app.prisma
  • Connection is established on server start, disconnected on shutdown
  • Queries are logged in development, only errors in production

Schema Overview

Models

ModelTablePurpose
UserusersPanel users with role-based access
ProjectprojectsHosted applications/websites
DeploymentdeploymentsBuild + deploy lifecycle tracking
EnvVarenv_varsEncrypted environment variables
DomaindomainsCustom domains with SSL status
DnsRecorddns_recordsDNS zone records (A, CNAME, MX, etc.)
DatabasedatabasesUser-provisioned MySQL/PostgreSQL instances
MailboxmailboxesEmail accounts per domain

Enums

EnumValues
RoleADMIN, USER
ProjectTypeNODEJS, NEXTJS, PYTHON, REACT, STATIC, PHP, WORDPRESS, DOCKER
ProjectStatusCREATING, ACTIVE, STOPPED, ERROR, DELETED
DeploymentStatusQUEUED, BUILDING, DEPLOYING, RUNNING, FAILED, CANCELLED
DomainStatusPENDING, ACTIVE, ERROR
DatabaseTypePOSTGRESQL, MYSQL
MailboxStatusACTIVE, SUSPENDED, DELETED

Key Relations

  • User → has many Projects, Domains, Databases, Mailboxes
  • Project → belongs to User, has many Deployments, EnvVars, Domains, Databases
  • Domain → belongs to User, optionally linked to Project, has many DnsRecords, Mailboxes
  • All child records cascade-delete when their parent is deleted

Unique Constraints

  • User.email — globally unique
  • Project [userId, name] — project names unique per user
  • EnvVar [projectId, key] — env var keys unique per project
  • Domain.hostname — globally unique
  • Database [userId, name] — database names unique per user
  • Mailbox.address — globally unique

Common Commands

Run from apps/api/:

bash
# Generate Prisma Client after schema changes
pnpm db:generate

# Create and apply a new migration
pnpm db:migrate

# Push schema to DB without creating a migration file (prototyping)
pnpm db:push

# Seed the database with development data
pnpm db:seed

# Open Prisma Studio (visual DB browser)
pnpm db:studio

# Reset DB: drop all data, re-apply migrations, re-seed
pnpm db:reset

Migration Workflow

Adding a New Model

  1. Edit prisma/schema.prisma — add the new model with @@map("table_name")
  2. Run pnpm db:migrate — Prisma creates a migration SQL file
  3. Name the migration descriptively: add_ssl_certificates_table
  4. Run pnpm db:generate — regenerate the client with new types
  5. Use app.prisma.newModel.findMany() etc. in service layer

Modifying an Existing Model

  1. Edit the model in schema.prisma
  2. Run pnpm db:migrate
  3. If migration fails due to data constraints, edit the generated SQL or use pnpm db:push for prototyping

Querying Patterns

ts
// In a Fastify route handler:
app.get("/", async (request, reply) => {
  const projects = await app.prisma.project.findMany({
    where: { userId: request.user.id },
    include: { deployments: { take: 1, orderBy: { createdAt: "desc" } } },
  });
  return { projects };
});

Always use the service layer for complex queries — routes should only call service methods.

How to Test

  1. Start PostgreSQL: docker-compose up -d postgres
  2. Apply migrations: cd apps/api && pnpm db:migrate
  3. Seed data: pnpm db:seed
  4. Open Prisma Studio: pnpm db:studio
  5. Verify the users table contains the admin user

How to Extend

  • New model: Add to schema.prisma, migrate, generate client
  • New enum: Add to schema.prisma enums section, migrate
  • New relation: Add fields to both sides of the relation, migrate
  • Indexes: Add @@index([field]) for frequently queried columns

Important Decisions

DecisionRationale
cuid() IDsURL-safe, sortable, no collision risk across distributed systems
Snake-case DB columnsPostgreSQL convention via @map(), while keeping camelCase in TypeScript
Cascade deletesSimplifies cleanup — deleting a User removes all their Projects, Domains, etc.
Per-user unique project namesUsers can have projects with the same name as other users
Separate Deployment modelTracks every deploy attempt, not just current state
EnvVar encryption placeholderValues stored as strings now; AES-256-GCM encryption added in F1.7

Released under the MIT license.