The Jumpstart tool produces a layered architecture to make the resulting application focused and easy to maintain. Running jumpstart model.csv produces a generated-app/ directory with a lot of files. This post is a guided tour of what those files are, how they relate to each other, and why the architecture is structured the way it is.
The Output Directory Structure
generated-app/
├── database/
│ ├── ddl/ Database schema scripts
│ └── data/ Seed data scripts
├── server/
│ ├── api/ REST API (ASP.NET Core 9)
│ ├── scheduler/ Schedules tasks and delegates to script agents
│ └── scripagent/ Executes PowerShell, C#, or Python on remote computers
├── shared/
│ ├── common/ Shared utilities and infrastructure
│ ├── domain/ Domain model classes
│ ├── logic/ Business logic layer
│ └── persist/ Data access layer
├── web/
│ └── blazor/ Blazor WebAssembly frontend
└── test/
├── test-api/ API integration tests
├── test-scriptagent/ Remote script execution tests
├── test-workflow/ Distributed workflow execution tests
├── test-script/ Script execution tests
└── test-persist/ Persist layer tests
The Database Layer
The database layer is pure SQL, generated for PostgreSQL or SQL Server (or both). For each entity in your model, the generator produces:
A CREATE TABLE statement with all columns, data types, nullability constraints, and default values. The primary key uses a generated sequence (BIGINT with a dedicated sequence object) rather than IDENTITY/SERIAL so that sequence values can be pre-allocated and controlled independently of inserts.
A unique index on the Real World Key columns — the columns you marked with RWK=1 in the metadata CSV. These are the human-meaningful identifiers for each entity and must be unique.
A history table ({table_name}_history) that mirrors the structure of the base table and is populated by the persistence layer on every update. This gives every entity a full audit trail without any application-level configuration.
Seed data scripts for lookup/enum tables, written as INSERT statements that are safe to re-run. The topological sort ensures parent seed data is always loaded before child seed data.
The database/ddl/build.sh (or build.ps1 / build.py) script runs the DDL in the correct order against your database.
The Domain Layer
The shared/domain/ directory contains C# partial classes for every entity. Each entity gets two files:
Customer.generated.cs (FORCE=TRUE) — the machine-generated partial class. Contains properties for every column, correctly typed (string, long, decimal, DateTime, etc.) with appropriate nullability. Never edit this file; it is overwritten on every regeneration.
Customer.user.cs (FORCE=FALSE) — a hand-editable partial class created once and left alone. This is where you add computed properties, validation logic, or any other customizations that belong on the domain object itself.
There are also View classes — CustomerView, InvoiceView — that extend the base domain class with synthesized display columns resolved from foreign key chains. View classes are used as API response types for list and detail endpoints.
The Persistence Layer
The persistence layer (server/persist/) is an ADO.NET data access layer with a database provider abstraction. The abstraction means the same generated application can run against PostgreSQL or SQL Server by changing a configuration value.
For each entity, the persistence layer provides:
- Get — retrieves a single record by ID
- GetView — retrieves a view record (with resolved display values) by ID
- List — retrieves all active records as a list of view objects
- Insert — inserts a new record and returns the generated ID
- Update — updates an existing record using optimistic concurrency (the
versioncolumn is checked and incremented) - Delete — soft-deletes a record by setting
is_active=0 - History — retrieves the audit history from the history table
- Children — retrieves child records for parent-child relationships
On every insert and update, the persistence layer automatically sets created_by, last_updated, last_updated_by, and version. The is_active flag is set to 1 on insert and 0 on delete. None of this requires any hand-written code.
The Logic Layer and AOP Proxy
The logic layer (server/logic/) sits between the API and persistence. Rather than generating trivial pass-through methods, Jumpstart uses a proxy-based AOP (Aspect-Oriented Programming) pattern to inject cross-cutting concerns at this layer without cluttering the generated code.
Each entity has a logic class with a generated partial and a user-editable partial:
CustomerLogic.generated.cs — generated methods that delegate to the persistence layer.
CustomerLogic.user.cs — where you override or extend the generated methods with your business rules.
The proxy wraps the logic class and applies aspects transparently:
Logging — every method call, its arguments, and its result are logged automatically.
Authorization — method calls can be checked against the RBAC system. Operations defined in the sec.operation table are enforced here.
Event publishing — after successful mutations (insert, update, delete), the proxy publishes events to the event system. Event handlers defined in core.event_service can trigger script execution in response.
This AOP approach means that adding logging to your entire application is not a matter of adding logging calls to hundreds of methods — it’s a matter of configuring the proxy once.
The API Layer
The API layer (server/api/) is an ASP.NET Core 9 application. Every entity produces a generated partial class controller. The full set of generated endpoints for any entity:
| Method | Route | Description |
|---|---|---|
| GET | /api/{entity} | List all active records (view objects) |
| GET | /api/{entity}/{id} | Get one record by ID |
| GET | /api/{entity}/view/{id} | Get view by ID (with resolved FK display values) |
| GET | /api/{entity}/enum | Get [{id, name}] list for dropdown population |
| POST | /api/{entity} | Create a new record |
| PUT | /api/{entity}/{id} | Update an existing record |
| DELETE | /api/{entity}/{id} | Delete a record |
| GET | /api/{entity}/history/{id} | Get the full audit history for a record |
| GET | /api/{entity}/children/{id}?child={name} | Get child records by relationship name |
Because controllers are partial class, you add custom endpoints in a user file without touching the generated file:
// CustomerController.user.cs
public partial class CustomerController
{
[HttpGet("search")]
public IEnumerable<CustomerView> Search([FromQuery] string q)
{
// custom search logic
}
}
Real-Time Notifications via SignalR
Every POST, PUT, and DELETE operation publishes a real-time notification through SignalR’s NotificationHub. Blazor clients subscribe to these notifications on the list pages and automatically refresh when data changes. If two users have the same list page open, both see updates in real time without polling.
The Blazor WebAssembly Frontend
The web layer (web/blazor/) is a Blazor WebAssembly application. For each entity, the generator produces:
ListCustomer.razor — a list page with a data table component. Columns correspond to the entity’s RWK columns and any synthesized display values from view relationships. The data table supports sorting and includes a context menu. The page subscribes to SignalR notifications for live updates.
EditCustomer.razor — an edit form with an input control for every column. Control types are derived from the column’s data type and FK type: text inputs for VARCHAR, number inputs for BIGINT/NUMERIC, date pickers for DATE/TIMESTAMP, radio buttons for BOOLEAN, and dropdown selects for enum FK columns. Parent-child relationships render as tabs below the main form, each loading its child records from the children endpoint.
The navigation menu is generated from the NAV_MENU values in your metadata CSV. Entities are grouped under their specified menu sections automatically.
The Scheduling Server
The scheduling server is a .NET Worker Service that runs separately from the main API. It’s responsible for distributed workflow orchestration.
At startup, the scheduler registers itself as a server_node in the database. It then polls the schedule and workflow tables to determine what work needs to be dispatched and when. When a workflow is triggered — either on a schedule or by an API call — the scheduler coordinates execution across available agent nodes.
The scheduling server exposes its own minimal API for triggering workflows on demand:
GET /api/workflow/run/{id}
Execution state flows through core.exec_log, which records start time, end time, status, and output for every execution. The main application’s UI surfaces this history on the workflow edit page.
The Agent Server
The agent server is another .NET Worker Service. It connects to the database, registers as a server_node of type agent, and polls for assigned work from the scheduler.
When work is assigned, the agent executes the specified core.script record. Three script types are supported:
C# — scripts are compiled and executed in-process using Roslyn.
PowerShell — scripts are executed via a hosted PowerShell runspace.
Python — scripts are executed as a subprocess with output captured.
Script output is streamed back in real time through SignalR to any connected UI clients that are watching the execution — the same NotificationHub used by the main application. This means you can watch a long-running Python script’s console output in your browser as it runs on a remote agent node.
The agent architecture supports multiple registered nodes. The scheduler distributes work across available agents, enabling horizontal scaling of script execution without changes to the application code.
The Whole Picture
What Jumpstart generates is not a monolith masquerading as a proper architecture. It’s a genuinely layered system: a clean domain model, a database-abstracted persistence layer, a logic layer with AOP-injected cross-cutting concerns, a RESTful API with real-time notification support, a modern WebAssembly frontend, and two independent worker services for scheduling and agent-based script execution.
Every layer follows consistent patterns. Every entity participates in every layer. And because the generator is template-driven, these patterns can be evolved and regenerated as the project matures — the architecture grows with your model rather than diverging from it.