Browse documentation

Type registry

FlowpanelTypes, FlowpanelResources, and the types that resolve through them.

FlowPanel ships no code generator and reads no schema file. Everything it knows about your types, it learns from two empty interfaces you augment:

declare module "@flowpanel/kit" {
  interface FlowpanelTypes {
    db: typeof db;
  }
  interface FlowpanelResources {
    users: typeof schema.users.$inferSelect;
    orders: typeof schema.orders.$inferSelect;
  }
}

Both are declared in @flowpanel/core and re-exported by @flowpanel/kit. Augment whichever specifier you actually import — declaration merging targets the module you name, and for a kit install that is "@flowpanel/kit".

Everything on this page is opt-in. Leave both interfaces empty and the framework still runs; the types simply degrade to unknown / string / Record<string, unknown>, and the checks move from compile time to defineAdmin's introspect-time validation.

FlowpanelTypes

A one-key registry for framework-wide type bindings. Today it carries db.

interface FlowpanelTypes {}

InferDB

type InferDB = FlowpanelTypes extends { db: infer D } ? D : unknown;

InferDB is the default Db parameter on three declarations, which is where augmenting it pays off:

DeclarationReached from
WidgetContext<DB = InferDB>every widget query, a StatItem.value resolver, a custom() widget's props resolver
ActionContext<Db = InferDB>every row / bulk / dashboard / drawer action run — those signatures name ActionContext<InferDB> explicitly
Adapter<DB = InferDB, Ref>the adapter's own db property

Augment FlowpanelTypes["db"] once and all three carry your real client with no per-callsite annotation; skip it and they are unknown, so every use needs a cast.

declare module "@flowpanel/kit" {
  interface FlowpanelTypes {
    db: typeof db;
  }
}

The adapter-facing contexts — QueryContext<Db = unknown> and the ListQueryContext / ItemQueryContext / MutationContext built on it — default their Db to unknown instead, and take it as an explicit parameter. An adapter binds it once (ListQueryContext<Row, MyDb>); a FieldDef.options or defaultValue resolver, which receives a bare QueryContext, sees ctx.db as unknown and has to narrow it.

FlowpanelResources

A map of resolved resource name → row type. "Resolved" is the name defineAdmin keys the resource by: a Drizzle table declared pgTable("ai_usage", …) is "ai_usage" even when the exported variable is aiUsage, unless the resource sets options.name.

interface FlowpanelResources {}

InferRow

type InferRow<Ref> = Ref extends { $inferSelect: infer R }
  ? R
  : Ref extends keyof FlowpanelResources
    ? FlowpanelResources[Ref]
    : Record<string, unknown>;

This is what turns resource(ref, options) into ResourceOptions<InferRow<Ref>>, and therefore what makes columns, search, filters[].field, defaultSort.field, rowKey, and every FieldDef.name type-check against real column names. Three cases, in order:

  1. A Drizzle table — anything with $inferSelect — resolves through it, with no registry entry needed.
  2. A string ref that is a key of FlowpanelResources resolves to that row type. This is the Prisma path: resource("User", …) needs interface FlowpanelResources { User: User }.
  3. Anything else falls back to Record<string, unknown>, whose keys are string — loose, but it compiles.

ResourceName

type ResourceName = [keyof FlowpanelResources] extends [never]
  ? string
  : keyof FlowpanelResources & string;

The type of every place one config points at another resource by name: a drawer or detail tab's resource, and a table({ resource }) widget. While the registry is empty it is plain string, so nothing breaks for apps that never augment it. Add one entry and every such string is checked against the registry — with a "Did you mean?" suggestion from defineAdmin on top, which runs whether or not the compiler caught it.

ReferenceSpec

type ReferenceSpec = [keyof FlowpanelResources] extends [never]
  ? { resource: string; labelField: string }
  : {
      [R in keyof FlowpanelResources]: {
        resource: R;
        labelField: keyof FlowpanelResources[R] & string;
      };
    }[keyof FlowpanelResources];

A foreign-key target, used by ColumnDef.reference (renders the FK as a looked-up label linking to the target row) and FieldDef.reference (renders a searchable picker).

The augmented form is a distributed union, which is the point: it pairs each resource name with that resource's own keys, so labelField cannot name a column of a different table.

{ field: "userId", reference: { resource: "users", labelField: "email" } }
//                                                  ^ keyof users row, not of orders

Unaugmented, both members are plain string and nothing is checked at compile time.

What this buys you, per level

Augmentedctx.dbColumn namesCross-resource names
NothingunknownDrizzle: checked. Prisma: stringstring
FlowpanelTypes.dbyour clientunchangedunchanged
FlowpanelResourcesunchangedchecked for string refs toochecked, with suggestions

defineAdmin re-checks resource names and column names at config time in every row of that table, so the registry buys you earlier errors, not the only errors.

Where to put the augmentation

One place. declare module merges globally, so repeating the block per resource file only risks conflicting entries — keep it in flowpanel.config.ts, or in a dedicated flowpanel.d.ts that is included by your tsconfig.json. See One resource per file.