HBForge
← Back to HBForge

Why We Built Our Own ORM Instead of Using Prisma

When we started building HBForge, the conventional wisdom was clear: just use Prisma. It has beautiful schema syntax, auto-generated types, a massive community, and investor backing. Every tutorial recommends it. Every boilerplate includes it.

We used it too. For two years, across a dozen production projects. And then we wrote our own ORM from scratch. Here is why.

The 15MB Elephant in the Room

Prisma is not a JavaScript library. It is a Rust binary wrapped in a JavaScript client. When you run npx prisma generate, it downloads a platform-specific query engine binary that weighs in at 15MB or more. On top of that, the generated client adds another 2-4MB of TypeScript definitions and runtime code.

For a traditional server deployment, this is mildly annoying. For serverless? It is a deal-breaker.

AWS Lambda has a 50MB deployment package limit (250MB unzipped). A Prisma-based function, once you add your application code, node_modules, and the query engine, routinely hits 40-60MB. You are burning over half your budget on the ORM alone. And Vercel Edge Functions have a 4MB limit — Prisma cannot even fit.

Cold Starts That Kill User Experience

Every time a serverless function cold-starts, Prisma must:

  1. Load the Node.js runtime
  2. Load the JavaScript client wrapper
  3. Spawn the Rust query engine as a child process
  4. Wait for the engine to establish a database connection pool
  5. Finally execute your actual query

On AWS Lambda, this process adds 800ms to 2 seconds to cold starts. We measured it across 50+ production functions. The median cold start with Prisma was 1.4 seconds. Without it? 180ms. The ORM was responsible for 87% of the cold start time.

For API endpoints serving real users, adding a second of latency to every cold start is unacceptable.

The Generated Client Problem

Prisma generates a client tailored to your schema. In theory, this enables type safety. In practice, it creates a fragile build pipeline:

Raw SQL Is a Second-Class Citizen

Every production application eventually needs raw SQL. Complex reporting queries, recursive CTEs, window functions, database-specific features — the ORM abstraction always leaks. Prisma's $queryRaw exists, but it returns unknown[], forcing you to cast everything manually. There is no way to compose raw SQL with Prisma's query builder. You are either fully in Prisma-land or fully writing raw SQL with no type safety.

What forge/data Does Differently

forge/data is a schema-first ORM written in pure JavaScript. No Rust binary. No code generation step. No child processes. The entire module is 2,800 lines of JavaScript that loads in under 10ms.

Define Your Schema in Code

forge/data — schema definition
import { defineSchema, types } from '@hyperbridge/forge/data';

const db = defineSchema({
  connection: 'postgresql://localhost:5432/myapp',
  models: {
    User: {
      id:        types.uuid().primaryKey(),
      email:     types.string().unique().index(),
      name:      types.string().min(2),
      role:      types.enum('admin', 'user').default('user'),
      posts:     types.hasMany('Post'),
      createdAt: types.timestamp().default('now'),
    },
    Post: {
      id:       types.uuid().primaryKey(),
      title:    types.string(),
      body:     types.text(),
      author:   types.belongsTo('User'),
      tags:     types.json().default([]),
    },
  },
});

No separate .prisma file. No generation step. The schema is your runtime — change it, and the query builder updates instantly.

Type-Safe Queries Without Code Generation

forge/data — querying
// Find with relations — fully typed from schema
const users = await db.User
  .where({ role: 'admin' })
  .include('posts')
  .orderBy('createdAt', 'desc')
  .limit(20)
  .exec();

// Composable query builder — chain freely
const query = db.Post.select('id', 'title');
if (tag) query.whereJsonContains('tags', tag);
if (since) query.where('createdAt', '>', since);
const results = await query.exec();

Raw SQL as a First-Class Feature

forge/data — raw SQL with type safety
// Raw SQL with automatic parameter binding
const report = await db.raw(`
  SELECT u.name, COUNT(p.id) as post_count
  FROM users u
  LEFT JOIN posts p ON p.author_id = u.id
  WHERE u.created_at > $1
  GROUP BY u.id
  HAVING COUNT(p.id) > $2
  ORDER BY post_count DESC
`, [startDate, minPosts]);

// Mix raw SQL with the query builder
const active = await db.User
  .whereRaw('last_login > NOW() - INTERVAL \'30 days\'')
  .include('posts')
  .exec();

Migrations Without a Binary

forge/data — migrations
// Auto-diff schema changes
await db.migrate(); // generates & applies SQL diffs

// Or generate migration files for review
await db.migrate({ dryRun: true });
// → outputs: ALTER TABLE users ADD COLUMN bio TEXT;

// Rollback support
await db.rollback(); // undo last migration

The Numbers Speak

MetricPrismaforge/data
Package size15MB+ (engine binary)48KB (pure JS)
Cold start (Lambda)1.4s median180ms median
Edge compatibleNo (binary required)Yes
Build step requiredprisma generateNone
Raw SQL composabilitySeparate escape hatchFirst-class, mixable
Dependencies@prisma/client + engineZero

When to Still Use Prisma

We are not saying Prisma is bad software. If you are building a traditional monolith deployed to a long-running server, the cold start penalty is irrelevant. If your team relies on Prisma Studio for visual database browsing, that is a real productivity tool. If you need multi-database support across MongoDB, MySQL, and PostgreSQL in the same project, Prisma handles that well.

But if you are building serverless-first, deploying to edge runtimes, or simply tired of watching your Lambda deployment package bloat beyond reason — forge/data was built for you. Pure JavaScript. No binaries. No generation step. Your ORM should be a library, not an infrastructure dependency.

HBForge is currently available exclusively for enterprise clients. Opening to the Developer Community on June 25, 2026.

Contact kr@hyperbridge.in for early access.