App Metadata Overlay
The app-metadata overlay is a presentation layer that describes how an application client should render the entities BifrostQL exposes: human-readable labels, form widgets, list grids, saved views, and relationship presentation. It is standalone JSON, served to SPA and React Native clients, and it sits on top of — without replacing — the existing schema-metadata system.
Why a separate layer
Section titled “Why a separate layer”BifrostQL already has a schema-metadata system: the dbo.table { key: value } rule grammar that controls how the GraphQL API itself behaves — tenant filters, soft-delete, EAV flattening, policy enforcement. That metadata is consumed server-side, on the query and mutation paths.
The overlay answers a different question: how should a client present this entity to a user? That is a UI concern, not an API-behavior concern, so it lives in its own layer with its own contract.
| Schema metadata | App-metadata overlay | |
|---|---|---|
| Purpose | Controls API behavior (filtering, mutations, security) | Controls client presentation (labels, forms, grids) |
| Grammar | dbo.table { key: value } rule strings | Standalone camelCase JSON |
| Consumed by | BifrostQL server (query/mutation pipeline) | SPA / React Native clients |
| Model type | DbModel, MetadataKeys | AppMetadataModel, EntityMetadata |
| Loader | MetadataLoader, IMetadataSource | AppMetadataLoader, IAppMetadataSource |
| Keyed by | Qualified table name | Qualified table name |
Both layers are keyed by qualified table name (e.g. dbo.users), so the overlay aligns with DbModel tables without modifying them.
How the two coexist
Section titled “How the two coexist”The overlay is a deliberately separate, coexisting pipeline. It is loaded independently, exposed independently, and is never merged into the schema-metadata system:
AddBifrostAppMetadata(...)registers the overlay as its own singletonAppMetadataModel. It is purely additive — it touches no service registered byAddBifrostQL, and can be added before, after, or omitted entirely with no effect on the schema-metadata pipeline.- The overlay deliberately does not reuse the
{ }rule-delimiter grammar. It is plain JSON. - Schema-metadata rules and overlay entries can both describe the same table; neither overrides the other because they govern different concerns.
Because the layers are independent, adding an overlay never changes existing API behavior, and all existing schema-metadata tests stay green.
The overlay shape
Section titled “The overlay shape”AppMetadataModel is a pure data aggregate — no database or GraphQL dependency — describing entities keyed by qualified table name:
EntityMetadata— label, icon, display fields, navigation placement, plus nestedFields,Grid, andRelationships.FieldMetadata— widget hint, validation rule, visibility, read-only, help text, layout group.GridPresetMetadata— default columns, filters, sort, namedSavedViews, and bulk actions.RelationshipMetadata— target entity (by qualified table name), relationship kind (foreignKeySelector,childCollection,nestedPanel), foreign-key field, and display columns.
Loading the overlay
Section titled “Loading the overlay”Overlay entries come from one or more IAppMetadataSource instances, merged in priority order by AppMetadataLoader:
FileAppMetadataSource— reads the overlay from a JSON file on disk (low priority). A missing file yields an empty overlay.DatabaseAppMetadataSource— reads the overlay from a database table, one entity per row (higher priority).CompositeAppMetadataSource— merges several sources; when more than one supplies the same qualified table name, the higher-priority source wins.
services.AddBifrostAppMetadata(new IAppMetadataSource[]{ new FileAppMetadataSource("app-metadata.json"), // priority 0 new DatabaseAppMetadataSource(connectionString), // priority 100});Serving the overlay to clients
Section titled “Serving the overlay to clients”The overlay is exposed to SPA and React Native clients through a GET endpoint, following the same middleware pattern BifrostQL uses for its info endpoint:
app.UseBifrostAppMetadata(); // GET /_app-metadata// or with optionsapp.UseBifrostAppMetadata(o => o.Path = "/meta");The endpoint serves the overlay as the stable camelCase JSON contract defined by AppMetadataJson — the same portable, RN-friendly shape the model serializes to. When no overlay is registered, the endpoint returns an empty overlay rather than 404, so clients always receive the stable contract.
Example: a Membership Manager
Section titled “Example: a Membership Manager”A Membership Manager application tracks members, households, dues, and events. With the overlay, every presentation concern is data:
- Labels and navigation —
members→ “Members”, iconperson, nav placementmain. - Forms — each field carries a widget (
text,select,datepicker), a layout group, and optional validation/help text, so a client renders the form generically with no entity-specific form code. - Grids — each entity’s
GridPresetMetadatagives default columns, sort, filters, and saved views, so the list view needs no hardcoded grid. - Relationships —
members → households(foreign-key selector),members → dues(child collection),events → households— every target resolves to another overlay entity by qualified table name.
The result: a client can describe and render all four entities purely from the overlay JSON, with no hardcoded forms or grids.