The adapter is the only part of FlowPanel that talks to your database.
Implement this interface to support an ORM that has no shipped adapter — nothing
else in the framework issues a query.

Shipped adapters: [`drizzleAdapter`](/docs/introduction/drizzle) and
[`prismaAdapter`](/docs/introduction/prisma).

## Adapter

**Adapter** — What FlowPanel needs from a database layer. Implement it to support an ORM
that has no shipped adapter — nothing else in the framework talks to the DB.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `AdapterKind` | yes | Which ORM this adapter wraps. Shipped adapters use `"drizzle"` / `"prisma"`. |
| `db` | `DB` | yes | The client handed to every `ctx.db`. |
| `transaction` | `function` | no | Execute work against one transaction-bound database handle. |
| `introspect` | `function` | yes | Describe a table: its columns, types and primary key. |
| `inferSchema` | `function` | yes | Derive validation schemas from the table, used when a resource sets no `schema`. |
| `list` | `function` | yes | One page of rows, honoring filters, sort, search, scope and soft delete. |
| `get` | `function` | yes | A single row by id, or null. Must respect tenant scope. |
| `create` | `function` | yes | Insert a row. Must reject rows that fall outside the caller's scope. |
| `update` | `function` | yes | Update a row by id, or return null when it does not exist in scope. |
| `delete` | `function` | yes | Delete a row — or stamp `ctx.softDelete.column` when soft delete is on. |
| `restore` | `function` | no | Clear the soft-delete stamp. Without it, the admin hides the restore button. |
| `applyMigration` | `function` | no | Optional migration support, used by `flowpanel migrate`.
The CLI's earlier `listAppliedMigrations` result may be stale. Implementations
that support concurrent migrators must serialize and recheck `id` at the
database boundary. |
| `runMigrationSql` | `function` | no |  |
| `listAppliedMigrations` | `function` | no |  |
| `markMigrationApplied` | `function` | no |  |

`Ref` is whatever you accept in `resource(ref, …)` — a Drizzle table object, a
Prisma delegate, or your own descriptor. `DB` is the client you expose as
`ctx.db` everywhere.

`kind` is `AdapterKind` — `"drizzle" | "prisma" | (string & {})`. The shipped
kinds autocomplete; a third-party adapter picks its own label. Nothing in
FlowPanel branches on it.

`kind`, `db`, `introspect`, `inferSchema`, `list`, `get`, `create`, `update`
and `delete` are required. `restore` is optional — an adapter without it makes
the admin hide the restore button rather than fail at runtime. `applyMigration`
and `listAppliedMigrations` are optional too and power `flowpanel migrate`;
without both, that command exits before executing SQL. The older
`runMigrationSql` and `markMigrationApplied` hooks remain typed for adapter
compatibility. The CLI can compose both as a temporary upgrade path and prints
a warning because that legacy pair cannot keep SQL and its bookkeeping record
atomic. Implement `applyMigration(id, sql)` before relying on rollback safety.

Both shipped adapters execute a migration and write its applied ID through one
adapter operation. The CLI's earlier applied-ID list is only a snapshot, so an
adapter that permits concurrent migrators must serialize and recheck the ID at
the database boundary. A duplicate ID becomes a no-op only after that recheck.

PostgreSQL and SQLite keep every statement and the marker in one transaction —
Drizzle and Prisma both take a lock inside it (a PostgreSQL advisory lock, a
SQLite write lock) and recheck the ID before executing anything.

MySQL implicitly commits DDL, so no tool can promise rollback for an arbitrary
MySQL file: review migrations, make DDL restart-safe, back up production data,
and apply them in a controlled release step. Drizzle pins one connection and
holds a `GET_LOCK` advisory lock. Prisma cannot pin a connection, so it claims
a durable row in `_flowpanel_migration_claims` instead: the claim serializes
migrators, survives a crashed one, and records the statement a failed run
stopped at. A failed claim stops later runs until you repair the schema and
delete that row, so a half-applied file is never silently retried.

Because the lock protocol and the SQL dialect differ per provider,
`prismaAdapter` takes a required `provider` — the same value as the
`datasource` block in `schema.prisma`.

Both adapters split ordinary multi-statement SQL safely. They reject
dialect-specific procedural scripts (for example SQLite/MySQL trigger bodies or
stored procedures) before execution unless the body uses PostgreSQL dollar
quoting, and Prisma also rejects MySQL executable comments and client
directives such as `DELIMITER`, `SOURCE` and `.read`. Apply those files through
the ORM's native migration workflow or a dialect-native runner; a rejected file
is never recorded as applied.

## ResourceIntrospection

Returned by `introspect`. This is what lets a resource declare `columns: ["id",
"email"]` and get working controls, filters and validation for free.

**ResourceIntrospection** — What the adapter reports about one table.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | yes | Table name, used as the resource's default `name`. |
| `columns` | `array` | yes |  |
| `primaryKey` | `string` | yes |  |

## ColumnMeta

**ColumnMeta** — One column, as the adapter sees it. Drives inferred controls and validation.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | yes |  |
| `type` | `"reference" \| "array" \| "enum" \| "json" \| "date" \| "boolean" \| "number" \| "string"` | yes | Normalized type. Decides the default form control and filter. |
| `nullable` | `boolean` | yes |  |
| `unique` | `boolean` | yes |  |
| `primaryKey` | `boolean` | yes |  |
| `enumValues` | `array` | no | Allowed values, for `enum` columns. |
| `references` | `object` | no | Foreign key target, when the column references another table. |
| `maxLength` | `number` | no |  |
| `readable` | `boolean` | no | Whether this column may be selected and returned by the adapter. |
| `writableOnCreate` | `boolean` | no | Whether create input may contain this column. |
| `writableOnUpdate` | `boolean` | no | Whether update input may contain this column. |
| `generated` | `boolean` | no | Database-computed column that must not be accepted as input. |
| `sensitive` | `boolean` | no | Adapter-discovered secret. Always excluded from generated read projections. |

`type` is consulted in exactly one place: the fully generated form a resource
gets when it declares neither `create.fields` nor `update.fields`. The mapping
is short, and everything not listed falls through to a text input:

| `ColumnMeta.type` | Generated form control |
| --- | --- |
| `number` | `number` |
| `boolean` | `checkbox` |
| `date` | `datetime-local` |
| `json` | `json` |
| `string`, `enum`, `array`, `reference` | `text` |

Two things this table does **not** promise:

- **There is no filter inference.** A bare string in `filters` always resolves
  to a `text` filter regardless of the column's type — declare a `FilterDef`
  with an explicit `type` for anything else.
- **A declared field is not inferred either.** A `FieldDef` without `type`
  renders as `"text"`, or `"reference"` when it sets `reference`. `enum`,
  `array` and `reference` columns therefore need an explicit `FieldDef.type`
  (plus `options` / `reference`) to get a richer control.

`enumValues` and `references` are reported for your own use — nothing in the
framework turns them into controls on their own.

## What an implementation must honor

The interface is small, but three of its obligations are load-bearing. Get them
wrong and the admin is still functional — just unsafe.

**Tenant scope.** When `ctx.scopeRequired` is true, every method must constrain
its query with `ctx.applyScope`. `create` included: writing a row outside the
caller's scope is as much a leak as reading one.

**The search allowlist.** `list` may only search columns in `ctx.searchFields`.
Searching every text column turns the search box into a way to confirm values in
columns the operator was never shown.

**Soft delete.** When `ctx.softDelete` is set, `list` and `get` must exclude
stamped rows unless `ctx.includeDeleted` is set, and `delete` must stamp the
column rather than remove the row.

## Declared columns are the boundary

FlowPanel projects every row through the resource's declared surface before it
crosses to the client. An adapter that returns extra columns is not a leak by
itself — but the projection is what makes that safe, so do not bypass it in
custom routes of your own.

## Adapter options

Each shipped adapter takes its own options shape.

```ts
export function drizzleAdapter<DB>(opts: DrizzleAdapterOptions<DB>): Adapter<DB, Table>;
```

```ts
export function prismaAdapter<P>(opts: PrismaAdapterOptions<P>): Adapter<P, string>;
```

Wrap a set of BullMQ Queue instances into a FlowPanel BullMQ adapter.

```ts
export function bullmqAdapter(queues: Record<string, Queue>): BullMQAdapter;
```

**DrizzleAdapterOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `db` | `DB` | yes |  |
| `schema` | `Record<string, unknown>` | yes |  |
| `dialect` | `DrizzleDialect` | no | Inferred from `schema` when omitted. |

**PrismaAdapterOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `prisma` | `P` | yes |  |
| `provider` | `PrismaProvider` | yes | The `datasource` provider from schema.prisma; migrations are dialect-specific. |
| `dmmf` | `object` | no |  |

**BullMQAdapterOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `queues` | `Record<string, object>` | yes |  |

`bullmqAdapter` is the exception: it takes the queue map itself, not an
options object — `bullmqAdapter({ email: emailQueue, scrape: scrapeQueue })`.
`BullMQAdapterOptions` is exported for symmetry and nothing consumes it.

## Also exported

Each adapter package exposes the pieces its own `Adapter` is assembled from, so
you can reuse one without adopting the whole adapter — a script that only needs
column metadata, or a custom adapter that wants Drizzle's schema inference.

| Package | Also exports |
| --- | --- |
| `@flowpanel/kit/drizzle` | `introspect`, `inferSchema`, `toCsv`, `toJson` |
| `@flowpanel/kit/prisma` | `introspect`, `inferSchema`, `PrismaDmmf` (type) |
| `@flowpanel/kit/bullmq` | `BullMQAdapter` (type) |

`introspect(ref)` returns the same `ResourceIntrospection` the framework reads;
`inferSchema(ref)` returns the `{ create, update, select }` Zod trio a resource
falls back to when it declares no `schema`. `PrismaDmmf` is the shape
`prismaAdapter`'s optional `dmmf` option accepts.

## Export helpers

`@flowpanel/kit/drizzle` also exports `toCsv` and `toJson` — standalone
row serializers, not wired into any route by default.

Serialize an array of row objects to CSV.

```ts
export function toCsv<Row extends Record<string, unknown>>(rows: Row[], fields: string[]): string;
```

Serialize an array of row objects to a JSON string projecting only `fields`.

```ts
export function toJson<Row extends Record<string, unknown>>(rows: Row[], fields: string[]): string;
```

Both project each row down to `fields` before serializing, the same
declared-columns discipline as the rest of the framework. `toCsv` quotes
values containing a comma, quote, or newline and stringifies `Date`s to ISO
and objects to JSON; `toJson` returns a JSON string of the projected rows.
Use them from a custom export route when `DataTable`'s built-in `exportable`
option — which downloads only the rows currently on screen — isn't enough.
