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:
| Declaration | Reached 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:
- A Drizzle table — anything with
$inferSelect— resolves through it, with no registry entry needed. - A string ref that is a key of
FlowpanelResourcesresolves to that row type. This is the Prisma path:resource("User", …)needsinterface FlowpanelResources { User: User }. - Anything else falls back to
Record<string, unknown>, whose keys arestring— 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 ordersUnaugmented, both members are plain string and nothing is checked at compile
time.
What this buys you, per level
| Augmented | ctx.db | Column names | Cross-resource names |
|---|---|---|---|
| Nothing | unknown | Drizzle: checked. Prisma: string | string |
FlowpanelTypes.db | your client | unchanged | unchanged |
FlowpanelResources | unchanged | checked for string refs too | checked, 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.