# Database Architecture MetaBuilder uses **Prisma ORM** as the data access layer with support for **multi-tenant** architecture and **DBAL** (Database Abstraction Layer) for server-side operations. ## Architecture Overview ``` ┌─────────────────────────────────────────────┐ │ React Components (Client) │ ├─────────────────────────────────────────────┤ │ useDatabase() hooks / API Routes │ ├─────────────────────────────────────────────┤ │ Database Layer │ │ ├─ Prisma ORM (Client-side/API) │ │ └─ DBAL (Server-side only) │ ├─────────────────────────────────────────────┤ │ Database Driver │ │ ├─ SQLite (Development) │ │ └─ PostgreSQL (Production) │ ├─────────────────────────────────────────────┤ │ Persistent Storage │ └─────────────────────────────────────────────┘ ``` ## Prisma ORM **File**: `prisma/schema.prisma` Prisma provides: - Type-safe database access - Automatic migrations - Query builder - Relationship management ### Schema Structure ```prisma // Define tables as models model User { id String @id @default(cuid()) email String @unique password String level Int tenant Tenant @relation(fields: [tenantId], references: [id]) tenantId String workflows Workflow[] @@unique([email, tenantId]) } model Workflow { id String @id @default(cuid()) name String config String @db.Text user User @relation(fields: [userId], references: [id]) userId String tenant Tenant @relation(fields: [tenantId], references: [id]) tenantId String } model Tenant { id String @id @default(cuid()) name String @unique users User[] workflows Workflow[] createdAt DateTime @default(now()) } ``` ## Multi-Tenant Design All tables include `tenantId` for data isolation: ```typescript // Always filter by tenant const userWorkflows = await prisma.workflow.findMany({ where: { tenantId: user.tenantId, userId: user.id, }, }) ``` ### Tenant Benefits - Complete data isolation between customers - Independent scaling per tenant - Separate backups and recovery - Regulatory compliance (data residency) ## DBAL (Database Abstraction Layer) **File**: `src/lib/database-dbal.server.ts` Advanced server-side database operations: ```typescript // Server-side only - compile-time enforcement import { dbalQuery } from '@/lib/database-dbal.server' // Atomic operations with validation const result = await dbalQuery({ operation: 'transfer_workflow', from: user1Id, to: user2Id, }) ``` ### DBAL vs Prisma | Feature | Prisma | DBAL | |---------|--------|------| | **Location** | Client + Server | Server only | | **Use Case** | Standard CRUD | Complex operations | | **Transactions** | Basic | Advanced with rollback | | **Validation** | Schema | Custom rules | | **Performance** | Good | Optimized | ### Primary Key Mapping DBAL adapters normalize primary key lookups for Prisma models that use non-`id` keys. The adapter maps the entity name to its primary key field so standard `read`/`update`/`delete` calls work correctly. Non-`id` primary keys in this repo: - `Credential.username` - `InstalledPackage.packageId` - `PackageData.packageId` - `PasswordResetToken.username` - `SystemConfig.key` ## Database Operations ### Create (Insert) ```typescript const newUser = await prisma.user.create({ data: { email: 'user@example.com', password: hashPassword('password'), level: 2, tenant: { connect: { id: tenantId } }, }, }) ``` ### Read (Query) ```typescript // Single record const user = await prisma.user.findUnique({ where: { id: userId }, }) // Multiple records with filters const workflows = await prisma.workflow.findMany({ where: { tenantId: user.tenantId, status: 'active', }, orderBy: { createdAt: 'desc' }, take: 10, }) ``` ### Update ```typescript const updated = await prisma.user.update({ where: { id: userId }, data: { email: 'newemail@example.com', level: 3, }, }) ``` ### Delete ```typescript await prisma.user.delete({ where: { id: userId }, }) ``` ### Relationships ```typescript // Include related data const user = await prisma.user.findUnique({ where: { id: userId }, include: { workflows: true, tenant: true, }, }) // Access related data user.workflows.forEach(workflow => { console.log(workflow.name) }) ``` ## Transactions Atomic operations across multiple records: ```typescript const result = await prisma.$transaction(async (tx) => { // All operations succeed or all fail const user = await tx.user.create({ data: userData }) const workflow = await tx.workflow.create({ data: { ...workflowData, userId: user.id } }) return { user, workflow } }) ``` ## Migrations Schema changes require migrations: ```bash # Create migration after schema change npm run db:migrate # Apply migrations npm run db:push # View database with UI npm run db:studio ``` ## Connection Management ### Development (SQLite) ```env DATABASE_URL="file:./dev.db" ``` ### Production (PostgreSQL) ```env DATABASE_URL="postgresql://user:password@host:5432/metabuilder" ``` ## Performance Optimization ### Indexing ```prisma model User { id String @id email String @unique // Automatically indexed tenantId String @@index([tenantId]) // Composite index for queries } ``` ### Pagination ```typescript const page = 1 const pageSize = 20 const workflows = await prisma.workflow.findMany({ where: { tenantId }, skip: (page - 1) * pageSize, take: pageSize, }) ``` ### Select Specific Fields ```typescript // Only retrieve needed columns const users = await prisma.user.findMany({ select: { id: true, email: true, level: true, }, }) ``` ## Error Handling ```typescript import { Prisma } from '@prisma/client' try { await prisma.user.create({ data: userData }) } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === 'P2002') { // Unique constraint violation throw new Error('Email already exists') } } throw error } ``` ## Best Practices ✅ **Do:** - Always filter by `tenantId` for tenant data - Use TypeScript for type safety - Create migrations for schema changes - Use transactions for related operations - Validate input before database operations - Cache frequently accessed data - Use indexes for common queries ❌ **Don't:** - Store secrets in database - Trust user input in queries - Assume data is available (check null) - Use `findMany()` without filters on large tables - Forget `tenantId` in queries - Execute raw SQL without escaping ## Related Files - Schema: `prisma/schema.prisma` - Migrations: `prisma/migrations/` - Database wrapper: `src/lib/database.ts` - DBAL (server-side): `src/lib/database-dbal.server.ts` - Type definitions: `src/lib/level-types.ts` See `/docs/` for more database patterns and guides.