Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 TypePurpose
AlbumThe model struct for creating new entities
AlbumViewRead-only view of an entity’s current state
AlbumMutMutable 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:

  • String
  • bool
  • Integers: i16, i32, i64
  • Floating point: f64
  • Option<String>, Option<i32>, Option<i64>, Option<f64>
  • Vec<u8>, Json, EntityId, and typed Ref<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