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 one Transport interface with two implementations: createIpcTransport() for the desktop app and createHttpTransport() for a browser. Your frontend code calls the same methods either way. IPC passes flat arguments and results; the HTTP transport speaks the server’s JSON:API wire and flattens it back into the same entity types.
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:[package]name = "my-app"[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"] }schemars = "0.8"thiserror = "2"spectaandschemarsare dependencies of the generated DTOs (Create{Entity}Input/Update{Entity}Input) which derivespecta::TypeandJsonSchema. The TypeScript bindings do not go through specta: ontogen-ts emits them at build time — see the TypeScript bindings guide. -
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::clients::ClientGenerator;use ontogen::servers::{NamingConfig, ServerGenerator};use ontogen::{ClientsConfig, 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(),},],sse_route_overrides: Default::default(),route_prefix: None,store_type: Some("Store".into()),store_import: Some("crate::store::Store".into()),pagination: None,extra_surfaces: vec![],error_source_dir: None, // Pipeline fills in its schema directory};let clients_config = ClientsConfig {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(),},],store_type: Some("Store".into()),store_import: Some("crate::store::Store".into()),// Every other field at its inert default: no formatting, no// route prefix, no pagination...ClientsConfig::new("src/api/v1", "AppState", "crate::api::v1", "crate::schema", "crate::AppState")};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())).store_id_strategy(ontogen::IdStrategy::Provided).api("src/api/v1/generated", "AppState").servers(servers_config).clients(clients_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 client config’s
route_prefix,paginationandnamingmust match the server config’s, so the HTTP transport calls the routes the server serves. -
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”The generated IPC module ends with
ipc_handler(), which wrapstauri::generate_handler!over every generated command. Insrc/main.rs, pass it to Tauri:use std::sync::Arc;fn main() {tauri::Builder::default().setup(|app| {// Initialize your databaselet db = /* create DatabaseConnection */;let state = Arc::new(my_app::AppState::new(db));app.manage(state);Ok(())}).invoke_handler(my_app::api::transport::ipc::generated::ipc_handler()).run(tauri::generate_context!()).expect("error while running tauri application");}Each entity gets five IPC commands named
{entity}_{op}:exercise_list,exercise_get_by_id,exercise_create,exercise_update,exercise_delete. A custom endpoint is{module}_{fn}with the module singularized (stats::get_workoutisstat_get_workout), and an event op adds a{event}_subscribe/{event}_unsubscribepair. -
Use the TypeScript client
Section titled “Use the TypeScript client”The generated
transport.tsexports theTransportinterface and its two implementations. Pick one when the app starts:import { createHttpTransport, createIpcTransport } from '~/generated/transport';const transport = '__TAURI_INTERNALS__' in window ? createIpcTransport() : createHttpTransport();const exercises = await transport.exerciseList({ sort: ['name'] });const newExercise = await transport.exerciseCreate({id: 'ex-bench-press',name: 'Bench Press',muscle_group: 'chest',equipment: 'barbell',notes: null,});createIpcTransport()callsinvoke('exercise_create', { input })and gets theExerciseback as is.createHttpTransport()sendsPOST /api/exercisesas a JSON:API document, reads the resource object it answers, and returns the same flatExercise; a failure throws aJsonApiErrorcarrying the HTTPstatusand theerrorsarray. See Client Generation.
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]pub async fn exercise_list( sort: Option<Vec<String>>, ontogen_state: ::tauri::State<'_, ::std::sync::Arc<AppState>>,) -> Result<Vec<Exercise>, String> { let ontogen_order = ::ontogen_core::order::parse_sort(sort.unwrap_or_default()).map_err(|ontogen_e| ontogen_e.to_string())?; let ontogen_store = ontogen_state.store().await.map_err(|ontogen_e| ontogen_e.to_string())?; exercise::list(&ontogen_store, &ontogen_order).await.map_err(|ontogen_e| ontogen_e.to_string())}
#[::tauri::command]pub async fn exercise_create( input: CreateExerciseInput, ontogen_state: ::tauri::State<'_, ::std::sync::Arc<AppState>>,) -> Result<Exercise, String> { let ontogen_store = ontogen_state.store().await.map_err(|ontogen_e| ontogen_e.to_string())?; exercise::create(&ontogen_store, input).await.map_err(|ontogen_e| ontogen_e.to_string())}Key things to notice:
::tauri::State<'_, ::std::sync::Arc<AppState>>is Tauri’s state extractor. TheAppStateis the same one you registered withapp.manage(). Runtime items are written by their full paths so that none collides with an entity of the same name.- Arguments and results are flat: the
Exerciseitself, not a JSON:API document.sorttakes the same keys as the HTTPsortparameter (name,-name). - 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. The wires differ — the Axum server speaks JSON:API, IPC stays flat — but the Transport methods hide that, so calling code is the same.