Defining Models
Models define the structure of your entities. Define them once in Rust, and they work everywhere—native servers, browser clients, and mobile apps.
Basic Model Definition
Use the #[derive(Model)] macro to define a model:
#[derive(Model, Debug, Serialize, Deserialize)]
pub struct Album {
#[active_type(YrsString)]
pub name: String,
pub artist: String,
pub year: i32,
}
This single definition generates:
| Generated Type | Purpose |
|---|---|
Album | The model struct for creating new entities |
AlbumView | Read-only view of an entity’s current state |
AlbumMut | Mutable handle for updating entities in a transaction |
Field Types
Basic Types
#[derive(Model, Debug, Serialize, Deserialize)]
pub struct Task {
pub title: String,
pub completed: bool,
pub priority: i32,
}
Current built-in projected types include:
Stringbool- Integers:
i16,i32,i64 - Floating point:
f64 Option<String>,Option<i32>,Option<i64>,Option<f64>Vec<u8>,Json,EntityId, and typedRef<T>references
Plain String fields infer the Yrs text backend. Other built-ins infer LWW;
use #[active_type(LWW)] when you explicitly want whole-value LWW semantics
for a String.
CRDT Types
Use #[active_type(...)] to choose an active value backend explicitly. The shipped CRDT-backed type is YrsString for collaborative text:
#[derive(Model, Debug, Serialize, Deserialize)]
pub struct Document {
#[active_type(YrsString)]
pub content: String,
pub title: String,
}
Entity References
Use Ref<T> to create typed references between entities:
#[derive(Model, Debug, Serialize, Deserialize, Clone)]
pub struct Artist {
pub name: String,
}
#[derive(Model, Debug, Serialize, Deserialize, Clone)]
pub struct Song {
pub title: String,
pub artist: Ref<Artist>,
}
References enable graph-style navigation between related entities.
JSON Fields
Use Json for schemaless, dynamic data:
#[derive(Model, Debug, Serialize, Deserialize, Clone)]
pub struct Track {
pub name: String,
pub metadata: Json,
}
JSON fields support nested path queries like metadata.genre = 'rock'.
Creating Entities
Use a transaction to create new entities:
let trx = ctx.begin();
let album = trx.create(&Album {
name: "Parade".into(),
artist: "Prince".into(),
year: 1986,
}).await?;
let album_id = album.id();
trx.commit().await?;
Reading Entities
Access data through the View type:
let view: AlbumView = ctx.get(album_id).await?;
println!("Album: {} by {} ({})", view.name()?, view.artist()?, view.year()?);
Updating Entities
Views remain read-only. To update an entity, edit the View inside a transaction, call the active field type’s mutation method, and commit:
let trx = ctx.begin();
let album = view.edit(&trx)?;
album.name().replace("Parade - Music from the Motion Picture")?;
album.year().set(&1987)?;
trx.commit().await?;
Here name() is a YrsString, so it offers text operations such as
insert, delete, overwrite, and replace. year() is an LWW<i32>,
so it uses set. Mutation handles stop accepting writes when their
transaction closes.
Generated TypeScript
When you build your WASM bindings, TypeScript types are generated automatically:
Creation and mutation use the generated model and View APIs:
export async function createAlbum(
name: string,
artist: string,
year: number,
): Promise<AlbumView> {
const transaction = ctx().begin();
const album = await Album.create(transaction, { name, artist, year });
await transaction.commit();
return album;
}
export async function renameAlbum(
album: AlbumView,
name: string,
): Promise<void> {
const transaction = ctx().begin();
album.edit(transaction).name.replace(name);
await transaction.commit();
}
The generated surface includes the model’s creation/query namespace
(Album), read-only AlbumView, transaction-bound AlbumMut, typed
AlbumLiveQuery, result/change-set wrappers, and typed reference wrappers.
View fields and live-query results are JavaScript getters (album.name,
albums.items); mutations go through the active field wrapper returned by
album.edit(transaction).
Next Steps
- Querying Data - How to query and filter entities
- Query Syntax - Full AnkQL syntax reference