Tauri Integration
Ontogen pairs naturally with Tauri. You define entities in the Tauri crate’s src/schema/, run the generator pipeline in build.rs, and get Tauri IPC command handlers alongside your Axum HTTP handlers — both generated from the same API surface.
The TypeScript client generator produces a split transport layer that uses Tauri IPC when running as a desktop app and HTTP when running in a browser. Your frontend code calls the same functions either way.
This recipe walks through setting up a Tauri + Ontogen project from scratch.
Project structure
Section titled “Project structure”A typical Tauri + Ontogen project has two codebases side by side:
my-app/ src-tauri/ # Tauri Rust crate build.rs # Ontogen pipeline + tauri_build Cargo.toml src/ schema/ # Your entity definitions persistence/db/ # Generated SeaORM entities + conversions store/ # Generated CRUD + your hooks api/v1/ # Generated API + your custom endpoints api/transport/http/ # Generated Axum handlers api/transport/ipc/ # Generated Tauri IPC handlers lib.rs main.rs src-nuxt/ # (or src-vue, src-react, etc.) app/generated/ transport.ts # Generated TypeScript client types.ts # Type bindingsRecipe: Set up Tauri + Ontogen
Section titled “Recipe: Set up Tauri + Ontogen”-
Add dependencies
Section titled “Add dependencies”In
src-tauri/Cargo.toml:[build-dependencies]ontogen = "0.1"tauri-build = { version = "2", features = [] }[dependencies]ontogen-macros = "0.1"tauri = { version = "2", features = [] }sea-orm = { version = "1", features = ["sqlx-sqlite", "runtime-tokio-rustls"] }serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }axum = "0.8"specta = { version = "=2.0.0-rc.23", features = ["derive", "function"] }schemars = "0.8"thiserror = "2"spectaandschemarsare needed because the generated IPC commands use#[specta::specta]for TypeScript type generation andJsonSchemaderives in DTOs. -
Write the build script
Section titled “Write the build script”Create
src-tauri/build.rsusing thePipelinebuilder. This mirrors the canonicalexamples/iron-log/src-tauri/build.rs:use std::path::PathBuf;use ontogen::servers::{ClientGenerator, NamingConfig, ServerGenerator};use ontogen::{Pipeline, ServersConfig};fn main() {println!("cargo:rerun-if-changed=build.rs");ontogen::emit_rerun_directives_excluding(&PathBuf::from("src/api/v1"),&["generated"],);let servers_config = ServersConfig {api_dir: "src/api/v1".into(),state_type: "AppState".into(),service_import_path: "crate::api::v1".into(),types_import_path: "crate::schema".into(),state_import: "crate::AppState".into(),naming: NamingConfig::default(),generators: vec![ServerGenerator::HttpAxum {output: "src/api/transport/http/generated.rs".into(),},ServerGenerator::TauriIpc {output: "src/api/transport/ipc/generated.rs".into(),},],client_generators: vec![ClientGenerator::HttpTauriIpcSplit {output: "../src-nuxt/app/generated/transport.ts".into(),bindings_path: "../src-nuxt/app/generated/types.ts".into(),},ClientGenerator::AdminRegistry {output: "../src-nuxt/app/admin/generated/admin-registry.ts".into(),},],rustfmt_edition: "2024".into(),sse_route_overrides: Default::default(),ts_skip_commands: vec![],route_prefix: None,store_type: Some("Store".into()),store_import: Some("crate::store::Store".into()),pagination: None,// Pipeline auto-fills this from parsed schema; leave empty.schema_entities: Vec::new(),};Pipeline::new("src/schema").seaorm("src/persistence/db/entities/generated","src/persistence/db/conversions/generated",).dtos("src/schema/dto").store("src/store/generated", Some::<PathBuf>("src/store/hooks".into())).api("src/api/v1/generated", "AppState").servers(servers_config).build().unwrap_or_else(|e| {e.emit_cargo_warning();panic!("ontogen pipeline failed: {e}");});tauri_build::build();}Notice
tauri_build::build()at the end. Ontogen runs first, generates all the code, and then Tauri’s build step runs. Order matters.The
ontogen::servers::*re-exports (ClientGenerator,NamingConfig,ServerGenerator) replace the olderontogen::servers::config::*andontogen::servers::types::*paths, which are nowpub(crate). -
Set up
Section titled “Set up AppState”AppStateThe generated IPC handlers expect an
AppStatetype with astore()method. Insrc/lib.rs:pub mod api;pub mod persistence;pub mod schema;pub mod store;use std::sync::Arc;use sea_orm::DatabaseConnection;use crate::schema::AppError;use crate::store::Store;pub struct AppState {store: Store,}impl AppState {pub fn new(db: Arc<DatabaseConnection>) -> Self {Self { store: Store::new(db) }}pub async fn store(&self) -> Result<&Store, AppError> {Ok(&self.store)}}The generated IPC handlers call
state.store().await?to get the store reference. Theasynconstore()lets you do async initialization or project scoping in more complex setups. -
Register IPC commands
Section titled “Register IPC commands”In
src/main.rs, register the generated commands with Tauri:use std::sync::Arc;fn main() {tauri::Builder::default().setup(|app| {// Initialize your databaselet db = /* create DatabaseConnection */;let state = Arc::new(iron_log::AppState::new(db));app.manage(state);Ok(())}).invoke_handler(tauri::generate_handler![// Import from your generated IPC moduleiron_log::api::transport::ipc::generated::get_exercises,iron_log::api::transport::ipc::generated::get_exercise_by_id,iron_log::api::transport::ipc::generated::create_exercise,iron_log::api::transport::ipc::generated::update_exercise,iron_log::api::transport::ipc::generated::delete_exercise,// ... repeat for each entity]).run(tauri::generate_context!()).expect("error while running tauri application");}Each entity gets five IPC commands (list, get, create, update, delete). The generated command names follow the pattern
get_{entities},get_{entity}_by_id,create_{entity},update_{entity},delete_{entity}. -
Use the TypeScript client
Section titled “Use the TypeScript client”The generated
transport.tsgives you typed functions that work in both web and desktop contexts:import { getExercises, createExercise } from '~/generated/transport';// Works via Tauri IPC in desktop, HTTP in browserconst exercises = await getExercises();const newExercise = await createExercise({id: 'ex-bench-press',name: 'Bench Press',muscle_group: 'chest',equipment: 'barbell',});The split transport detects at runtime whether Tauri’s IPC is available. If it is, calls go through
invoke(). If not, they fall back to HTTP fetch.
What the generated IPC code looks like
Section titled “What the generated IPC code looks like”Each entity’s CRUD operations become Tauri commands. Here’s what the generator produces for an Exercise entity:
#[tauri::command]#[specta::specta]pub async fn get_exercises( state: State<'_, Arc<AppState>>,) -> Result<Vec<Exercise>, String> { let store = state.store().await.map_err(|e| e.to_string())?; exercise::list(&store).await.map_err(|e| e.to_string())}
#[tauri::command]#[specta::specta]pub async fn create_exercise( input: CreateExerciseInput, state: State<'_, Arc<AppState>>,) -> Result<Exercise, String> { let store = state.store().await.map_err(|e| e.to_string())?; exercise::create(&store, input).await.map_err(|e| e.to_string())}Key things to notice:
State<'_, Arc<AppState>>is Tauri’s state extractor. TheAppStateis the same one you registered withapp.manage().#[specta::specta]enables TypeScript type generation from the Rust signatures.- Errors are mapped to
Stringbecause Tauri IPC requires string-serializable errors. - The handler delegates to the API layer (
exercise::list,exercise::create), which delegates to the store.
Both transports, same API
Section titled “Both transports, same API”The power of Ontogen’s pipeline is that both transport layers (HTTP and IPC) are generated from the same ApiOutput. Adding a new entity or custom endpoint automatically appears in both.