Writing Data

Writing Data

Insert, update, and delete APIs with local-first execution, framework context hooks, and an intro to durability tiers.

Local-first writes

All writes execute against the local database first. insert, update, and delete return write handles immediately. Call .wait({ tier: ... }) on those handles when you need confirmation that the write reached a specific durability tier.

Jazz also exposes explicit batch builders:

  • beginTransaction(...) returns DbTransaction
  • beginDirectBatch(...) returns DbDirectBatch

For browser Blob, File, or ReadableStream uploads, use db.createFileFromBlob(...) or db.createFileFromStream(...) and store the returned file ID on your own row.

See Files & Blobs for more details.

Getting a Db handle

Every framework provides a hook to access the database handle. In plain TypeScript, use the Db returned by createDb directly.

import { useDb } from "jazz-tools/react";

const db = useDb();
<script setup lang="ts">
import { useDb } from "jazz-tools/vue";

const db = useDb();
</script>
<script lang="ts">
  import { getDb } from 'jazz-tools/svelte';

  const db = getDb();
</script>
import { createDb } from "jazz-tools";

const db = await createDb({
  appId: "my-app",
  env: "dev",
  userBranch: "main",
});

Insert, update, delete

Mutations are methods on the database handle. All three take a table as their first argument.

export async function writeTodoCrud(db: Db, todoId: string) {
  db.insert(app.todos, {
    title: "Write docs",
    done: false,
    owner_id: EXAMPLE_OWNER_ID,
    projectId: EXAMPLE_PROJECT_ID,
  });
  db.update(app.todos, todoId, { done: true });
  db.delete(app.todos, todoId);
}
pub async fn write_todo_crud(client: &JazzClient, existing_id: ObjectId) -> jazz_tools::Result<()> {
    let values = todo_values("Write docs", "");

    let _new_row = client.create("todos", values).await?;
    client
        .update(
            existing_id,
            vec![("done".to_string(), Value::Boolean(true))],
        )
        .await?;
    client.delete(existing_id).await?;
    Ok(())
}

Partial updates and nullable fields

update(...) only modifies the keys you pass. Omitted fields are left unchanged; explicitly passing undefined also leaves a field unchanged. To clear a nullable column in TypeScript, pass null. Required fields cannot be set to null.

export function clearNullableTodoFields(db: Db, todoId: string) {
  db.update(app.todos, todoId, { owner_id: null }); // clears the nullable FK
  db.update(app.todos, todoId, { description: undefined }); // leaves the field unchanged
}
pub async fn clear_nullable_fields(
    client: &JazzClient,
    todo_id: ObjectId,
) -> jazz_tools::Result<()> {
    // Set a nullable column to null
    client
        .update(todo_id, vec![("owner_id".to_string(), Value::Null)])
        .await?;

    // Only the specified columns are changed; omitted columns are left as-is.
    Ok(())
}

Write durability tiers

When using insert(...).wait({ tier }), update(...).wait({ tier }), or delete(...).wait({ tier }), the tier controls how far the mutation must propagate before the promise resolves: locally on the client (local), the nearest edge server (edge), or the global core (global).

TierResolves whenDefault for
localPersisted to local OPFSBrowser/client
edgeAcknowledged by nearest sync serverBackend/server
globalPropagated to global core
export async function writeTodoWithDurabilityTiers(db: Db) {
  const { id } = await db
    .insert(app.todos, {
      title: "Write docs with durability tier",
      done: false,
      owner_id: EXAMPLE_OWNER_ID,
      projectId: EXAMPLE_PROJECT_ID,
    })
    .wait({ tier: "edge" });

  await db.update(app.todos, id, { done: true }).wait({ tier: "global" });
  await db.delete(app.todos, id).wait({ tier: "global" });
}
pub async fn write_todo_with_default_durability(
    client: &JazzClient,
) -> jazz_tools::Result<ObjectId> {
    let (id, _row_values) = client
        .create(
            "todos",
            todo_values("Write docs with default durability behavior", ""),
        )
        .await?;

    // Rust currently does not expose per-write durability tier arguments.
    // Writes apply locally first, then sync asynchronously to higher tiers.
    Ok(id)
}

See Durability Tiers for the full reference, including read durability, data flow between tiers, and consistency semantics.

Need to clear local data during development? See How do I reset browser storage?

wait({ tier }) resolves when the batch reaches the requested durability tier. If the batch is rejected, it rejects with PersistedWriteRejectedError instead of hanging.

const pending = db.insert(app.todos, {
  title: "Ship review fixes",
  done: false,
});

console.log(pending.batchId);

try {
  const row = await pending.wait({ tier: "global" });
  console.log(row.id);
} catch (error) {
  if (error instanceof PersistedWriteRejectedError) {
    console.error(error.code, error.reason);
  }
}

You can also set a global onMutationError handler that will be notified anytime a write that was not explicitly awaited fails:

db.onMutationError((error) => {
  console.error("DB mutation failed: ", error);
});

Explicit batches

Use an explicit transaction when several writes should settle together after an authority validates the sealed batch:

const tx = db.beginTransaction(app.todos);

tx.insert(app.todos, { title: "Draft copy", done: false });
const stagedDrafts = await tx.all(app.todos.where({ done: false }));
tx.update(app.todos, todoId, { done: true });

const committed = tx.commit();
await committed.wait({ tier: "edge" });
console.log(committed.batchId);

The changes made as part of the transaction are scoped to it, and will only be globally visible once it's committed and accepted by the authority.

DbTransaction can also read its own staged state through all(...) and one(...) before commit.

Use a direct batch when you want to group several ordinary writes under one batch id, but still want them to be globally visible immediately:

const batch = db.beginDirectBatch(app.todos);

batch.insert(app.todos, { title: "Grouped direct write", done: false });
batch.update(app.todos, todoId, { done: true });

console.log(batch.batchId());

On this page