Skip to content

Todo app with shared workspaces

Let’s consider a fairly common application scenario: An app (in this case a todo app) with shared workspaces. For the sake of this guide, we’ll keep things simple but you should be able to nicely extend this to a more complex app.

  • There are multiple independent todo workspaces
  • Each workspace is initially created by a single user
  • Users can join the workspace by knowing the workspace id and get read and write access
  • For simplicity, the user identity is chosen when the app initially starts (i.e. a username) but in a real app this would be handled by a proper auth setup
  • We are splitting up our data model into two kinds of stores (with respective eventlogs and SQLite databases): The workspace store and the user store.

For the workspace store we have the following events:

  • workspaceCreated
  • todoAdded
  • todoCompleted
  • todoDeleted
  • userJoined

And the following state model:

  • workspace table (with a single row for the workspace itself)
  • todo table (with one row per todo item)
  • member table (with one row per user who has joined the workspace)

For the user store we have the following events:

  • workspaceCreated
  • workspaceJoined

And the following state model:

  • user table (with a single row for the user itself)

Note that the workspaceCreated event is used both in the workspace and the user store. This is because each eventlog should be “self-sufficient” and not rely on other eventlogs to be present to fulfill its purpose.

import { Events, makeSchema, Schema, State } from '@livestore/livestore'
// Emitted when this user creates a new workspace
const workspaceCreated = Events.synced({
name: 'v1.ListCreated',
schema: Schema.Struct({ workspaceId: Schema.String }),
})
// Emitted when this user joins an existing workspace
const workspaceJoined = Events.synced({
name: 'v1.ListJoined',
schema: Schema.Struct({ workspaceId: Schema.String }),
})
const events = { workspaceCreated, workspaceJoined }
// Table to store basic user info
// Contains only one row as this store is per-user.
const userTable = State.SQLite.table({
name: 'user',
columns: {
// Assuming username is unique and used as the identifier
username: State.SQLite.text({ primaryKey: true }),
},
})
// Table to track which workspaces this user is part of
const userListTable = State.SQLite.table({
name: 'userList',
columns: {
workspaceId: State.SQLite.text({ primaryKey: true }),
// Could add role/permissions here later
},
})
const tables = { user: userTable, userList: userListTable }
const materializers = State.SQLite.materializers(events, {
// When the user creates or joins a workspace, add it to their workspace table
workspaceCreated: ({ workspaceId }) => tables.userList.insert({ workspaceId }),
workspaceJoined: ({ workspaceId }) => tables.userList.insert({ workspaceId }),
})
const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ events, state })

To make this app more production-ready, we might want to do the following:

  • Use a proper auth setup to enforce a trusted user identity
  • Introduce a proper user invite process
  • Introduce access levels (e.g. read-only, read-write)
  • Introduce end-to-end encryption
  • If each todo item has a lot of data (e.g. think of a GitHub/Linear issue with lots of details), it might make sense to split up each todo item into its own store.