Skip to content

The architect pattern

One source struct, every surface — wire, repo trait, storage.

3 min read

The canonical shape for any new entity: a plain Rust struct decorated with #[derive(architect::Entity)]. The derive emits everything else.

What you write per entity#

// features/<feature>/src/lib.rs  (the contract: default features, wasm-clean)

use chrono::{DateTime, Utc};
use uuid::Uuid;

#[derive(architect::Entity, ::facet::Facet, Clone, Debug, PartialEq)]
#[architect(table_name = "examples", repo)]
pub struct Example {
    #[architect(primary_key, auto_increment = false, on_create = Uuid::new_v4())]
    pub id: Uuid,

    #[architect(filterable, sortable, fulltext)]
    pub name: String,

    #[architect(filterable, fulltext)]
    pub description: String,

    #[architect(exclude(create, update), on_create = Utc::now())]
    pub created_at: DateTime<Utc>,

    #[architect(exclude(create, update), on_create = Utc::now(), on_update = Utc::now())]
    pub updated_at: DateTime<Utc>,
}

Nothing else. No parallel DTO struct, no manual From<Model> for Example, no cfg_attr gymnastics on the fields.

What the derive emits#

Always (wasm-clean):

  • Example — the wire struct (left as you wrote it; you derive facet::Facet yourself so it travels on vox transports).
  • ExampleCreate, ExampleUpdate, ExampleList — payload structs with the right field exclusions.
  • ExampleRepo — the #[vox::service] trait with get / list / create / update / delete. Vox generates ExampleRepoClient for consumers and ExampleRepoDispatcher for the server.
  • ExampleRepoLayer — a Layer service token (+ a <snake>_repo_layer helper) so the repo composes through architect’s layer system: layers![ExampleRepoLayer], .provide(backend) / backend.into_router(). See idioms §6.

Under --features server-seaorm:

  • The SeaORM Model + Entity + Column + Relation + ActiveModel with the right #[sea_orm(...)] attributes synthesized from your architect attributes.
  • impl From<Model> for Example — the bridge between storage and wire.
  • ExampleRepoStorage<C: ConnectionTrait + ...> — concrete ExampleRepo implementation against SeaORM. Sort handling is wired against Column variants for every #[architect(sortable)] field.

With the store flag, under the consumer’s atom + vox features (the Dioxus client side):

  • ExampleStore (+ provide_example_store / use_example_store) — the shared optimistic cache, and ExampleClientError — the typed error.
  • use_example(id) / use_example_list() — data hooks returning an AtomResult phase for pages to match.
  • ExampleMutations (use_example_mutations) — optimistic create/update/delete with rollback, failure notification, and reactivity-key invalidation. See composing the UI.

How a client consumes it#

Identical from wasm and desktop — only the link construction differs:

let link = WsLink::connect("ws://localhost:4040/vox").await?;
let client = initiator_on(link, TransportMode::Bare)
    .establish::<ExampleRepoClient>()
    .await?;

let created = client.create(ExampleCreate {
    name: "alpha".into(),
    description: "first row".into(),
}).await?;

let listed = client.list(Page { index: 0, size: 50 }, None, None).await?;

This is the raw form. The example apps don’t hard-code a URL — they program against ExampleRepoClient and inject a Transport (remote WebSocket or in-process) at the app root, so the same screens run against a server or an embedded backend. See idioms §7 — inject the transport and the end-to-end walkthrough.

Field attributes#

AttributeEffect
primary_keyMarks the SeaORM primary key. Required.
auto_increment = falseDisables SeaORM’s auto-increment (use for UUIDs).
on_create = <expr>Server-side default for inserts. Field drops out of <T>Create.
on_update = <expr>Server-side default for updates.
exclude(create) / exclude(update)Drop the field from the corresponding payload struct.
filterableReserved for the structured filter AST.
sortableAdds a match arm in <T>RepoStorage::list mapping the wire-side field name to Column::<PascalCase>.
fulltextReserved for FTS5 emission.

Container attributes#

AttributeEffect
table_name = "..."SeaORM table name. Defaults to snake_case of the struct name.
repoEmit the <T>Repo #[vox::service] trait + the server-side <T>RepoStorage<C>.
storeAlso emit the Dioxus client state layer (store, typed hooks, optimistic mutations). Requires repo; gated on the consumer’s atom + vox features; pk must be Clone + Eq + Hash + FromStr (Uuid, String, integer ids — Copy not required).
formAlso emit typed form bindings (<T>CreateFields / <T>UpdateFields, submit() → the wire payload). Gated on the consumer’s form feature. Field attrs: form(optional), form(label = "…").
eventsAlso emit the live-event story: <T>Event enum, <T>Events subscribe trait, the <T>Evented<R> publish-through wrapper (its Services bundle mounts CRUD + the event feed), and — with store — the use_<t>_events() client hook that makes every store-rendered page live. Requires repo. Full walkthrough: server push.

The attribute form#

#[architect::entity(table_name = "…", repo)] writes the derive line for you — Entity, Facet, Clone, Debug, PartialEq, the fake hook — so an entity is its fields and its options and nothing else. #[architect::wire] does the same for plain wire types (and adds #[repr(u8)] to enums). Add Eq / Hash / Default / thiserror::Error as extra #[derive(…)] lines when you want them.

What else comes out#

  • The HTTP face: <entity>_http::router, <Entity>RepoHttpClient, and with events the SSE feed — on the same scheme every #[architect::service] uses; #[architect(path = "…")] sets the prefix. See the HTTP face.
  • Clients that implement the repo trait: <Entity>RepoClient (vox) and <Entity>RepoHttpClient both impl <Entity>Repo, so code written against the trait runs over any wire.
  • The migration (under server): <Entity>Migration creates the table from the derived model plus an index per filterable / sortable column. Collect them with architect::migrator!(Migrator: [NoteMigration, …]) and run architect::storage::migrate::<Migrator>(&db).

Linked from

architect·Published with architect