@farming-labs/orm

Framework Authors

This guide is for people building a framework, platform layer, or reusable module system on top of Farming Labs ORM.

That usually means the framework wants to own storage for things like:

The goal is not to make the framework into its own database product. The goal is to keep one durable storage contract while letting the consuming app bring Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, Cloudflare D1, Cloudflare KV, Redis, Supabase, EdgeDB / Gel, direct SQL, MongoDB, Firestore, DynamoDB, Unstorage, or another supported runtime.

The problem frameworks usually run into

Without a unifying layer, framework-owned modules tend to drift into:

That is how a framework slowly turns into an adapter ecosystem instead of one storage layer.

The intended integration path

1. Keep the framework schema in the package

framework-schema.ts
import { datetime, defineSchema, id, json, model, string, unique } from "@farming-labs/orm";

export const frameworkSchema = defineSchema({
  frameworkState: model({
    table: "framework_state",
    fields: {
      id: id(),
      scope: string().unique(),
      state: json(),
      updatedAt: datetime().defaultNow(),
    },
  }),

  billingAccount: model({
    table: "billing_accounts",
    fields: {
      id: id(),
      workspaceId: string().unique(),
      providerCustomerId: string().unique(),
      plan: string(),
      status: string(),
      renewsAt: datetime().nullable(),
      metadata: json().nullable(),
    },
  }),

  cacheEntry: model({
    table: "cache_entries",
    fields: {
      id: id(),
      namespace: string(),
      key: string(),
      value: json(),
      expiresAt: datetime().nullable(),
      updatedAt: datetime().defaultNow(),
    },
    unique: [unique(["namespace", "key"])],
  }),

  kvItem: model({
    table: "kv_items",
    fields: {
      id: id(),
      namespace: string(),
      key: string(),
      value: json(),
      updatedAt: datetime().defaultNow(),
    },
    unique: [unique(["namespace", "key"])],
  }),

  rateLimitBucket: model({
    table: "rate_limit_buckets",
    fields: {
      id: id(),
      scope: string(),
      identifier: string(),
      remaining: string(),
      resetAt: datetime(),
      updatedAt: datetime().defaultNow(),
    },
    unique: [unique(["scope", "identifier"])],
  }),

  auditLog: model({
    table: "audit_logs",
    fields: {
      id: id(),
      scope: string(),
      action: string(),
      actorId: string().nullable(),
      payload: json(),
      createdAt: datetime().defaultNow(),
    },
  }),

  observabilitySnapshot: model({
    table: "observability_snapshots",
    fields: {
      id: id(),
      scope: string(),
      kind: string(),
      snapshot: json(),
      capturedAt: datetime().defaultNow(),
    },
    unique: [unique(["scope", "kind"])],
  }),
});

That single schema can later power:

2. Accept the raw client from the app

The framework boundary should usually accept the app's real database or ORM client, not force the app to wrap it into another adapter shape first.

create-framework-orm.ts
import { createOrmFromRuntime } from "@farming-labs/orm-runtime";
import { frameworkSchema } from "./framework-schema";

export function createFrameworkOrm(database: unknown) {
  return createOrmFromRuntime({
    schema: frameworkSchema,
    client: database,
  });
}

That is what lets one framework runtime support many execution stacks without forking the framework storage logic into one implementation per backend.

3. Inspect the runtime when integration fails

If the app passes a wrapped or unsupported client, inspect it before constructing the ORM:

inspect-client.ts
import { inspectDatabaseRuntime } from "@farming-labs/orm";

const inspection = inspectDatabaseRuntime(database);

if (!inspection.runtime) {
  throw new Error(inspection.summary);
}

That gives the framework a much better onboarding story than a generic "unsupported client" failure.

4. Write the framework storage layer once

framework-store.ts
import type { OrmClient } from "@farming-labs/orm";
import { frameworkSchema } from "./framework-schema";

export function createFrameworkStore(orm: OrmClient<typeof frameworkSchema>) {
  return {
    getFrameworkState(scope: string) {
      return orm.frameworkState.findUnique({
        where: { scope },
        select: {
          id: true,
          scope: true,
          state: true,
          updatedAt: true,
        },
      });
    },

    putCacheEntry(input: {
      namespace: string;
      key: string;
      value: Record<string, unknown>;
      expiresAt?: Date | null;
    }) {
      return orm.cacheEntry.upsert({
        where: {
          namespace_key: {
            namespace: input.namespace,
            key: input.key,
          },
        },
        create: input,
        update: {
          value: input.value,
          expiresAt: input.expiresAt ?? null,
          updatedAt: new Date(),
        },
      });
    },

    putKvItem(input: { namespace: string; key: string; value: Record<string, unknown> }) {
      return orm.kvItem.upsert({
        where: {
          namespace_key: {
            namespace: input.namespace,
            key: input.key,
          },
        },
        create: input,
        update: {
          value: input.value,
          updatedAt: new Date(),
        },
      });
    },

    appendAuditLog(input: {
      scope: string;
      action: string;
      actorId?: string | null;
      payload: Record<string, unknown>;
    }) {
      return orm.auditLog.create({
        data: {
          ...input,
          actorId: input.actorId ?? null,
        },
      });
    },

    recordObservabilitySnapshot(input: {
      scope: string;
      kind: string;
      snapshot: Record<string, unknown>;
    }) {
      return orm.observabilitySnapshot.upsert({
        where: {
          scope_kind: {
            scope: input.scope,
            kind: input.kind,
          },
        },
        create: input,
        update: {
          snapshot: input.snapshot,
          capturedAt: new Date(),
        },
      });
    },
  };
}

This is the key value:

5. Use capabilities instead of backend guesses

When a framework needs behavior decisions, inspect capabilities:

capabilities.ts
const orm = await createFrameworkOrm(database);

orm.$driver.capabilities.supportsTransactions;
orm.$driver.capabilities.numericIds;
orm.$driver.capabilities.upsert;
orm.$driver.capabilities.returning.update;
orm.$driver.capabilities.supportsSchemaNamespaces;

That gives the framework a generic way to make runtime decisions without hard-coding Prisma-only, SQL-only, or Mongo-only assumptions.

6. Handle normalized errors at the framework boundary

errors.ts
import { isOrmError } from "@farming-labs/orm";

try {
  await store.putKvItem(input);
} catch (error) {
  if (isOrmError(error) && error.code === "UNIQUE_CONSTRAINT_VIOLATION") {
    throw new Error("The framework key already exists.");
  }

  throw error;
}

That keeps the framework from owning one parser for Prisma codes, another for SQLSTATEs, another for Mongo errors, and so on.

7. Use one setup path in tests, starters, and install flows

If the framework wants to prepare the live database in tests, demos, starter kits, or install flows, use the setup helpers:

setup.ts
import { bootstrapDatabase, pushSchema } from "@farming-labs/orm-runtime/setup";

await pushSchema({
  schema: frameworkSchema,
  client: database,
});

const orm = await bootstrapDatabase({
  schema: frameworkSchema,
  client: database,
});

That gives you:

instead of one flow per backend family.

Where this fits best

This pattern is especially strong when the framework wants to behave like:

That is why it fits reusable full-stack frameworks, platform layers, starter kits, and module systems so well.

Best next pages