Skip to content

Ontogen

Define your entities once as annotated Rust structs. Ontogen generates your persistence layer, CRUD store, API endpoints, server transports, and typed client libraries — all at build time, all from a single source of truth.

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
▼ SeaOrmOutput
gen_store() ──► StoreOutput
gen_api() ──► ApiOutput
gen_servers() ──► ServersOutput
(also emits TypeScript clients
and admin registry)

Each 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.

Markdown I/O

YAML-frontmatter parsers, Markdown writers, and filesystem operations for content-as-code workflows. Read and write entities as Markdown files.

6Pipeline stages
39Files from 4 entities
~95%Less hand-written boilerplate
3Server transports

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, StoreOutput, ApiOutput, ServersOutput. No magic, no framework types. Just data that the next stage can use to make smarter decisions.

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.