UNPKG

@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
# AI Coding System Guide: Building a Secure Multi-Tenant Application with This ORM ## Project Overview 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` ### Phase 2: ORM Schema Definition **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 ``` ### Phase 3: ORM Client Setup **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; } }; } ``` ### Phase 4: Authentication Setup **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; } ``` ### Phase 5: API Routes **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 } ); } } ``` ### Phase 6: Frontend Components **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> ); } ``` ### Phase 7: Environment Configuration **7.1 Environment Variables** File: `.env` ```bash # Database DATABASE_URL="Server=localhost;Database=ORMtest;Integrated Security=true;TrustServerCertificate=true" # NextAuth NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=your-secret-key-here # ORM Configuration LOG_LEVEL=debug LOG_MODULES=mssql,stitcher # Feature Flags ENABLE_AUDIT_LOGGING=true ENABLE_PERFORMANCE_MONITORING=true ``` File: `.env.example` ```bash # Copy this file to .env and fill in your values # Database DATABASE_URL="Server=localhost;Database=ORMtest;Integrated Security=true;TrustServerCertificate=true" # NextAuth NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=generate-a-random-secret # ORM Configuration LOG_LEVEL=off LOG_MODULES= # Feature Flags ENABLE_AUDIT_LOGGING=true ENABLE_PERFORMANCE_MONITORING=false ``` ### Phase 8: Testing **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); }); }); ``` ## Key Implementation Principles ### 1. Defense in Depth 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 ``` ### 2. Context Management 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 } ``` ### 3. Middleware Ordering 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({ ... })); ``` ### 4. Error Handling 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 }); } ``` ## Deployment Checklist ### Pre-Production - [ ] 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); }, []); } ``` ### ✅ Do: Use API Routes ```typescript // GOOD - Use API layer function MyComponent() { const [customers, setCustomers] = useState([]); useEffect(() => { fetch('/api/customers') .then(res => res.json()) .then(data => setCustomers(data.customers)); }, []); } ``` ### ❌ Don't: Set Global Context ```typescript // BAD - Race conditions with concurrent requests db.setContext({ user: { tenantId: 123 } }); // Request 1 starts // Request 2 overwrites context // Request 1 uses wrong context! ``` ### ✅ Do: Set Context Per Request ```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 } ``` ### ❌ Don't: Trust User Input for tenant_id ```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 } }); ``` ### ✅ Do: Use Session tenant_id ```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 } }); ``` ## Resources - [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