Everything Jumpstart generates flows from a single CSV file. The quality and completeness of what you get out depends entirely on how well you describe your data model going in. This post is a practical reference for writing that metadata file — covering the CSV format, the three foreign key types, view synthesis, and the built-in columns you get for free.
The CSV Format
Each row in the metadata CSV describes a single column in a single table. Table-level metadata (schema, display label, navigation menu) is specified only on the first row of each table; subsequent rows for the same table only need the column-level fields.
TABLE_CATALOG,TABLE_SCHEMA,TABLE_NAME,TABLE_LABEL,NAV_MENU,COLUMN_NAME,COLUMN_LABEL,FK_TYPE,FK_TABLE,RWK,ORDINAL_POSITION,COLUMN_DEFAULT,IS_NULLABLE,DATA_TYPE,MSSQL_DATA_TYPE,CHARACTER_MAXIMUM_LENGTH
myapp,app,invoice,Invoice,Finance,id,Invoice ID,,,0,1,NULL,NO,BIGINT,bigint,NULL
myapp,app,invoice,,,invoice_number,Invoice Number,,,1,2,NULL,NO,VARCHAR,nvarchar,50
myapp,app,invoice,,,invoice_date,Invoice Date,,,0,3,NULL,NO,DATE,date,NULL
myapp,app,invoice,,,total_amount,Total Amount,,,0,4,NULL,NO,NUMERIC,decimal,NULL
A few rules to keep in mind:
Every table must have an id column of type BIGINT as its primary key — the generator expects this and builds sequences and foreign keys around it.
The RWK column (Real World Key) marks columns that are the human-meaningful identifier for the entity. RWK columns appear in list views, form the unique index, and are used by the generator to synthesize display values wherever this table is referenced as a foreign key. Mark at least one column per table as RWK (1); mark the rest 0.
TABLE_CATALOG sets the application namespace and only needs to appear on the very first row of your entire CSV.
The Three Foreign Key Types
The FK_TYPE column is where the real expressiveness of Jumpstart’s metadata lives. Three values produce dramatically different generated behavior.
Enum (Lookup Tables)
Use FK_TYPE=enum for foreign keys that reference static lookup tables — the kind of tables that rarely change and exist to constrain a column to a fixed set of named options.
# The lookup table
myapp,core,invoice_status,Invoice Status,,id,Invoice Status ID,,,0,1,NULL,NO,BIGINT,bigint,NULL
myapp,core,invoice_status,,,name,Name,,,1,2,NULL,NO,VARCHAR,nvarchar,50
# The referencing column on invoice
myapp,app,invoice,,,invoice_status_id,Invoice Status,enum,invoice_status,,5,NULL,NO,BIGINT,bigint,NULL
When the generator sees FK_TYPE=enum, it:
- Finds the RWK column on
invoice_status(which isname) - Synthesizes an
invoice_status_namecolumn on theInvoiceViewclass - Generates a
GET /api/invoicestatus/enumendpoint returning[{ id, name }]pairs - Renders a dropdown select control in the Blazor edit form, populated from that endpoint
- Generates the appropriate
LEFT JOINin the view SQL
The result is a fully working status dropdown with no additional configuration.
Parent (One-to-Many Hierarchies)
Use FK_TYPE=parent for foreign keys that define a one-to-many relationship where the child belongs to the parent — invoice lines belonging to an invoice, comments belonging to a ticket, line items belonging to an order.
# invoice_line references invoice as its parent
myapp,app,invoice_line,Invoice Line,,id,Invoice Line ID,,,0,1,NULL,NO,BIGINT,bigint,NULL
myapp,app,invoice_line,,,invoice_id,Invoice,parent,invoice,,1,2,NULL,NO,BIGINT,bigint,NULL
myapp,app,invoice_line,,,description,Description,,,1,3,NULL,NO,VARCHAR,nvarchar,255
myapp,app,invoice_line,,,amount,Amount,,,0,4,NULL,NO,NUMERIC,decimal,NULL
When the generator sees FK_TYPE=parent, it:
- Registers
invoice_lineas a child ofinvoice - Generates a
GET /api/invoice/children/{id}?child=invoicelineendpoint on the invoice controller - Renders a child tab on the Blazor invoice edit page that loads and displays the related invoice lines
- Creates the standard foreign key constraint and index in DDL
Parent relationships are recursive — a child table can itself have a parent, creating arbitrarily deep hierarchies that are all navigable through the generated UI.
Map (Many-to-Many Junction Tables)
Use FK_TYPE=map for junction tables that implement many-to-many relationships. Both sides of the relationship are marked as map FKs on the junction table.
# A many-to-many between principal and role
myapp,sec,principal_role,Principal Role,,id,Principal Role ID,,,0,1,NULL,NO,BIGINT,bigint,NULL
myapp,sec,principal_role,,,principal_id,Principal,map,principal,,1,2,NULL,NO,BIGINT,bigint,NULL
myapp,sec,principal_role,,,role_id,Role,map,op_role,,1,3,NULL,NO,BIGINT,bigint,NULL
Map FKs generate:
- A composite unique index on the mapping columns (
principal_id,role_id) - Standard foreign key constraints on both sides
- Navigation between the related entities in the UI
Views: Synthesized Joins
Any table whose name ends with _view is treated as a SQL view rather than a base table. Views are the right place for read models that need to present human-readable display values alongside foreign key IDs.
# invoice_view joins invoice to customer and invoice_status
myapp,app,invoice_view,Invoice,,id,Invoice ID,,,0,1,NULL,NO,BIGINT,bigint,NULL
myapp,app,invoice_view,,,invoice_id,Invoice,parent,invoice,,1,2,NULL,NO,BIGINT,bigint,NULL
myapp,app,invoice_view,,,customer_id,Customer,enum,customer,,1,3,NULL,NO,BIGINT,bigint,NULL
The generator’s ProcessView pass recursively follows FK chains from the view definition. For each FK, it:
- Finds the referenced table’s RWK column(s)
- Synthesizes a display column on the view (e.g.,
customer_namefromcustomer.name) - Registers a
LEFT OUTER JOINclause with the appropriate ON condition
If the FK chain is deeper — for example, if customer itself has a FK to org with an RWK column name — ProcessView follows that chain too, synthesizing org_name and adding the additional JOIN.
The resulting CREATE VIEW SQL and the corresponding CustomerInvoiceView C# class are fully generated. List pages in the UI use view endpoints by default so users always see resolved display values, not raw integer IDs.
Audit Columns You Get for Free
You never need to define audit columns in your metadata CSV. Every non-view table automatically receives these columns via global.csv:
| Column | Type | Purpose |
|---|---|---|
is_active | integer | Soft delete flag — 1 is active, 0 is deleted |
created_by | varchar(50) | Username of the creator |
last_updated | timestamp | Timestamp of the last modification |
last_updated_by | varchar(50) | Username of the last modifier |
txn_id | bigint | Optimistic concurrency version number |
The persistence layer manages audit tracking automatically on every insert and update. When a record is inserted, the record is created normally with the is_active flag set to 1. The id and txn_id columns are set to the same value. As tables are updated, the prior record’s is_active flag is cleared and a new record is created with the values of the updated columns. The txn_id of the new record is incremented. The txn_id and the id are generated from the same sequence; all txn_id's within a table are unique. Thus, the txn_id is the literal primary key of each table in the database. The API’s history endpoint (GET /api/{entity}/history/{id}) uses a generated query to retrieve all records with the same id sorted by txn_id for full record of changes.
Built-In System Tables
Your metadata CSV defines your application’s domain entities. Jumpstart’s core.csv defines the plumbing that every application needs, merged in automatically:
| Schema | Table | Purpose |
|---|---|---|
app | org | Multi-organization support |
app | principal | Users |
app | principal_org | User-to-org membership |
sec | operation | Permissioned operations |
sec | op_role | Authorization roles |
sec | op_role_map | Operation-to-role assignments |
sec | op_role_member | User-to-role membership |
core | workflow | Workflow definitions |
core | exec_log | Execution history |
core | script | Stored scripts (C#, PowerShell, Python) |
core | schedule | Scheduling configuration |
core | server_node | Registered execution nodes |
These tables follow all the same conventions as your domain tables. They appear in the generated database, get controllers, get UI pages, and participate in the RBAC system like everything else.
Type Mapping Reference
The generator maps PostgreSQL types to .NET types, SQL Server types, and UI input controls automatically. Here are the most common mappings:
| PostgreSQL | .NET | SQL Server | UI Control |
|---|---|---|---|
BIGINT | long | bigint | Number |
INTEGER | int | int | Number |
NUMERIC | decimal | decimal | Number |
VARCHAR(n) | string | nvarchar(n) | Text |
TEXT | string | nvarchar(MAX) | TextArea |
BOOLEAN | bool | bit | Radio |
DATE | DateTime | date | Date |
TIMESTAMP | DateTime | datetime2 | Date |
UUID | Guid | uniqueidentifier | Text |
Specify DATA_TYPE using PostgreSQL names. Add MSSQL_DATA_TYPE if you need SQL Server support — otherwise the generator applies its default mapping.
Custom Routing with URI
By default, each entity’s API and navigation URLs are derived from its lowercase domain object name. A customer table gets /api/customer and a navigation link to /customer. If you need a different URL structure, the optional URI column overrides this:
# The workflow table in core.csv uses a custom URI
myapp,core,workflow,Workflow,,id,...,core/workflow
With that in place, the navigation menu links to /core/workflow instead of /workflow. This is useful for organizing built-in system tables under a namespace that keeps them visually distinct from your application’s domain entities.
Putting It Together
A well-designed metadata CSV follows a few practical conventions. Define lookup tables before the tables that reference them — while Jumpstart’s topological sort handles DDL ordering automatically, it makes the CSV easier to read. Group tables by schema or business domain. Be deliberate about RWK columns — they’re the columns that will appear in list views and dropdowns everywhere the entity is referenced, so pick columns that are genuinely human-readable and unique. Use views for any read model that needs to present data from multiple tables together.
The metadata file is the single source of truth for your application. Everything else is derived from it.