Designing Your Data Model with Jumpstart

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:

  1. Finds the RWK column on invoice_status (which is name)
  2. Synthesizes an invoice_status_name column on the InvoiceView class
  3. Generates a GET /api/invoicestatus/enum endpoint returning [{ id, name }] pairs
  4. Renders a dropdown select control in the Blazor edit form, populated from that endpoint
  5. Generates the appropriate LEFT JOIN in 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:

  1. Registers invoice_line as a child of invoice
  2. Generates a GET /api/invoice/children/{id}?child=invoiceline endpoint on the invoice controller
  3. Renders a child tab on the Blazor invoice edit page that loads and displays the related invoice lines
  4. 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:

  1. A composite unique index on the mapping columns (principal_id, role_id)
  2. Standard foreign key constraints on both sides
  3. 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:

  1. Finds the referenced table’s RWK column(s)
  2. Synthesizes a display column on the view (e.g., customer_name from customer.name)
  3. Registers a LEFT OUTER JOIN clause 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 nameProcessView 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:

ColumnTypePurpose
is_activeintegerSoft delete flag — 1 is active, 0 is deleted
created_byvarchar(50)Username of the creator
last_updatedtimestampTimestamp of the last modification
last_updated_byvarchar(50)Username of the last modifier
txn_idbigintOptimistic 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:

SchemaTablePurpose
apporgMulti-organization support
appprincipalUsers
appprincipal_orgUser-to-org membership
secoperationPermissioned operations
secop_roleAuthorization roles
secop_role_mapOperation-to-role assignments
secop_role_memberUser-to-role membership
coreworkflowWorkflow definitions
coreexec_logExecution history
corescriptStored scripts (C#, PowerShell, Python)
corescheduleScheduling configuration
coreserver_nodeRegistered 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.NETSQL ServerUI Control
BIGINTlongbigintNumber
INTEGERintintNumber
NUMERICdecimaldecimalNumber
VARCHAR(n)stringnvarchar(n)Text
TEXTstringnvarchar(MAX)TextArea
BOOLEANboolbitRadio
DATEDateTimedateDate
TIMESTAMPDateTimedatetime2Date
UUIDGuiduniqueidentifierText

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.