Skip to content

ADR-007: Neon + Drizzle for Database Layer ​

Status: Accepted
Date: 2026-02-01
Deciders: Abdisamed Mohamed
Related ADRs: ADR-002 (Edge-First Architecture), ADR-003 (Cost Optimization)

Context ​

CepatEdge requires a database solution that provides:

  • Edge compatibility (low latency connections)
  • Serverless scaling (pay-per-compute)
  • Type safety (TypeScript integration)
  • Performance (fast queries, connection pooling)
  • Cost efficiency (free tier utilization)

Traditional databases have limitations:

  • Regional databases add latency for global users
  • Server-based databases require management overhead
  • Heavy ORMs increase bundle size and complexity

Decision ​

Use Neon Serverless PostgreSQL with Drizzle ORM for the database layer.

Implementation ​

Database Configuration ​

typescript
// Neon connection (serverless)
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';

// Environment-based connection
export function createDb(env: Env) {
  const sql = neon(env.DATABASE_URL);
  return drizzle(sql, { schema });
}

Schema Definition ​

typescript
import { pgTable, text, integer, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('User', {
  id: text('id').primaryKey(),
  email: text('email').unique().notNull(),
  name: text('name').notNull(),
  role: text('role').notNull(),
}, (table) => ({
  emailIdx: { name: 'idx_users_email', columns: [table.email] },
  roleIdx: { name: 'idx_users_role', columns: [table.role] },
}));

export const maintenanceRequests = pgTable('MaintenanceRequest', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  status: text('status').default('pending'),
  submittedBy: text('submittedBy'),
}, (table) => ({
  statusIdx: { name: 'idx_requests_status', columns: [table.status] },
  submittedByIdx: { name: 'idx_requests_submitted', columns: [table.submittedBy] },
}));

Query Operations ​

typescript
// Type-safe queries
export async function getUserById(id: string) {
  const db = createDb(env);

  const result = await db
    .select()
    .from(users)
    .where(eq(users.id, id))
    .limit(1);

  return result[0] || null;
}

// Complex queries with joins
export async function getMaintenanceWithDetails(id: string) {
  const db = createDb(env);

  return await db
    .select({
      request: maintenanceRequests,
      employee: users,
      technician: users,
    })
    .from(maintenanceRequests)
    .leftJoin(users, eq(maintenanceRequests.submittedBy, users.id))
    .leftJoin(users, eq(maintenanceRequests.assignedTo, users.id))
    .where(eq(maintenanceRequests.id, id));
}

Migration System ​

bash
# Generate migrations
npx drizzle-kit generate

# Push schema changes
npx drizzle-kit push

# Check migration status
npx drizzle-kit check

Consequences ​

Positive ​

  • Edge Compatible: Direct connections from Workers to database
  • Serverless: Automatic scaling, pay-per-compute model
  • Type Safety: Full TypeScript integration with Drizzle
  • Performance: Connection pooling, query optimization
  • Developer Experience: Intuitive API, excellent DX

Negative ​

  • PostgreSQL Only: Limited to PostgreSQL dialect
  • Learning Curve: New ORM for existing team members
  • Migration Complexity: Schema changes require careful planning
  • Connection Limits: Neon has connection limits (though generous)

Mitigation ​

  • Documentation: Comprehensive migration guides
  • Type Generation: Automatic TypeScript types from schema
  • Testing: Extensive test coverage for database operations
  • Monitoring: Built-in query performance monitoring

Performance Characteristics ​

Connection Performance ​

  • Connection Time: <50ms globally
  • Connection Pooling: Automatic connection reuse
  • Concurrent Connections: Up to 1000+ connections

Query Performance ​

  • Simple Queries: <10ms
  • Complex Joins: <50ms
  • Bulk Operations: <100ms for 1000 records
  • Indexed Queries: <5ms with proper indexing

Scaling ​

  • Compute Scaling: Automatic based on usage
  • Storage Scaling: Up to 512GB
  • Global Distribution: Data centers worldwide

Cost Optimization ​

Free Tier Utilization ​

  • Storage: 1GB free
  • Compute Hours: 100 hours/month free
  • Data Transfer: Generous free allowance

Scaling Costs ​

Operation          | Free Tier          | Paid Rate
-------------------|-------------------|--------------------
Storage            | 1GB               | $0.0001/GB/hour
Compute Hours      | 100 hours         | $0.0001/second
Data Transfer      | 100GB             | $0.1/GB

Optimization Strategies ​

typescript
// Efficient queries
const optimizedQuery = await db
  .select({ id: users.id, name: users.name }) // Select only needed fields
  .from(users)
  .where(eq(users.active, true)) // Filter early
  .limit(50) // Paginate results
  .orderBy(users.createdAt); // Use indexed columns

Alternatives Considered ​

PlanetScale ​

  • Pros: Serverless MySQL, branching, insights
  • Cons: MySQL dialect, PlanetScale-specific features

Cloudflare D1 ​

  • Pros: Edge-native, SQLite-compatible, zero latency
  • Cons: SQLite limitations, smaller ecosystem

Supabase ​

  • Pros: Full backend-as-service, real-time features
  • Cons: Additional services, vendor lock-in

Traditional PostgreSQL ​

  • Pros: Full PostgreSQL features, mature ecosystem
  • Cons: Management overhead, scaling complexity, latency

Migration Strategy ​

From Existing Database ​

  1. Schema Analysis: Map existing schema to Drizzle schema
  2. Data Migration: Export/import data with transformation
  3. Query Migration: Convert existing queries to Drizzle API
  4. Testing: Comprehensive testing of migrated operations

Incremental Migration ​

typescript
// Phase 1: Read operations
const users = await db.select().from(usersTable);

// Phase 2: Write operations
await db.insert(usersTable).values(newUser);

// Phase 3: Complex operations
await db.update(usersTable)
  .set({ lastLogin: new Date() })
  .where(eq(usersTable.id, userId));

Monitoring & Observability ​

Query Performance ​

typescript
// Built-in query logging
const db = createDb(env).$withLogger({
  logQuery: (query, params) => {
    console.log('Query:', query, 'Params:', params);
  }
});

Connection Monitoring ​

  • Connection Count: Monitor active connections
  • Query Latency: Track query execution times
  • Error Rates: Monitor database errors
  • Usage Patterns: Analyze query patterns for optimization

References ​

  • Neon Serverless Documentation
  • Drizzle ORM Documentation
  • PostgreSQL Performance Best Practices
  • Edge Database Patterns