@aradox/multi-orm
Version:
Type-safe ORM with multi-datasource support, row-level security, and Prisma-like API for PostgreSQL, SQL Server, and HTTP APIs
1,159 lines (951 loc) • 33 kB
Markdown
This guide explains how to structure and build a production-ready multi-tenant SaaS application using this ORM system with row-level security (RLS), role-based access control (RBAC), and audit logging.
## Architecture Layers
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js/React) │
│ - User interface │
│ - Client-side validation │
│ - Never imports ORM directly │
└────────────────┬────────────────────────────────────────────┘
│ HTTP/HTTPS
┌────────────────▼────────────────────────────────────────────┐
│ API Layer (Next.js API Routes / Express) │
│ - Authentication (NextAuth, Passport, etc.) │
│ - Authorization checks │
│ - Rate limiting │
│ - Input validation │
│ - Sets ORM context │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ ORM Middleware Layer │
│ - Row-level security (tenant isolation) │
│ - Role-based access control │
│ - Audit logging │
│ - Performance monitoring │
│ - Data sanitization │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Database Layer (PostgreSQL / SQL Server) │
│ - Database-level RLS (final line of defense) │
│ - Constraints and indexes │
│ - Stored procedures (optional) │
└─────────────────────────────────────────────────────────────┘
```
## Project Structure
```
my-saas-app/
├── app/ # Next.js 14+ app directory
│ ├── api/ # API routes
│ │ ├── customers/
│ │ │ └── route.ts # Customer CRUD endpoints
│ │ ├── orders/
│ │ │ └── route.ts # Order CRUD endpoints
│ │ └── auth/
│ │ └── [...nextauth]/
│ │ └── route.ts # NextAuth configuration
│ ├── (dashboard)/ # Protected routes
│ │ ├── customers/
│ │ │ └── page.tsx # Customers page
│ │ ├── orders/
│ │ │ └── page.tsx # Orders page
│ │ └── layout.tsx # Dashboard layout
│ └── layout.tsx # Root layout
│
├── lib/ # Shared utilities
│ ├── orm/ # ORM configuration
│ │ ├── client.ts # ORM client instance
│ │ ├── middleware.ts # Custom middleware
│ │ └── schema.qts # ORM schema definition
│ ├── auth.ts # Authentication utilities
│ └── permissions.ts # Permission checks
│
├── database/ # Database setup
│ ├── schema.sql # Database schema
│ ├── setup-rls-postgres.sql # PostgreSQL RLS setup
│ ├── setup-rls-sqlserver.sql # SQL Server RLS setup
│ ├── migrations/ # Database migrations
│ │ ├── 001_initial.sql
│ │ └── 002_add_tenant_id.sql
│ └── seeds/ # Seed data
│ └── dev.sql
│
├── middleware.ts # Next.js middleware (auth check)
├── .env # Environment variables
├── .env.example # Example environment variables
└── package.json # Dependencies
```
## Step-by-Step Implementation Guide
### Phase 1: Database Setup
**1.1 Create Database Schema**
File: `database/schema.sql`
```sql
-- Create tenants table
CREATE TABLE tenants (
id INT PRIMARY KEY IDENTITY(1,1),
name NVARCHAR(255) NOT NULL,
subdomain NVARCHAR(100) UNIQUE NOT NULL,
created_at DATETIME2 DEFAULT GETDATE(),
is_active BIT DEFAULT 1
);
-- Create users table
CREATE TABLE users (
id INT PRIMARY KEY IDENTITY(1,1),
tenant_id INT NOT NULL,
email NVARCHAR(255) NOT NULL,
password_hash NVARCHAR(255) NOT NULL,
first_name NVARCHAR(100),
last_name NVARCHAR(100),
role NVARCHAR(50) NOT NULL DEFAULT 'user',
created_at DATETIME2 DEFAULT GETDATE(),
CONSTRAINT FK_users_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id),
CONSTRAINT UQ_email_tenant UNIQUE (email, tenant_id)
);
-- Create customers table (multi-tenant)
CREATE TABLE customers (
id INT PRIMARY KEY IDENTITY(1,1),
tenant_id INT NOT NULL,
name NVARCHAR(255) NOT NULL,
email NVARCHAR(255),
phone NVARCHAR(50),
is_active BIT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
CONSTRAINT FK_customers_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Create orders table (multi-tenant + user ownership)
CREATE TABLE orders (
id INT PRIMARY KEY IDENTITY(1,1),
tenant_id INT NOT NULL,
customer_id INT NOT NULL,
user_id INT NOT NULL,
order_number NVARCHAR(50) NOT NULL,
total_amount DECIMAL(10,2),
status NVARCHAR(50) DEFAULT 'pending',
created_at DATETIME2 DEFAULT GETDATE(),
CONSTRAINT FK_orders_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id),
CONSTRAINT FK_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id),
CONSTRAINT FK_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Create audit log table
CREATE TABLE audit_logs (
id INT PRIMARY KEY IDENTITY(1,1),
tenant_id INT,
user_id INT,
model NVARCHAR(100) NOT NULL,
operation NVARCHAR(50) NOT NULL,
record_id INT,
old_values NVARCHAR(MAX),
new_values NVARCHAR(MAX),
timestamp DATETIME2 DEFAULT GETDATE(),
ip_address NVARCHAR(50),
user_agent NVARCHAR(500)
);
-- Create indexes
CREATE INDEX IX_customers_tenant ON customers(tenant_id);
CREATE INDEX IX_customers_email ON customers(email);
CREATE INDEX IX_orders_tenant ON orders(tenant_id);
CREATE INDEX IX_orders_customer ON orders(customer_id);
CREATE INDEX IX_orders_user ON orders(user_id);
CREATE INDEX IX_audit_logs_tenant ON audit_logs(tenant_id);
CREATE INDEX IX_audit_logs_timestamp ON audit_logs(timestamp);
```
**1.2 Setup Database-Level RLS**
Run the appropriate RLS setup script:
- PostgreSQL: `database/setup-rls-postgres.sql`
- SQL Server: `database/setup-rls-sqlserver.sql`
**2.1 Define ORM Schema**
File: `lib/orm/schema.qts`
```typescript
// Datasources
datasource main_db {
provider = "sqlserver"
url = env("DATABASE_URL")
}
// Config
config {
maxIncludeDepth = 5
maxFanOut = 100
strictMode = false
}
// Tenants model
model Tenants @datasource(main_db) {
id Int @id @default(autoincrement())
name String
subdomain String
created_at DateTime?
is_active Boolean
// Relations
users Users[] @relation(fields:[id],references:[tenant_id],strategy:"lookup")
customers Customers[] @relation(fields:[id],references:[tenant_id],strategy:"lookup")
orders Orders[] @relation(fields:[id],references:[tenant_id],strategy:"lookup")
}
// Users model
model Users @datasource(main_db) {
id Int @id @default(autoincrement())
tenant_id Int
email String
password_hash String
first_name String?
last_name String?
role String
created_at DateTime?
// Relations
tenant Tenants @relation(fields:[tenant_id],references:[id],strategy:"lookup")
orders Orders[] @relation(fields:[id],references:[user_id],strategy:"lookup")
}
// Customers model (multi-tenant)
model Customers @datasource(main_db) {
id Int @id @default(autoincrement())
tenant_id Int
name String
email String?
phone String?
is_active Boolean
created_at DateTime?
// Relations
tenant Tenants @relation(fields:[tenant_id],references:[id],strategy:"lookup")
orders Orders[] @relation(fields:[id],references:[customer_id],strategy:"lookup")
}
// Orders model (multi-tenant + user ownership)
model Orders @datasource(main_db) {
id Int @id @default(autoincrement())
tenant_id Int
customer_id Int
user_id Int
order_number String
total_amount Float?
status String?
created_at DateTime?
// Relations
tenant Tenants @relation(fields:[tenant_id],references:[id],strategy:"lookup")
customer Customers @relation(fields:[customer_id],references:[id],strategy:"lookup")
user Users @relation(fields:[user_id],references:[id],strategy:"lookup")
}
```
**2.2 Generate Types**
```bash
npm run generate
```
**3.1 Create ORM Client Instance**
File: `lib/orm/client.ts`
```typescript
import { ORMClient } from '@your-org/orm';
import * as fs from 'fs';
import * as path from 'path';
import {
tenantIsolationMiddleware,
rbacMiddleware,
auditLoggingMiddleware,
performanceMonitoringMiddleware
} from './middleware';
// Load schema
const schemaPath = path.join(process.cwd(), 'lib/orm/schema.qts');
const schema = fs.readFileSync(schemaPath, 'utf-8');
// Create ORM instance
const orm = new ORMClient(schema, { schemaPath });
// Generate client
export const db = orm.generate();
// Register global middleware
db.use(tenantIsolationMiddleware([
'Customers',
'Orders',
'Products'
]));
db.use(rbacMiddleware({
'Customers': ['user', 'admin', 'sales'],
'Orders': ['user', 'admin', 'sales'],
'Users': ['admin'],
'Tenants': ['admin']
}));
db.use(auditLoggingMiddleware({
async log(entry) {
// Log to database
await db.AuditLogs.create({
data: {
tenant_id: entry.context?.user?.tenantId,
user_id: entry.context?.user?.id,
model: entry.model,
operation: entry.operation,
record_id: entry.result?.id,
new_values: JSON.stringify(entry.args.data),
timestamp: entry.timestamp,
ip_address: entry.context?.metadata?.ip,
user_agent: entry.context?.metadata?.userAgent
}
});
}
}));
db.use(performanceMonitoringMiddleware({
slowQueryThreshold: 1000 // Log queries over 1 second
}));
export default db;
```
**3.2 Create Custom Middleware**
File: `lib/orm/middleware.ts`
```typescript
import { Middleware } from '@your-org/orm/types/middleware';
import db from './client';
// Re-export built-in middleware
export {
tenantIsolationMiddleware,
rbacMiddleware,
auditLoggingMiddleware,
performanceMonitoringMiddleware
} from '@your-org/orm/middleware/examples';
// Custom middleware: Auto-set user_id on create
export function autoSetUserMiddleware(): Middleware {
return {
name: 'auto-set-user',
beforeQuery: ({ operation, args, context }) => {
if (operation === 'create' && context.user?.id) {
args.data = {
...args.data,
user_id: context.user.id,
tenant_id: context.user.tenantId
};
}
return args;
}
};
}
// Custom middleware: Soft delete
export function softDeleteMiddleware(models: string[]): Middleware {
return {
name: 'soft-delete',
beforeQuery: ({ model, operation, args }) => {
if (!models.includes(model)) return args;
// Filter out soft-deleted records
if (['findMany', 'findUnique', 'count'].includes(operation)) {
args.where = {
...args.where,
deleted_at: null
};
}
// Convert delete to update
if (operation === 'delete') {
args.data = { deleted_at: new Date() };
}
return args;
}
};
}
```
**4.1 Configure NextAuth**
File: `app/api/auth/[...nextauth]/route.ts`
```typescript
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { compare } from 'bcryptjs';
import db from '@/lib/orm/client';
const handler = NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
subdomain: { label: "Subdomain", type: "text" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password || !credentials?.subdomain) {
return null;
}
// Find tenant by subdomain
const tenant = await db.Tenants.findFirst({
where: { subdomain: credentials.subdomain, is_active: true }
});
if (!tenant) return null;
// Set context to query this tenant's users
db.setContext({ user: { tenantId: tenant.id } });
// Find user
const user = await db.Users.findFirst({
where: {
email: credentials.email,
tenant_id: tenant.id
}
});
if (!user) return null;
// Verify password
const isValid = await compare(credentials.password, user.password_hash);
if (!isValid) return null;
return {
id: user.id.toString(),
email: user.email,
name: `${user.first_name} ${user.last_name}`,
tenantId: user.tenant_id,
role: user.role
};
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.tenantId = user.tenantId;
token.role = user.role;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
session.user.tenantId = token.tenantId as number;
session.user.role = token.role as string;
}
return session;
}
},
pages: {
signIn: '/login'
}
});
export { handler as GET, handler as POST };
```
**4.2 Create Auth Helper**
File: `lib/auth.ts`
```typescript
import { getServerSession } from 'next-auth';
import db from './orm/client';
export async function requireAuth() {
const session = await getServerSession();
if (!session?.user) {
throw new Error('Unauthorized');
}
// Set ORM context
db.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId,
role: session.user.role
}
});
return session;
}
export async function requireRole(allowedRoles: string[]) {
const session = await requireAuth();
if (!allowedRoles.includes(session.user.role)) {
throw new Error('Forbidden');
}
return session;
}
```
**5.1 Customers API**
File: `app/api/customers/route.ts`
```typescript
import { NextRequest, NextResponse } from 'next/server';
import db from '@/lib/orm/client';
import { requireAuth } from '@/lib/auth';
// GET /api/customers - List customers
export async function GET(request: NextRequest) {
try {
const session = await requireAuth();
// Set context with request metadata
db.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId,
role: session.user.role
},
metadata: {
requestId: request.headers.get('x-request-id'),
ip: request.headers.get('x-forwarded-for'),
userAgent: request.headers.get('user-agent')
}
});
// Parse query params
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
const search = searchParams.get('search');
// Build where clause
const where: any = { is_active: true };
if (search) {
where.name = { contains: search };
}
// Query customers (automatically filtered by tenant)
const customers = await db.Customers.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { name: 'asc' }
});
// Count total
const total = await db.Customers.count({ where });
return NextResponse.json({
customers,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Error fetching customers:', error);
return NextResponse.json(
{ error: error.message },
{ status: error.message === 'Unauthorized' ? 401 : 500 }
);
}
}
// POST /api/customers - Create customer
export async function POST(request: NextRequest) {
try {
const session = await requireAuth();
db.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId,
role: session.user.role
},
metadata: {
requestId: request.headers.get('x-request-id'),
ip: request.headers.get('x-forwarded-for')
}
});
const data = await request.json();
// Validate input
if (!data.name || !data.email) {
return NextResponse.json(
{ error: 'Name and email are required' },
{ status: 400 }
);
}
// Create customer (tenant_id automatically set by middleware)
const customer = await db.Customers.create({
data: {
name: data.name,
email: data.email,
phone: data.phone,
is_active: true,
tenant_id: session.user.tenantId // Explicitly set
}
});
return NextResponse.json(customer, { status: 201 });
} catch (error) {
console.error('Error creating customer:', error);
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}
```
**5.2 Orders API**
File: `app/api/orders/route.ts`
```typescript
import { NextRequest, NextResponse } from 'next/server';
import db from '@/lib/orm/client';
import { requireAuth } from '@/lib/auth';
// GET /api/orders - List orders
export async function GET(request: NextRequest) {
try {
const session = await requireAuth();
db.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId,
role: session.user.role
}
});
const { searchParams } = new URL(request.url);
const status = searchParams.get('status');
// Build where clause
const where: any = {};
if (status) {
where.status = status;
}
// Non-admin users only see their own orders
if (session.user.role !== 'admin') {
where.user_id = session.user.id;
}
// Query orders with related data
const orders = await db.Orders.findMany({
where,
include: {
customer: true
},
orderBy: { created_at: 'desc' },
take: 50
});
return NextResponse.json({ orders });
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}
// POST /api/orders - Create order
export async function POST(request: NextRequest) {
try {
const session = await requireAuth();
db.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId,
role: session.user.role
}
});
const data = await request.json();
// Validate
if (!data.customer_id || !data.order_number) {
return NextResponse.json(
{ error: 'customer_id and order_number are required' },
{ status: 400 }
);
}
// Create order
const order = await db.Orders.create({
data: {
customer_id: data.customer_id,
user_id: session.user.id,
tenant_id: session.user.tenantId,
order_number: data.order_number,
total_amount: data.total_amount,
status: 'pending'
}
});
return NextResponse.json(order, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}
```
**6.1 Customers Page**
File: `app/(dashboard)/customers/page.tsx`
```typescript
import { requireAuth } from '@/lib/auth';
import db from '@/lib/orm/client';
import CustomersList from '@/components/CustomersList';
export default async function CustomersPage() {
const session = await requireAuth();
// Set ORM context
db.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId,
role: session.user.role
}
});
// Fetch customers (server component)
const customers = await db.Customers.findMany({
where: { is_active: true },
orderBy: { name: 'asc' },
take: 50
});
return (
<div>
<h1>Customers</h1>
<CustomersList initialCustomers={customers} />
</div>
);
}
```
**7.1 Environment Variables**
File: `.env`
```bash
DATABASE_URL="Server=localhost;Database=ORMtest;Integrated Security=true;TrustServerCertificate=true"
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here
LOG_LEVEL=debug
LOG_MODULES=mssql,stitcher
ENABLE_AUDIT_LOGGING=true
ENABLE_PERFORMANCE_MONITORING=true
```
File: `.env.example`
```bash
DATABASE_URL="Server=localhost;Database=ORMtest;Integrated Security=true;TrustServerCertificate=true"
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=generate-a-random-secret
LOG_LEVEL=off
LOG_MODULES=
ENABLE_AUDIT_LOGGING=true
ENABLE_PERFORMANCE_MONITORING=false
```
**8.1 Create Test Setup**
File: `tests/setup.ts`
```typescript
import db from '../lib/orm/client';
export async function setupTestTenant() {
// Create test tenant
const tenant = await db.Tenants.create({
data: {
name: 'Test Tenant',
subdomain: 'test',
is_active: true
}
});
// Create test user
const user = await db.Users.create({
data: {
tenant_id: tenant.id,
email: 'test@example.com',
password_hash: 'hashed-password',
first_name: 'Test',
last_name: 'User',
role: 'admin'
}
});
return { tenant, user };
}
export async function cleanupTestData(tenantId: number) {
// Delete in reverse order of foreign keys
await db.Orders.deleteMany({ where: { tenant_id: tenantId } });
await db.Customers.deleteMany({ where: { tenant_id: tenantId } });
await db.Users.deleteMany({ where: { tenant_id: tenantId } });
await db.Tenants.delete({ where: { id: tenantId } });
}
```
**8.2 Example Test**
File: `tests/customers.test.ts`
```typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import db from '../lib/orm/client';
import { setupTestTenant, cleanupTestData } from './setup';
describe('Customers API - Multi-Tenant', () => {
let tenant1: any, user1: any;
let tenant2: any, user2: any;
beforeAll(async () => {
// Setup two tenants
const t1 = await setupTestTenant();
tenant1 = t1.tenant;
user1 = t1.user;
const t2 = await setupTestTenant();
tenant2 = t2.tenant;
user2 = t2.user;
// Create customer for tenant 1
db.setContext({ user: { id: user1.id, tenantId: tenant1.id } });
await db.Customers.create({
data: {
tenant_id: tenant1.id,
name: 'Customer T1',
email: 'customer1@tenant1.com',
is_active: true
}
});
// Create customer for tenant 2
db.setContext({ user: { id: user2.id, tenantId: tenant2.id } });
await db.Customers.create({
data: {
tenant_id: tenant2.id,
name: 'Customer T2',
email: 'customer2@tenant2.com',
is_active: true
}
});
});
afterAll(async () => {
await cleanupTestData(tenant1.id);
await cleanupTestData(tenant2.id);
});
it('should only return tenant 1 customers for user 1', async () => {
db.setContext({ user: { id: user1.id, tenantId: tenant1.id } });
const customers = await db.Customers.findMany({});
expect(customers).toHaveLength(1);
expect(customers[0].name).toBe('Customer T1');
expect(customers[0].tenant_id).toBe(tenant1.id);
});
it('should only return tenant 2 customers for user 2', async () => {
db.setContext({ user: { id: user2.id, tenantId: tenant2.id } });
const customers = await db.Customers.findMany({});
expect(customers).toHaveLength(1);
expect(customers[0].name).toBe('Customer T2');
expect(customers[0].tenant_id).toBe(tenant2.id);
});
it('should not allow cross-tenant access', async () => {
// User 1 tries to query tenant 2's data
db.setContext({ user: { id: user1.id, tenantId: tenant1.id } });
const customers = await db.Customers.findMany({
where: { tenant_id: tenant2.id } // Trying to access tenant 2
});
// Middleware should filter this out
expect(customers).toHaveLength(0);
});
});
```
Implement security at multiple layers:
```typescript
// Layer 1: Database-level RLS (SQL Server/PostgreSQL)
-- Enforced by database, cannot be bypassed
// Layer 2: ORM Middleware
db.use(tenantIsolationMiddleware(['Customers', 'Orders']));
// Layer 3: API Validation
if (!user.can('read:customers')) {
throw new Error('Forbidden');
}
// Layer 4: Frontend (least trusted)
// Only show UI elements user has access to
```
Set context at the beginning of each request:
```typescript
// ✅ Good: Set context per request
export async function GET(request: NextRequest) {
const session = await requireAuth();
db.setContext({
user: session.user,
metadata: {
requestId: request.headers.get('x-request-id'),
ip: request.headers.get('x-forwarded-for')
}
});
// All queries in this request use this context
}
```
Register middleware in order of restrictiveness:
```typescript
// 1. Authentication/RBAC (most restrictive)
db.use(rbacMiddleware({ ... }));
// 2. Tenant isolation
db.use(tenantIsolationMiddleware([...]));
// 3. Audit logging
db.use(auditLoggingMiddleware({ ... }));
// 4. Performance monitoring (least restrictive)
db.use(performanceMonitoringMiddleware({ ... }));
```
Handle middleware errors gracefully:
```typescript
try {
const customers = await db.Customers.findMany({});
return NextResponse.json({ customers });
} catch (error) {
if (error.message.includes('RBAC:')) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (error.message.includes('Tenant isolation:')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
```
- [ ] Database-level RLS enabled on all tables
- [ ] All API routes require authentication
- [ ] Middleware registered in correct order
- [ ] Environment variables configured
- [ ] SSL/TLS certificates installed
- [ ] Rate limiting configured
- [ ] Audit logging enabled
- [ ] Error monitoring (Sentry, etc.) configured
### Production
- [ ] LOG_LEVEL set to 'error' or 'warn'
- [ ] ENABLE_PERFORMANCE_MONITORING enabled
- [ ] Database backups configured
- [ ] Connection pooling configured
- [ ] CDN configured for static assets
- [ ] Health check endpoints created
- [ ] Monitoring dashboards created
### Post-Deployment
- [ ] Test multi-tenant isolation
- [ ] Verify RLS working at database level
- [ ] Check audit logs are being written
- [ ] Monitor slow queries
- [ ] Review error logs
- [ ] Verify rate limiting works
- [ ] Test authentication flows
## Common Pitfalls to Avoid
### ❌ Don't: Expose ORM to Frontend
```typescript
// BAD - Never do this!
import db from './lib/orm/client';
function MyComponent() {
const [customers, setCustomers] = useState([]);
useEffect(() => {
// This exposes database credentials to browser!
db.Customers.findMany({}).then(setCustomers);
}, []);
}
```
```typescript
// GOOD - Use API layer
function MyComponent() {
const [customers, setCustomers] = useState([]);
useEffect(() => {
fetch('/api/customers')
.then(res => res.json())
.then(data => setCustomers(data.customers));
}, []);
}
```
```typescript
// BAD - Race conditions with concurrent requests
db.setContext({ user: { tenantId: 123 } });
// Request 1 starts
// Request 2 overwrites context
// Request 1 uses wrong context!
```
```typescript
// GOOD - Each request has its own context
export async function GET(request: NextRequest) {
const session = await requireAuth();
db.setContext({ user: session.user });
// Context is only for this request
}
```
```typescript
// BAD - User could pass any tenant_id
const data = await request.json();
await db.Customers.create({
data: {
tenant_id: data.tenant_id, // NEVER trust this!
name: data.name
}
});
```
```typescript
// GOOD - Always use authenticated user's tenant
const session = await requireAuth();
await db.Customers.create({
data: {
tenant_id: session.user.tenantId, // From authenticated session
name: data.name
}
});
```
- [ORM Hooks & Middleware Guide](./HOOKS_MIDDLEWARE_GUIDE.md)
- [SQL Logging Guide](./SQL_LOGGING_GUIDE.md)
- [Database RLS Setup (PostgreSQL)](../database/setup-rls-postgres.sql)
- [Database RLS Setup (SQL Server)](../database/setup-rls-sqlserver.sql)
- [NextAuth Documentation](https://next-auth.js.org/)
- [Next.js App Router](https://nextjs.org/docs/app)
## Support
For issues or questions:
1. Check the [Hooks & Middleware Guide](./HOOKS_MIDDLEWARE_GUIDE.md)
2. Review example implementations in `Example/` directory
3. Check test files for usage patterns
4. Review audit logs for security issues