The architect pattern
One source struct, every surface — wire, repo trait, storage.
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 derivefacet::Facetyourself so it travels on vox transports).ExampleCreate,ExampleUpdate,ExampleList— payload structs with the right field exclusions.ExampleRepo— the#[vox::service]trait withget/list/create/update/delete. Vox generatesExampleRepoClientfor consumers andExampleRepoDispatcherfor the server.ExampleRepoLayer— aLayerservice token (+ a<snake>_repo_layerhelper) 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+ActiveModelwith the right#[sea_orm(...)]attributes synthesized from your architect attributes. impl From<Model> for Example— the bridge between storage and wire.ExampleRepoStorage<C: ConnectionTrait + ...>— concreteExampleRepoimplementation against SeaORM. Sort handling is wired againstColumnvariants 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, andExampleClientError— the typed error.use_example(id)/use_example_list()— data hooks returning anAtomResultphase for pages tomatch.ExampleMutations(use_example_mutations) — optimisticcreate/update/deletewith 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#
| Attribute | Effect |
|---|---|
primary_key | Marks the SeaORM primary key. Required. |
auto_increment = false | Disables 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. |
filterable | Reserved for the structured filter AST. |
sortable | Adds a match arm in <T>RepoStorage::list mapping the wire-side field name to Column::<PascalCase>. |
fulltext | Reserved for FTS5 emission. |
Container attributes#
| Attribute | Effect |
|---|---|
table_name = "..." | SeaORM table name. Defaults to snake_case of the struct name. |
repo | Emit the <T>Repo #[vox::service] trait + the server-side <T>RepoStorage<C>. |
store | Also 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). |
form | Also 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 = "…"). |
events | Also 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 witheventsthe 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>RepoHttpClientbothimpl <Entity>Repo, so code written against the trait runs over any wire. - The migration (under
server):<Entity>Migrationcreates the table from the derived model plus an index perfilterable/sortablecolumn. Collect them witharchitect::migrator!(Migrator: [NoteMigration, …])and runarchitect::storage::migrate::<Migrator>(&db).
Linked from
- Build a feature, end to endDefine an entity and a service, write a backend, serve it, consume it — the whole architect flow.
- The contractEntities, errors and services: what you write, what each attribute generates, and how to choose wire types.
- Services and entitiesThe derives: one Rust declaration gives the wire types, the vox service, the HTTP face, the clients and the actions.
- ArchitectureThe patterns the architect template is showcasing.
- Server push: streams end to endPubSub fan-out, Entity events, #[subscribe] siblings, the client hooks — and the real-time-safety rules.
- Composing the UIHow architect's Dioxus client is structured: feature crates own state + presentation, the shell owns transport + routing + composition, and pages compose by matching an AtomResult phase.
- ReferenceCrate map, derive attributes, and macro outputs.