# Framework Authors
URL: /docs/use-cases/framework-authors
LLM index: /llms.txt
Description: How frameworks and reusable platform layers can build one storage contract and let apps bring the final runtime.

# 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:

- auth state
- billing state
- cache entries
- independent key-value records
- rate limits
- audit or event logs
- observability snapshots
- feature flags
- framework-owned metadata

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:

- one Prisma implementation
- one Drizzle implementation
- one SQL-only setup path
- one MongoDB implementation
- one document-store implementation
- duplicated install docs
- duplicated error handling

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

```ts title="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:

- runtime execution
- generated Prisma output
- generated Drizzle output
- safe SQL output
- setup/bootstrap helpers

### 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.

```ts title="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:

```ts title="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

```ts title="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:

- the framework writes the storage layer once
- Farming Labs ORM handles the translation across the supported database and ORM
  stacks underneath

### 5. Use capabilities instead of backend guesses

When a framework needs behavior decisions, inspect capabilities:

```ts title="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

```ts title="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:

```ts title="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:

- one framework-owned install shape
- one starter-kit setup shape
- one demo/bootstrap shape
- one test setup shape

instead of one flow per backend family.

## Where this fits best

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

- "bring your own database or ORM client"
- "one storage contract, many execution stacks"
- "one docs story and one setup story instead of many adapter stories"
- "framework-owned modules without framework-owned adapter sprawl"

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

## Best next pages

- framework-scale examples:
  [Full-Stack Frameworks](/docs/use-cases/fullstack-frameworks)
- shared platform package examples:
  [Internal Platforms](/docs/use-cases/internal-platforms)
- support coverage by stack:
  [Support Matrix](/docs/integrations/support-matrix)