SeaORM Entities
Table models with typed columns, relation enums, and junction tables for many-to-many relationships. Plus from_model() and to_active_model() conversions.
You’ve defined your data model. Now you need a database entity. And a conversion layer. And CRUD methods. And an API module. And HTTP handlers. And IPC commands. And a TypeScript client. And an admin UI registry.
For every single entity.
That’s hundreds of lines of code that look almost identical across your codebase - the kind of boilerplate where the 95th copy-paste is where bugs hide.
Ontogen is a build-time code generator that runs in your build.rs. You write one annotated struct per entity, and a layered pipeline of independent generators produces everything downstream.
Schema/*.rs ──► parse_schema() ──► SchemaOutput │ ┌────────────┼────────────┐ ▼ ▼ ▼ gen_seaorm gen_markdown_io gen_dtos independent │ │ generators └──────┬─────┘ ▼ StoreConfig.backend gen_store() ──► StoreOutput │ ▼ gen_api() ──► ApiOutput │ ┌─────────┴─────────┐ ▼ ▼ gen_servers() gen_clients() ServersOutput TS bindings, HTTP / IPC Axum / IPC / MCP clients, admin registryEach generator is standalone. You can run the full pipeline, drive it through the Pipeline builder, or pick individual stages. Upstream outputs enrich downstream generators but are never required.
#[derive(OntologyEntity)]#[ontology(entity, table = "tasks", directory = "tasks", prefix = "task")]pub struct Task { #[ontology(id)] pub id: String,
pub name: String, pub description: Option<String>,
#[ontology(enum_field)] pub status: Option<TaskStatus>,
#[ontology(relation(belongs_to, target = "Agent"))] pub assignee_id: Option<String>,
#[ontology(relation(many_to_many, target = "Requirement"))] pub fulfills: Vec<String>,}From that single struct definition, one cargo build generates:
SeaORM Entities
Table models with typed columns, relation enums, and junction tables for many-to-many relationships. Plus from_model() and to_active_model() conversions.
CRUD Store
list(), get(), create(), update(), delete() methods with relation population, junction sync, and lifecycle hook call sites. Update structs with apply() methods.
API Layer
Forwarding functions that bridge your store to your transport layer. Generated modules merge seamlessly with hand-written custom endpoints.
Server Transports
Axum HTTP route handlers, Tauri IPC commands, and MCP tool definitions - all generated from the same API surface with consistent naming.
TypeScript Clients
Typed client functions for HTTP and Tauri IPC, split transport layers that pick the right backend at runtime, and admin UI registry with per-field metadata.
A Markdown Store
Or skip the database entirely. One config field routes the same generated CRUD at a vault of .md files — editable in any editor, diffable in git, navigable in Obsidian, with hand edits preserved across writes.
Independent generators, not a monolith. Each generator is a standalone function. gen_seaorm doesn’t know about gen_api. You can use just the persistence layer and skip everything else, or run the full pipeline. This isn’t a framework that owns your architecture - it’s a toolbox.
Generated and custom code coexist. Generated code lands in generated/ subdirectories. Your hand-written modules live alongside them. The API layer scans both and merges them into a unified intermediate representation. Downstream generators see no difference between generated and custom endpoints.
Typed IRs between stages. Each generator returns a plain Rust struct - SchemaOutput, SeaOrmOutput, MarkdownIoOutput, StoreOutput, ApiOutput, ServersOutput. No magic, no framework types. Just data that the next stage can use to make smarter decisions.
The store backend is one field. StoreConfig::backend picks SeaORM or a markdown vault at generation time, and everything above the store - API forwarding, HTTP/IPC/MCP transports, TypeScript clients - emits byte-identical output either way. That invariant is CI-enforced, not aspirational.
Lifecycle hooks you own. Hook files are scaffolded once per entity and never overwritten. You fill in before_create, after_update, before_delete - the generated store calls them at the right time. Your business logic stays in files you control.