# Full-Stack Frameworks
URL: /docs/use-cases/fullstack-frameworks
LLM index: /llms.txt
Description: How full-stack frameworks can integrate Farming Labs ORM without owning every backend-specific storage branch.

# Full-Stack Frameworks

Full-stack frameworks usually end up owning more storage-facing behavior than
they planned.

Even when the framework does not call itself an ORM or a database layer, it
often still needs a durable place for:

- auth and identity
- teams, organizations, and permissions
- billing and plan state
- CMS-like content
- route caches and render metadata
- background job state
- webhook logs and retries
- feature flags and rollout state
- environments, tenants, and project settings
- starter templates and example apps

The hard part is usually not query syntax. The hard part is having to explain,
support, and maintain the same storage story across several different runtime
stacks.

## Why frameworks drift so quickly

Frameworks often end up owning a surprising number of storage surfaces at once:

- framework core modules
- official add-ons
- starter kits
- example applications
- CLI setup flows
- test fixtures
- docs snippets

If each surface assumes a different backend shape, the framework slowly drifts
into:

- Prisma-only assumptions in one package
- Drizzle-specific examples in docs
- raw SQL helpers in workers
- duplicated migrations or setup stories
- plugin modules that only work with one storage style

Farming Labs ORM helps by letting the framework define and consume one
storage-facing contract while still letting the application choose the final
runtime.

## Where Farming Labs ORM fits inside a framework

For framework authors, the most useful pattern is:

1. define the framework-owned schema contract once
2. accept a raw runtime client from the app
3. create the ORM from that runtime
4. expose framework modules that talk to the unified runtime
5. use runtime-aware setup helpers in tests, templates, and examples

That keeps the framework focused on product behavior instead of backend
plumbing.

## A practical framework architecture

### 1. Keep the schema in a shared package

```ts title="framework-schema.ts"
import {
  datetime,
  defineSchema,
  enumeration,
  hasMany,
  id,
  model,
  string,
  belongsTo,
} from "@farming-labs/orm";

export const frameworkSchema = defineSchema({
  user: model({
    table: "users",
    fields: {
      id: id(),
      email: string().unique(),
      name: string(),
      createdAt: datetime().defaultNow(),
    },
    relations: {
      projects: hasMany("projectMembership", { foreignKey: "userId" }),
    },
  }),

  project: model({
    table: "projects",
    fields: {
      id: id(),
      slug: string().unique(),
      name: string(),
      environment: enumeration(["development", "preview", "production"]),
      createdAt: datetime().defaultNow(),
    },
    relations: {
      memberships: hasMany("projectMembership", { foreignKey: "projectId" }),
      routeCaches: hasMany("routeCache", { foreignKey: "projectId" }),
      webhooks: hasMany("webhookDelivery", { foreignKey: "projectId" }),
      jobs: hasMany("jobRun", { foreignKey: "projectId" }),
    },
  }),

  projectMembership: model({
    table: "project_memberships",
    fields: {
      id: id(),
      userId: string().references("user.id"),
      projectId: string().references("project.id"),
      role: string().default("member"),
      createdAt: datetime().defaultNow(),
    },
    relations: {
      user: belongsTo("user", { foreignKey: "userId" }),
      project: belongsTo("project", { foreignKey: "projectId" }),
    },
  }),

  routeCache: model({
    table: "route_cache",
    fields: {
      id: id(),
      projectId: string().references("project.id"),
      key: string().unique(),
      payload: string(),
      createdAt: datetime().defaultNow(),
    },
    relations: {
      project: belongsTo("project", { foreignKey: "projectId" }),
    },
  }),

  webhookDelivery: model({
    table: "webhook_deliveries",
    fields: {
      id: id(),
      projectId: string().references("project.id"),
      endpoint: string(),
      eventType: string(),
      status: string(),
      createdAt: datetime().defaultNow(),
    },
    relations: {
      project: belongsTo("project", { foreignKey: "projectId" }),
    },
  }),

  jobRun: model({
    table: "job_runs",
    fields: {
      id: id(),
      projectId: string().references("project.id"),
      jobType: string(),
      status: string(),
      createdAt: datetime().defaultNow(),
    },
    relations: {
      project: belongsTo("project", { foreignKey: "projectId" }),
    },
  }),
});
```

This does not mean the framework must own every model in one giant schema file.
In practice, many frameworks will compose several schema packages:

- auth schema
- billing schema
- organization schema
- cache or job schema

The important part is that the framework defines those contracts once instead of
rewriting them per backend.

If the framework also wants to render artifacts directly, it can call the
generators from the same schema package:

```ts
import { renderDrizzleSchema, renderPrismaSchema, renderSafeSql } from "@farming-labs/orm";
import { frameworkSchema } from "./framework-schema";

const prisma = renderPrismaSchema(frameworkSchema, {
  provider: "postgresql",
});

const drizzle = renderDrizzleSchema(frameworkSchema, {
  dialect: "pg",
});

const sql = renderSafeSql(frameworkSchema, {
  dialect: "postgres",
});
```

That is especially useful when the framework owns:

- official starter kits
- a setup CLI
- example apps
- documentation snippets
- self-hosted installation artifacts

### 2. Accept the raw client at the framework boundary

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

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

This is one of the most valuable integration points because the framework does
not need to force the app to manually build the ORM first.

The framework can simply say:

1. pass your raw Prisma, Drizzle, Kysely, MikroORM, SQL, Mongo, or Mongoose client
2. let the framework normalize it through Farming Labs ORM
3. use the framework modules on top of that normalized runtime

If the framework wants to explain integration errors more clearly, inspect the
runtime before constructing the ORM:

```ts
import { inspectDatabaseRuntime } from "@farming-labs/orm";

const inspection = inspectDatabaseRuntime(database);

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

### 3. Write framework storage helpers once

```ts title="framework-store.ts"
export function createFrameworkStore(orm: Awaited<ReturnType<typeof createFrameworkOrm>>) {
  return {
    findProjectBySlug(slug: string) {
      return orm.project.findUnique({
        where: { slug },
        select: {
          id: true,
          slug: true,
          name: true,
          environment: true,
        },
      });
    },

    findUserByEmail(email: string) {
      return orm.user.findUnique({
        where: { email },
      });
    },

    putCachedRoute(input: { projectId: string; key: string; payload: string }) {
      return orm.routeCache.upsert({
        where: { key: input.key },
        create: input,
        update: {
          payload: input.payload,
        },
      });
    },

    recordWebhookAttempt(input: {
      projectId: string;
      endpoint: string;
      eventType: string;
      status: string;
    }) {
      return orm.webhookDelivery.create({
        data: input,
      });
    },

    createJobRun(input: { projectId: string; jobType: string; status: string }) {
      return orm.jobRun.create({
        data: input,
      });
    },
  };
}
```

That keeps the framework code focused on framework behavior instead of backend
plumbing.

## Concrete places a full-stack framework can use the ORM

This is where the ORM tends to fit naturally.

### Framework auth package

A framework can own:

- users
- sessions
- linked accounts
- organization membership

while still accepting a raw Prisma, SQL, Mongo, or Drizzle runtime from the
application.

### Framework billing package

A framework can ship:

- plans
- subscriptions
- invoices
- usage events

as one shared billing contract across starter apps and self-hosted
deployments.

### Framework teams and permissions module

Many frameworks eventually need:

- projects
- organizations
- memberships
- roles
- feature ownership

Those are classic shared-contract models that benefit from one schema source of
truth.

### Framework route cache or render metadata module

A framework that owns route output caching, prerender metadata, or invalidation
state can keep one portable record shape instead of documenting it differently
for every backend.

### Framework CMS or admin module

If the framework wants to ship content, admin, or internal tooling modules, it
can define content entities once while still letting each app choose where those
records live.

### Framework jobs and workflows module

If the framework ships background jobs or workflow state, a unified runtime
surface is useful for:

- job creation
- status transitions
- retries
- dead-letter handling
- operator dashboards

### Framework webhooks and integration logs

Frameworks that coordinate external services usually need durable storage for:

- delivery attempts
- retry state
- payload metadata
- endpoint configuration

Those records are a good fit for one shared framework contract.

### Framework feature flags and environments module

If a framework owns environment variables, rollout flags, or preview-deployment
state, those are exactly the kinds of cross-cutting records that drift when each
backend gets its own interpretation.

### Framework starter kits and example apps

Even if the framework does not expose all of its internal models to users,
starter kits still benefit from one shared schema contract. That keeps:

- docs snippets
- demo apps
- test fixtures
- CLI templates

closer to the real product surface.

## How full-stack frameworks should structure the integration

The cleanest approach is usually to split responsibilities like this:

### Framework package responsibilities

- own the reusable schema contracts
- own the storage helpers for framework modules
- own runtime inspection and capability-driven branching
- own example and test bootstrap flows

### Application responsibilities

- choose the raw database client
- choose the final runtime stack
- choose whether to generate artifacts or stay runtime-first
- decide whether the framework schema is merged with app-owned schemas

That separation keeps the framework portable without taking runtime choice away
from the app.

## Runtime-first, generation-first, and hybrid integration

There are three common patterns.

### Runtime-first

Use this when the framework mostly wants a runtime contract and wants the app to
provide a raw client directly.

```ts
const orm = await createOrmFromRuntime({
  schema,
  client,
});
```

This is a great fit when:

- the framework wants a small integration API
- the app already owns its database client
- the framework does not need to own migrations directly

### Generation-first

Use this when the framework also wants to ship generated artifacts or starter
projects.

```ts title="farm-orm.config.ts"
export default defineConfig({
  schemas: [frameworkSchema],
  targets: {
    prisma: {
      out: "./generated/prisma/schema.prisma",
      provider: "postgresql",
    },
  },
});
```

This is a great fit when:

- the framework ships official app templates
- the framework wants generated Prisma, Drizzle, or SQL output
- the app wants to keep its own migration workflow

### Hybrid

Many frameworks will want both:

- generation for starter apps, docs, and templates
- runtime helpers for direct integration APIs

That is often the best long-term story for a full-stack framework because it
supports:

- framework-owned examples
- application-owned runtime choice
- package-level reuse
- docs that stay aligned with the actual schema

## Setup flows in dev, tests, and templates

Frameworks often need to bootstrap a real database for:

- e2e suites
- example apps
- local dev servers
- preview deployments
- template validation in CI

Use the runtime-aware setup helpers for that:

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

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

That gives the framework one runtime-aware setup story instead of one setup
path per backend family.

If the framework wants setup without immediately returning the ORM client, it
can split the flow:

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

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

That is useful for:

- CLI setup commands
- starter-template initialization
- test harness bootstrapping
- self-hosted installation steps

## Helper map for framework authors

- `renderPrismaSchema(...)`: generate Prisma artifacts from framework-owned schemas
- `renderDrizzleSchema(...)`: generate Drizzle schema modules for starters or examples
- `renderSafeSql(...)`: generate direct SQL artifacts for setup and self-hosting
- `inspectDatabaseRuntime(...)`: inspect what raw client the app actually passed
- `createOrmFromRuntime(...)`: normalize the app's runtime into the framework ORM
- `pushSchema(...)`: set up framework-owned tables before app startup or tests
- `bootstrapDatabase(...)`: set up the database and return the framework ORM in one step

## Capabilities matter for framework authors

Frameworks usually need to branch on behavior more carefully than app code.

```ts
const caps = orm.$driver.capabilities;

caps.numericIds;
caps.supportsSchemaNamespaces;
caps.supportsTransactions;
caps.upsert;
caps.returningMode.update;
caps.nativeRelations.filtered;
caps.textMatching;
```

That helps a framework choose:

- whether generated numeric IDs are available
- whether Postgres schema namespaces are usable
- whether a transaction-backed flow is safe
- whether relation-heavy framework helpers need a fallback path
- whether setup/bootstrap should be exposed publicly or kept framework-owned
- whether a given runtime is suitable for a specific framework module

## Error handling at the framework boundary

Frameworks should usually convert ORM failures into framework-facing errors at
the integration boundary.

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

try {
  await orm.user.create({
    data: {
      email: "ada@farminglabs.dev",
      name: "Ada",
    },
  });
} catch (error) {
  if (isOrmError(error)) {
    switch (error.code) {
      case "UNIQUE_CONSTRAINT_VIOLATION":
        // return framework-level validation error
        break;
      case "MISSING_TABLE":
        // return setup/configuration error
        break;
      case "TRANSACTION_CONFLICT":
        // optional retry or queue handoff
        break;
    }
  }

  throw error;
}
```

That keeps the framework from learning every backend's native error format and
lets it expose stable framework-facing error semantics instead.

## When this is especially valuable

Farming Labs ORM is especially useful for frameworks that:

- ship official modules across several backends
- maintain starter kits or example apps
- want to support both hosted and self-hosted installs
- own storage-facing product features, not just UI helpers
- want one documentation story for many runtime choices

## When this is less useful

Farming Labs ORM is less compelling when a framework:

- only supports one backend forever
- wants to stay deeply tied to one ORM-native schema language
- never needs to share storage-facing contracts across packages or templates

## Practical rule of thumb

If your framework pain sounds like:

- "we keep rewriting the same storage docs"
- "we keep branching on backend details in core code"
- "our starter templates drift across stacks"
- "our platform modules assume one database wrapper"

then Farming Labs ORM is a strong fit.