Back to catalog
LabfreeHands-On Labs

Lab: Build a Mesh-Native Todo App with UI and Storage Schemas

Build a complete mesh-native Todo application by defining a UI schema, a storage schema, and wiring them together with the DOSFI SDK — fully practical, end to end.

intermediate 90 minutesFree

What you'll build

Vertical stack context: DOSFIUniversity (research) → DOSFI Academy (this course) → MeshNativeExchange (publishing & monetizing). Students progress from Learning → Building → Publishing → Monetizing. In this lab, you will build a working Todo application on the DOSFI mesh. You will define a storage schema for persisting todo items, a UI schema for rendering the interface, and connect both using the DOSFI SDK. This lab is entirely hands-on: you will write schemas, send data, receive updates, and see your app render in real time.

Skills you'll practice

  • Define a storage schema for todo items on the mesh
  • Define a UI schema that renders a todo list interface
  • Wire the UI schema to the storage schema using the SDK
  • Implement create, toggle, and delete operations
  • Observe real-time UI updates when storage changes

Before you start

  • Completion of 'Lab: Your First DOSFI SDK Call' or equivalent SDK experience
  • Understanding of JSON schema basics
  • Familiarity with JavaScript async/await

Lab steps4 steps

  1. 1
    Defining the Storage Schema for Todo Items

    ## Module Overview Every mesh-native app starts with a storage schema — the public declaration of what data your app will persist on the mesh. In this lesson, you will define the storage schema for a Todo application, including fields for task title, completion status, and timestamps. ## Learning Objectives - Understand the structure of a DOSFI storage schema - Define fields with correct types and constraints - Register the storage schema with the SDK - Verify the schema is accepted by the mesh ## Lesson A storage schema is a JSON document that declares the shape of your application data. The mesh uses this schema to accept, validate, and store your data. You define it; the mesh enforces it. ### The Todo Storage Schema ```json { "schema_name": "todo_items", "version": "1.0.0", "fields": { "id": { "type": "string", "required": true, "primary_key": true }, "title": { "type": "string", "required": true, "max_length": 200 }, "completed": { "type": "boolean", "required": true, "default": false }, "created_at": { "type": "string", "format": "date-time", "required": true }, "updated_at": { "type": "string", "format": "date-time", "required": true } } } ``` ### Registering the Schema ```javascript import { client } from './dosfi.config.js'; const result = await client.storage.registerSchema(todoSchema); console.log('Schema registered:', result.schema_name); console.log('Version:', result.version); ``` The `registerSchema()` method returns a confirmation with the schema name and version. Once registered, the mesh will accept data that conforms to this schema. ### Schema Rules - `schema_name` must be unique within your application namespace. - `version` allows schema evolution. You can register new versions without breaking existing data. - `primary_key` marks the field used to identify individual records. - `required` fields must be present in every record. - `default` values are applied when a field is omitted. ## Text-Based Diagram ``` [todo_items schema] | +-- id (string, primary_key) +-- title (string, max 200 chars) +-- completed (boolean, default: false) +-- created_at (date-time) +-- updated_at (date-time) | v [client.storage.registerSchema(schema)] | v [Mesh accepts schema] | v [Confirmation: { schema_name, version }] ``` ## Example A script that registers the todo storage schema: ```javascript import { client } from './dosfi.config.js'; async function registerTodoSchema() { const schema = { schema_name: 'todo_items', version: '1.0.0', fields: { id: { type: 'string', required: true, primary_key: true }, title: { type: 'string', required: true, max_length: 200 }, completed: { type: 'boolean', required: true, default: false }, created_at: { type: 'string', format: 'date-time', required: true }, updated_at: { type: 'string', format: 'date-time', required: true } } }; const result = await client.storage.registerSchema(schema); console.log('Registered:', result.schema_name, 'v' + result.version); } registerTodoSchema(); ``` ## Exercises 1. Register the `todo_items` storage schema. Confirm the response includes the schema name and version. 2. Try registering the same schema name with a different version number. Observe the response. 3. Try registering a schema with a missing `primary_key` field. Observe the validation error. ## Expected Output ``` Registered: todo_items v1.0.0 ``` ## Troubleshooting Notes 1. **Error: `schema_name already exists`** — You registered this schema name before. Use a new version number or a different schema name. 2. **Error: `primary_key field is required`** — Every schema must have exactly one field with `primary_key: true`. Add it and retry. 3. **Error: `invalid field type`** — Supported types are `string`, `number`, `boolean`, and `date-time` (as a string with `format`). Check your field definitions. ## Validation Steps 1. Confirm `registerSchema()` returns the schema name and version. 2. Confirm that re-registering with a new version number succeeds. 3. Confirm that a schema without a `primary_key` field is rejected.

  2. 2
    Defining the UI Schema for the Todo Interface

    ## Module Overview With the storage schema in place, you now define the UI schema — the public declaration of how your application interface is structured. The UI schema describes components, layout, and data bindings. In this lesson, you will define a UI schema for the Todo app that renders a list, an input field, and toggle/delete buttons. ## Learning Objectives - Understand the structure of a DOSFI UI schema - Define components: list, input, button, checkbox - Bind UI components to storage schema fields - Register the UI schema with the SDK ## Lesson A UI schema is a JSON document that declares the visual structure of your app. It uses a component-based model where each component has a type, properties, and optional data bindings to your storage schema. ### The Todo UI Schema ```json { "schema_name": "todo_ui", "version": "1.0.0", "layout": { "type": "column", "children": [ { "type": "heading", "props": { "text": "My Todos", "level": 1 } }, { "type": "row", "children": [ { "type": "input", "props": { "placeholder": "Add a todo...", "bind": "new_todo_title" } }, { "type": "button", "props": { "label": "Add", "action": "create_todo" } } ] }, { "type": "list", "bind": "todo_items", "item_template": { "type": "row", "children": [ { "type": "checkbox", "props": { "bind": "completed", "action": "toggle_todo" } }, { "type": "text", "props": { "bind": "title" } }, { "type": "button", "props": { "label": "Delete", "action": "delete_todo" } } ] } } ] } } ``` ### Key Concepts - **`type`** — The component type: `column`, `row`, `heading`, `input`, `button`, `list`, `text`, `checkbox`. - **`bind`** — Links a component to a storage schema field or a local state variable. - **`action`** — Names the handler function that runs when the user interacts with the component. - **`item_template`** — For `list` components, defines how each item is rendered. ### Registering the UI Schema ```javascript const result = await client.ui.registerSchema(todoUISchema); console.log('UI schema registered:', result.schema_name); ``` ## Text-Based Diagram ``` [todo_ui schema] | +-- column | +-- heading: "My Todos" | +-- row | +-- input (bind: new_todo_title) | +-- button (action: create_todo) | +-- list (bind: todo_items) +-- item_template: row +-- checkbox (bind: completed, action: toggle_todo) +-- text (bind: title) +-- button (action: delete_todo) ``` ## Example A script that registers the todo UI schema: ```javascript import { client } from './dosfi.config.js'; const todoUISchema = { schema_name: 'todo_ui', version: '1.0.0', layout: { type: 'column', children: [ { type: 'heading', props: { text: 'My Todos', level: 1 } }, { type: 'row', children: [ { type: 'input', props: { placeholder: 'Add a todo...', bind: 'new_todo_title' } }, { type: 'button', props: { label: 'Add', action: 'create_todo' } } ] }, { type: 'list', bind: 'todo_items', item_template: { type: 'row', children: [ { type: 'checkbox', props: { bind: 'completed', action: 'toggle_todo' } }, { type: 'text', props: { bind: 'title' } }, { type: 'button', props: { label: 'Delete', action: 'delete_todo' } } ] } } ] } }; async function registerTodoUI() { const result = await client.ui.registerSchema(todoUISchema); console.log('UI registered:', result.schema_name, 'v' + result.version); } registerTodoUI(); ``` ## Exercises 1. Register the `todo_ui` UI schema. Confirm the response includes the schema name. 2. Add a second heading below the list that says "Completed items are checked." Re-register with version `1.0.1`. 3. Try registering a UI schema with an invalid component type. Observe the error. ## Expected Output ``` UI registered: todo_ui v1.0.0 ``` ## Troubleshooting Notes 1. **Error: `unknown component type`** — Use only supported types: `column`, `row`, `heading`, `input`, `button`, `list`, `text`, `checkbox`. 2. **Error: `bind target not found`** — The `bind` value must match a field in your storage schema or a declared local state variable. 3. **Error: `action handler not registered`** — Actions named in the UI schema must have corresponding handler functions registered via the SDK (covered in Lesson 3). ## Validation Steps 1. Confirm `registerSchema()` returns the UI schema name and version. 2. Confirm that a new version number can be registered without error. 3. Confirm that an invalid component type is rejected with a clear error message.

  3. 3
    Wiring Actions: Create, Toggle, and Delete Todos

    ## Module Overview With both schemas registered, you now wire the UI actions to storage operations. In this lesson, you will implement handler functions for `create_todo`, `toggle_todo`, and `delete_todo`, and register them with the SDK so the mesh can route user interactions to your code. ## Learning Objectives - Register action handler functions with the SDK - Implement `create_todo` to insert a new record into storage - Implement `toggle_todo` to update the `completed` field - Implement `delete_todo` to remove a record from storage - Verify that each action produces the correct storage change ## Lesson Action handlers are JavaScript functions that the SDK calls when a user interacts with a bound UI component. Each handler receives an event object with the action name, the bound data, and the user input. ### Registering Action Handlers ```javascript import { client } from './dosfi.config.js'; async function createTodo(event) { const title = event.state.new_todo_title; if (!title || title.trim() === '') return; const todo = { id: crypto.randomUUID(), title: title.trim(), completed: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; await client.storage.insert('todo_items', todo); console.log('Created todo:', todo.id); } async function toggleTodo(event) { const updated = { ...event.record, completed: !event.record.completed, updated_at: new Date().toISOString() }; await client.storage.update('todo_items', event.record.id, updated); console.log('Toggled todo:', event.record.id); } async function deleteTodo(event) { await client.storage.delete('todo_items', event.record.id); console.log('Deleted todo:', event.record.id); } client.ui.registerActions({ create_todo: createTodo, toggle_todo: toggleTodo, delete_todo: deleteTodo }); console.log('Action handlers registered.'); ``` ### The Event Object Each handler receives an `event` object: ```json { "action": "create_todo", "state": { "new_todo_title": "Buy groceries" }, "record": null } ``` For list-item actions, the `record` field contains the current item: ```json { "action": "toggle_todo", "state": {}, "record": { "id": "abc-123", "title": "Buy groceries", "completed": false } } ``` ### Storage Operations The SDK provides three storage methods: - `client.storage.insert(schemaName, record)` — Adds a new record. - `client.storage.update(schemaName, id, updates)` — Updates an existing record. - `client.storage.delete(schemaName, id)` — Removes a record. ## Text-Based Diagram ``` [User clicks "Add" button] | v [SDK fires create_todo action] | v [createTodo(event)] | +-- reads event.state.new_todo_title +-- constructs todo record +-- client.storage.insert('todo_items', todo) | v [Storage updated] | v [UI re-renders list] ``` ## Example A complete script that registers all three action handlers: ```javascript import { client } from './dosfi.config.js'; async function createTodo(event) { const title = event.state.new_todo_title; if (!title?.trim()) return; const todo = { id: crypto.randomUUID(), title: title.trim(), completed: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; await client.storage.insert('todo_items', todo); console.log('Created:', todo.title); } async function toggleTodo(event) { const updated = { ...event.record, completed: !event.record.completed, updated_at: new Date().toISOString() }; await client.storage.update('todo_items', event.record.id, updated); console.log('Toggled:', event.record.id); } async function deleteTodo(event) { await client.storage.delete('todo_items', event.record.id); console.log('Deleted:', event.record.id); } client.ui.registerActions({ create_todo: createTodo, toggle_todo: toggleTodo, delete_todo: deleteTodo }); console.log('All handlers registered. App is live.'); ``` ## Exercises 1. Register all three action handlers. Confirm the log message appears. 2. Simulate a `create_todo` event by calling `createTodo({ state: { new_todo_title: 'Test item' }, record: null })` directly. Then query `todo_items` storage to confirm the record was inserted. 3. Simulate a `toggle_todo` event on the record you just created. Query the storage to confirm `completed` changed to `true`. ## Expected Output ``` All handlers registered. App is live. Created: Test item Toggled: <uuid> ``` ## Troubleshooting Notes 1. **Error: `action handler not found`** — Ensure the action name in `registerActions()` matches the `action` property in your UI schema exactly. 2. **Error: `schema not registered`** — You must register the storage schema before calling `storage.insert()`. Complete Lesson 1 first. 3. **Handler does not fire** — Check that your handler is an `async` function and that you have called `registerActions()` before the UI is rendered. ## Validation Steps 1. Confirm all three handlers are registered without error. 2. Confirm that calling `createTodo` inserts a record into `todo_items` storage. 3. Confirm that calling `toggleTodo` updates the `completed` field of an existing record.

  4. 4
    Observing Real-Time UI Updates

    ## Module Overview In this final lesson, you will connect the storage and UI layers so that any change to storage automatically updates the rendered interface. You will subscribe to storage changes and confirm that your Todo app updates in real time as items are created, toggled, and deleted. ## Learning Objectives - Subscribe to storage changes for a specific schema - Observe real-time UI updates when records change - Test the full create-toggle-delete cycle end to end - Verify the app is production-ready for deployment ## Lesson The DOSFI SDK automatically propagates storage changes to bound UI components. When you call `storage.insert()`, `storage.update()`, or `storage.delete()`, the mesh notifies all subscribed clients, and the UI re-renders. ### Subscribing to Storage Changes ```javascript const subscription = client.storage.subscribe('todo_items', (change) => { console.log('Storage change:', change.type); console.log('Record:', change.record); }); ``` The `change` object has three types: - `insert` — A new record was added. - `update` — An existing record was modified. - `delete` — A record was removed. ### The Full Application Here is the complete Todo app, combining schemas, handlers, and the subscription: ```javascript import { client } from './dosfi.config.js'; async function createTodo(event) { const title = event.state.new_todo_title; if (!title?.trim()) return; const todo = { id: crypto.randomUUID(), title: title.trim(), completed: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; await client.storage.insert('todo_items', todo); } async function toggleTodo(event) { const updated = { ...event.record, completed: !event.record.completed, updated_at: new Date().toISOString() }; await client.storage.update('todo_items', event.record.id, updated); } async function deleteTodo(event) { await client.storage.delete('todo_items', event.record.id); } client.ui.registerActions({ create_todo: createTodo, toggle_todo: toggleTodo, delete_todo: deleteTodo }); const storageSub = client.storage.subscribe('todo_items', (change) => { console.log(`[${change.type}] ${change.record?.title || change.record?.id}`); }); console.log('Todo app is running. Interact with the UI to see real-time updates.'); ``` ### Testing the Full Cycle 1. Type a title in the input field and click "Add". 2. Observe the console: `[insert] Buy groceries`. 3. Observe the UI: the new item appears in the list. 4. Click the checkbox next to the item. 5. Observe the console: `[update] Buy groceries`. 6. Observe the UI: the item shows as checked. 7. Click "Delete". 8. Observe the console: `[delete] <uuid>`. 9. Observe the UI: the item disappears. ## Text-Based Diagram ``` [User: Add "Buy groceries"] | v [createTodo() -> storage.insert()] | v [Mesh storage updated] | +---> [storage.subscribe callback: insert] +---> [UI re-renders: item appears] | [User: Toggle checkbox] | v [toggleTodo() -> storage.update()] | v [Mesh storage updated] | +---> [storage.subscribe callback: update] +---> [UI re-renders: item checked] | [User: Click Delete] | v [deleteTodo() -> storage.delete()] | v [Mesh storage updated] | +---> [storage.subscribe callback: delete] +---> [UI re-renders: item removed] ``` ## Example A test script that programmatically exercises the full cycle: ```javascript import { client } from './dosfi.config.js'; async function testFullCycle() { const sub = client.storage.subscribe('todo_items', (change) => { console.log(`[${change.type}]`, change.record?.title || change.record?.id); }); await createTodo({ state: { new_todo_title: 'Test item' }, record: null }); await new Promise(r => setTimeout(r, 500)); const items = await client.storage.list('todo_items'); const item = items[0]; await toggleTodo({ state: {}, record: item }); await deleteTodo({ state: {}, record: { id: item.id } }); sub.unsubscribe(); console.log('Full cycle complete.'); } testFullCycle(); ``` ## Exercises 1. Run the full application and perform the create-toggle-delete cycle through the UI. Confirm the console logs match the expected output. 2. Open a second browser tab with the same app. Create a todo in the first tab. Confirm it appears in the second tab in real time. 3. Add a `console.log` inside the storage subscription callback that counts total changes. Perform 5 operations and confirm the count reaches 5. ## Expected Output ``` Todo app is running. Interact with the UI to see real-time updates. [insert] Buy groceries [update] Buy groceries [delete] <uuid> Full cycle complete. ``` ## Troubleshooting Notes 1. **UI does not update after storage change** — Ensure your UI schema has a `bind` on the `list` component pointing to `todo_items`. The bind name must match the storage schema name exactly. 2. **Subscription callback fires multiple times** — This can happen if you subscribe multiple times. Ensure you only call `storage.subscribe()` once per schema. 3. **Second tab does not receive updates** — Both tabs must use the same identity and the same schema names. Verify your `dosfi.config.js` is identical across tabs. ## Validation Steps 1. Confirm the storage subscription callback fires for insert, update, and delete operations. 2. Confirm the UI re-renders after each storage change without a page reload. 3. Confirm that a second browser tab receives real-time updates when changes are made in the first tab.