# Effect

LiveStore itself is built on top of [Effect](https://effect.website) which is a powerful library to write production-grade TypeScript code. It's also possible (and recommended) to use Effect directly in your application code.

## Store context for Effect

For applications built with Effect, LiveStore provides `makeStoreContext()` - a factory that creates typed store contexts for the Effect layer system. This preserves your schema types through Effect's dependency injection and supports multiple stores.

See the [Store Effect integration](/building-with-livestore/store#effect-integration) section for details on:
- Creating typed store contexts
- Using stores in Effect services
- Layer composition patterns
- Multiple stores in the same app

## Schema

LiveStore uses the [Effect Schema](https://effect.website/docs/schema/introduction/) library to define schemas for the following:

- Read model table column definitions
- Event event payloads definitions
- Query response types

For convenience, LiveStore re-exports the `Schema` module from the `effect` package, which is the same as if you'd import it via `import { Schema } from 'effect'` directly.

## `Equal` and `Hash` Traits

LiveStore's reactive primitives (`LiveQueryDef` and `SignalDef`) implement Effect's `Equal` and `Hash` traits, enabling efficient integration with Effect's data structures and collections.

## Effect atom integration

LiveStore integrates seamlessly with [Effect Atom](https://github.com/effect-atom/effect-atom) for reactive state management in React applications. This provides a powerful combination of Effect's functional programming capabilities with LiveStore's event sourcing and CQRS patterns.

Effect Atom is an external package developed by [Tim Smart](https://github.com/tim-smart) that provides a more Effect-idiomatic alternative to the `@livestore/react` package. While `@livestore/react` offers a straightforward React integration, Effect Atom leverages Effect API/patterns throughout, making it a natural choice for applications already using Effect.

### Installation

```bash
pnpm install @effect-atom/atom-livestore @effect-atom/atom-react
```

### Store creation

Create a LiveStore-backed atom store with persistence and worker support using the `AtomLivestore.Tag` pattern:


## `patterns/effect/store-setup/atoms.ts`

```ts filename="patterns/effect/store-setup/atoms.ts"
import { AtomLivestore } from '@effect-atom/atom-livestore'
import { unstable_batchedUpdates } from 'react-dom'

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from '@livestore/adapter-web/worker?worker'

import { schema } from './schema.ts'

export { schema } from './schema.ts'

// Create a persistent adapter with OPFS storage
const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

// Define the store as a service tag
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
  schema,
  storeId: 'default',
  adapter,
  batchUpdates: unstable_batchedUpdates, // React batching for performance
}) {}
```

### `patterns/effect/store-setup/schema.ts`

```ts filename="patterns/effect/store-setup/schema.ts"
import { Option } from 'effect'

import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// Define event payloads
export const events = {
  userCreated: Events.clientOnly({
    name: 'userCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      email: Schema.String,
    }),
  }),
  userUpdated: Events.clientOnly({
    name: 'userUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      email: Schema.optionalWith(Schema.String, { as: 'Option' }),
      isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
    }),
  }),
  productCreated: Events.clientOnly({
    name: 'productCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      description: Schema.String,
      price: Schema.Number,
    }),
  }),
  productUpdated: Events.clientOnly({
    name: 'productUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      description: Schema.optionalWith(Schema.String, { as: 'Option' }),
      price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
    }),
  }),
  todoCreated: Events.clientOnly({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  todoToggled: Events.clientOnly({
    name: 'todoToggled',
    schema: Schema.Struct({
      id: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  itemCreated: Events.clientOnly({
    name: 'itemCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
    }),
  }),
  itemUpdated: Events.clientOnly({
    name: 'itemUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      status: Schema.String,
    }),
  }),
}

// Define tables
const tables = {
  users: State.SQLite.table({
    name: 'users',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      email: State.SQLite.text(),
      isActive: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  products: State.SQLite.table({
    name: 'products',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      description: State.SQLite.text(),
      price: State.SQLite.real(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
}

// Define materializers
const materializers = State.SQLite.materializers(events, {
  userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
  userUpdated: ({ id, name, email, isActive }) => {
    const updates: { name?: string; email?: string; isActive?: boolean } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(email) === true) updates.email = email.value
    if (Option.isSome(isActive) === true) updates.isActive = isActive.value
    return tables.users.update(updates).where({ id })
  },
  todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
  todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
  productCreated: ({ id, name, description, price }) =>
    tables.products.insert({ id, name, description, price, createdAt: new Date() }),
  productUpdated: ({ id, name, description, price }) => {
    const updates: { name?: string; description?: string; price?: number } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(description) === true) updates.description = description.value
    if (Option.isSome(price) === true) updates.price = price.value
    return tables.products.update(updates).where({ id })
  },
  itemCreated: () => [], // Item events don't have a corresponding table
  itemUpdated: () => [], // Item events don't have a corresponding table
})

// Create state
const state = State.SQLite.makeState({ tables, materializers })

// Create the store schema
export const schema = makeSchema({ events, state })

export { tables }
```

The `StoreTag` class provides the following static methods:
- `StoreTag.runtime` - Access to Effect runtime
- `StoreTag.commit` - Commit events to the store
- `StoreTag.store` - Access store with Effect
- `StoreTag.storeUnsafe` - Direct store access when store is already loaded (synchronous)
- `StoreTag.makeQuery` - Create query atoms with Effect
- `StoreTag.makeQueryUnsafe` - Create query atoms without Effect

### Defining query atoms

Create reactive query atoms that automatically update when the underlying data changes:


## `patterns/effect/store-setup/queries.ts`

```ts filename="patterns/effect/store-setup/queries.ts"
import { Atom } from '@effect-atom/atom'
import { Schema } from 'effect'

import { queryDb, sql } from '@livestore/livestore'

import { StoreTag } from './atoms.ts'

// User schema for type safety
const User = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  isActive: Schema.Boolean,
})

const Product = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc,
})

// Search term atom for dynamic queries
export const searchTermAtom = Atom.make<string>('')

// Re-export from utils for convenience
export { usersQueryAtom as usersAtom } from './utils.ts'

// Query with SQL
export const activeUsersAtom = StoreTag.makeQuery(
  queryDb({
    query: sql`SELECT * FROM users WHERE isActive = true ORDER BY name`,
    schema: Schema.Array(User),
  }),
)

// Static query example - dynamic queries would need a different approach
// For dynamic queries, you'd typically use a derived atom that depends on searchTermAtom
export const searchResultsAtom = StoreTag.makeQuery(
  queryDb({
    query: sql`SELECT * FROM products ORDER BY createdAt DESC`,
    schema: Schema.Array(Product),
  }),
)
```

### `patterns/effect/store-setup/atoms.ts`

```ts filename="patterns/effect/store-setup/atoms.ts"
import { AtomLivestore } from '@effect-atom/atom-livestore'
import { unstable_batchedUpdates } from 'react-dom'

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from '@livestore/adapter-web/worker?worker'

import { schema } from './schema.ts'

export { schema } from './schema.ts'

// Create a persistent adapter with OPFS storage
const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

// Define the store as a service tag
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
  schema,
  storeId: 'default',
  adapter,
  batchUpdates: unstable_batchedUpdates, // React batching for performance
}) {}
```

### `patterns/effect/store-setup/schema.ts`

```ts filename="patterns/effect/store-setup/schema.ts"
import { Option } from 'effect'

import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// Define event payloads
export const events = {
  userCreated: Events.clientOnly({
    name: 'userCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      email: Schema.String,
    }),
  }),
  userUpdated: Events.clientOnly({
    name: 'userUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      email: Schema.optionalWith(Schema.String, { as: 'Option' }),
      isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
    }),
  }),
  productCreated: Events.clientOnly({
    name: 'productCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      description: Schema.String,
      price: Schema.Number,
    }),
  }),
  productUpdated: Events.clientOnly({
    name: 'productUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      description: Schema.optionalWith(Schema.String, { as: 'Option' }),
      price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
    }),
  }),
  todoCreated: Events.clientOnly({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  todoToggled: Events.clientOnly({
    name: 'todoToggled',
    schema: Schema.Struct({
      id: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  itemCreated: Events.clientOnly({
    name: 'itemCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
    }),
  }),
  itemUpdated: Events.clientOnly({
    name: 'itemUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      status: Schema.String,
    }),
  }),
}

// Define tables
const tables = {
  users: State.SQLite.table({
    name: 'users',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      email: State.SQLite.text(),
      isActive: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  products: State.SQLite.table({
    name: 'products',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      description: State.SQLite.text(),
      price: State.SQLite.real(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
}

// Define materializers
const materializers = State.SQLite.materializers(events, {
  userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
  userUpdated: ({ id, name, email, isActive }) => {
    const updates: { name?: string; email?: string; isActive?: boolean } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(email) === true) updates.email = email.value
    if (Option.isSome(isActive) === true) updates.isActive = isActive.value
    return tables.users.update(updates).where({ id })
  },
  todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
  todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
  productCreated: ({ id, name, description, price }) =>
    tables.products.insert({ id, name, description, price, createdAt: new Date() }),
  productUpdated: ({ id, name, description, price }) => {
    const updates: { name?: string; description?: string; price?: number } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(description) === true) updates.description = description.value
    if (Option.isSome(price) === true) updates.price = price.value
    return tables.products.update(updates).where({ id })
  },
  itemCreated: () => [], // Item events don't have a corresponding table
  itemUpdated: () => [], // Item events don't have a corresponding table
})

// Create state
const state = State.SQLite.makeState({ tables, materializers })

// Create the store schema
export const schema = makeSchema({ events, state })

export { tables }
```

### `patterns/effect/store-setup/utils.ts`

```ts filename="patterns/effect/store-setup/utils.ts"
import { Atom } from '@effect-atom/atom'

import { queryDb } from '@livestore/livestore'

import { StoreTag } from './atoms.ts'
import { tables } from './schema.ts'

// Common query atoms that can be reused
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))

// Common types for optimistic updates
export type PendingTodo = { id: string; text: string; completed: boolean }
export type PendingUser = { id: string; name: string; email: string }

// Common pending state atoms
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
```

### Using queries in React components

Access query results in React components with the `useAtomValue()` hook. When using `StoreTag.makeQuery` (non-unsafe API), the result is wrapped in a Result type for proper loading and error handling:


## `patterns/effect/store-setup/user-list.tsx`

```tsx filename="patterns/effect/store-setup/user-list.tsx"
import { Result, useAtomValue } from '@effect-atom/atom-react'

import { activeUsersAtom } from './queries.ts'

export const UserList = () => {
  const users = useAtomValue(activeUsersAtom)

  return Result.builder(users)
    .onInitial(() => <div>Loading users...</div>)
    .onSuccess((users) => (
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    ))
    .onDefect((error: any) => <div>Error: {error.message}</div>)
    .render()
}
```

### `patterns/effect/store-setup/atoms.ts`

```ts filename="patterns/effect/store-setup/atoms.ts"
import { AtomLivestore } from '@effect-atom/atom-livestore'
import { unstable_batchedUpdates } from 'react-dom'

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from '@livestore/adapter-web/worker?worker'

import { schema } from './schema.ts'

export { schema } from './schema.ts'

// Create a persistent adapter with OPFS storage
const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

// Define the store as a service tag
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
  schema,
  storeId: 'default',
  adapter,
  batchUpdates: unstable_batchedUpdates, // React batching for performance
}) {}
```

### `patterns/effect/store-setup/queries.ts`

```ts filename="patterns/effect/store-setup/queries.ts"
import { Atom } from '@effect-atom/atom'
import { Schema } from 'effect'

import { queryDb, sql } from '@livestore/livestore'

import { StoreTag } from './atoms.ts'

// User schema for type safety
const User = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  isActive: Schema.Boolean,
})

const Product = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc,
})

// Search term atom for dynamic queries
export const searchTermAtom = Atom.make<string>('')

// Re-export from utils for convenience
export { usersQueryAtom as usersAtom } from './utils.ts'

// Query with SQL
export const activeUsersAtom = StoreTag.makeQuery(
  queryDb({
    query: sql`SELECT * FROM users WHERE isActive = true ORDER BY name`,
    schema: Schema.Array(User),
  }),
)

// Static query example - dynamic queries would need a different approach
// For dynamic queries, you'd typically use a derived atom that depends on searchTermAtom
export const searchResultsAtom = StoreTag.makeQuery(
  queryDb({
    query: sql`SELECT * FROM products ORDER BY createdAt DESC`,
    schema: Schema.Array(Product),
  }),
)
```

### `patterns/effect/store-setup/schema.ts`

```ts filename="patterns/effect/store-setup/schema.ts"
import { Option } from 'effect'

import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// Define event payloads
export const events = {
  userCreated: Events.clientOnly({
    name: 'userCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      email: Schema.String,
    }),
  }),
  userUpdated: Events.clientOnly({
    name: 'userUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      email: Schema.optionalWith(Schema.String, { as: 'Option' }),
      isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
    }),
  }),
  productCreated: Events.clientOnly({
    name: 'productCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      description: Schema.String,
      price: Schema.Number,
    }),
  }),
  productUpdated: Events.clientOnly({
    name: 'productUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      description: Schema.optionalWith(Schema.String, { as: 'Option' }),
      price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
    }),
  }),
  todoCreated: Events.clientOnly({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  todoToggled: Events.clientOnly({
    name: 'todoToggled',
    schema: Schema.Struct({
      id: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  itemCreated: Events.clientOnly({
    name: 'itemCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
    }),
  }),
  itemUpdated: Events.clientOnly({
    name: 'itemUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      status: Schema.String,
    }),
  }),
}

// Define tables
const tables = {
  users: State.SQLite.table({
    name: 'users',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      email: State.SQLite.text(),
      isActive: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  products: State.SQLite.table({
    name: 'products',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      description: State.SQLite.text(),
      price: State.SQLite.real(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
}

// Define materializers
const materializers = State.SQLite.materializers(events, {
  userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
  userUpdated: ({ id, name, email, isActive }) => {
    const updates: { name?: string; email?: string; isActive?: boolean } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(email) === true) updates.email = email.value
    if (Option.isSome(isActive) === true) updates.isActive = isActive.value
    return tables.users.update(updates).where({ id })
  },
  todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
  todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
  productCreated: ({ id, name, description, price }) =>
    tables.products.insert({ id, name, description, price, createdAt: new Date() }),
  productUpdated: ({ id, name, description, price }) => {
    const updates: { name?: string; description?: string; price?: number } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(description) === true) updates.description = description.value
    if (Option.isSome(price) === true) updates.price = price.value
    return tables.products.update(updates).where({ id })
  },
  itemCreated: () => [], // Item events don't have a corresponding table
  itemUpdated: () => [], // Item events don't have a corresponding table
})

// Create state
const state = State.SQLite.makeState({ tables, materializers })

// Create the store schema
export const schema = makeSchema({ events, state })

export { tables }
```

### `patterns/effect/store-setup/utils.ts`

```ts filename="patterns/effect/store-setup/utils.ts"
import { Atom } from '@effect-atom/atom'

import { queryDb } from '@livestore/livestore'

import { StoreTag } from './atoms.ts'
import { tables } from './schema.ts'

// Common query atoms that can be reused
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))

// Common types for optimistic updates
export type PendingTodo = { id: string; text: string; completed: boolean }
export type PendingUser = { id: string; name: string; email: string }

// Common pending state atoms
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
```

### Integrating Effect services

Combine Effect services with LiveStore operations using the store's runtime:


## `patterns/effect/store-setup/services.tsx`

```tsx filename="patterns/effect/store-setup/services.tsx"
import { useAtomSet } from '@effect-atom/atom-react'
import { Context, Effect } from 'effect'
import { useCallback } from 'react'

import { StoreTag } from './atoms.ts'
import { events } from './schema.ts'

// Example service definition
export class MyService extends Context.Tag('MyService')<
  MyService,
  {
    processItem: (name: string) => Effect.Effect<{
      name: string
      metadata: Record<string, unknown>
    }>
  }
>() {}

// Use the commit hook for event handling
export const useCommit = () => useAtomSet(StoreTag.commit)

// Simple commit example
export const createItemAtom = StoreTag.runtime.fn<string>()((itemName, get) => {
  return Effect.sync(() => {
    const store = get(StoreTag.storeUnsafe)
    if (store !== undefined) {
      store.commit(
        events.itemCreated({
          id: crypto.randomUUID(),
          name: itemName,
          metadata: { createdAt: new Date().toISOString() },
        }),
      )
    }
  })
})

// Use in a React component
export const CreateItemButton = () => {
  const createItem = useAtomSet(createItemAtom)

  const handleClick = useCallback(() => {
    createItem('New Item')
  }, [createItem])

  return (
    <button type="button" onClick={handleClick}>
      Create Item
    </button>
  )
}
```

### `patterns/effect/store-setup/atoms.ts`

```ts filename="patterns/effect/store-setup/atoms.ts"
import { AtomLivestore } from '@effect-atom/atom-livestore'
import { unstable_batchedUpdates } from 'react-dom'

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from '@livestore/adapter-web/worker?worker'

import { schema } from './schema.ts'

export { schema } from './schema.ts'

// Create a persistent adapter with OPFS storage
const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

// Define the store as a service tag
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
  schema,
  storeId: 'default',
  adapter,
  batchUpdates: unstable_batchedUpdates, // React batching for performance
}) {}
```

### `patterns/effect/store-setup/schema.ts`

```ts filename="patterns/effect/store-setup/schema.ts"
import { Option } from 'effect'

import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// Define event payloads
export const events = {
  userCreated: Events.clientOnly({
    name: 'userCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      email: Schema.String,
    }),
  }),
  userUpdated: Events.clientOnly({
    name: 'userUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      email: Schema.optionalWith(Schema.String, { as: 'Option' }),
      isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
    }),
  }),
  productCreated: Events.clientOnly({
    name: 'productCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      description: Schema.String,
      price: Schema.Number,
    }),
  }),
  productUpdated: Events.clientOnly({
    name: 'productUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      description: Schema.optionalWith(Schema.String, { as: 'Option' }),
      price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
    }),
  }),
  todoCreated: Events.clientOnly({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  todoToggled: Events.clientOnly({
    name: 'todoToggled',
    schema: Schema.Struct({
      id: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  itemCreated: Events.clientOnly({
    name: 'itemCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
    }),
  }),
  itemUpdated: Events.clientOnly({
    name: 'itemUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      status: Schema.String,
    }),
  }),
}

// Define tables
const tables = {
  users: State.SQLite.table({
    name: 'users',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      email: State.SQLite.text(),
      isActive: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  products: State.SQLite.table({
    name: 'products',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      description: State.SQLite.text(),
      price: State.SQLite.real(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
}

// Define materializers
const materializers = State.SQLite.materializers(events, {
  userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
  userUpdated: ({ id, name, email, isActive }) => {
    const updates: { name?: string; email?: string; isActive?: boolean } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(email) === true) updates.email = email.value
    if (Option.isSome(isActive) === true) updates.isActive = isActive.value
    return tables.users.update(updates).where({ id })
  },
  todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
  todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
  productCreated: ({ id, name, description, price }) =>
    tables.products.insert({ id, name, description, price, createdAt: new Date() }),
  productUpdated: ({ id, name, description, price }) => {
    const updates: { name?: string; description?: string; price?: number } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(description) === true) updates.description = description.value
    if (Option.isSome(price) === true) updates.price = price.value
    return tables.products.update(updates).where({ id })
  },
  itemCreated: () => [], // Item events don't have a corresponding table
  itemUpdated: () => [], // Item events don't have a corresponding table
})

// Create state
const state = State.SQLite.makeState({ tables, materializers })

// Create the store schema
export const schema = makeSchema({ events, state })

export { tables }
```

### Advanced patterns

#### Optimistic updates

Combine local state with LiveStore for optimistic UI updates. When using `StoreTag.makeQueryUnsafe`, the data is directly available:


## `patterns/effect/optimistic-example/optimistic.ts`

```ts filename="patterns/effect/optimistic-example/optimistic.ts"
import { Atom } from '@effect-atom/atom'

import { pendingTodosAtom, todosQueryUnsafeAtom } from '../store-setup/utils.ts'

// Combine real and pending todos for optimistic UI
export const optimisticTodoAtom = Atom.make((get) => {
  const todos = get(todosQueryUnsafeAtom) // Direct array, not wrapped in Result
  const pending = get(pendingTodosAtom)

  return [...(todos || []), ...pending]
})
```

### `patterns/effect/store-setup/atoms.ts`

```ts filename="patterns/effect/store-setup/atoms.ts"
import { AtomLivestore } from '@effect-atom/atom-livestore'
import { unstable_batchedUpdates } from 'react-dom'

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from '@livestore/adapter-web/worker?worker'

import { schema } from './schema.ts'

export { schema } from './schema.ts'

// Create a persistent adapter with OPFS storage
const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

// Define the store as a service tag
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
  schema,
  storeId: 'default',
  adapter,
  batchUpdates: unstable_batchedUpdates, // React batching for performance
}) {}
```

### `patterns/effect/store-setup/schema.ts`

```ts filename="patterns/effect/store-setup/schema.ts"
import { Option } from 'effect'

import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// Define event payloads
export const events = {
  userCreated: Events.clientOnly({
    name: 'userCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      email: Schema.String,
    }),
  }),
  userUpdated: Events.clientOnly({
    name: 'userUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      email: Schema.optionalWith(Schema.String, { as: 'Option' }),
      isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
    }),
  }),
  productCreated: Events.clientOnly({
    name: 'productCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      description: Schema.String,
      price: Schema.Number,
    }),
  }),
  productUpdated: Events.clientOnly({
    name: 'productUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      description: Schema.optionalWith(Schema.String, { as: 'Option' }),
      price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
    }),
  }),
  todoCreated: Events.clientOnly({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  todoToggled: Events.clientOnly({
    name: 'todoToggled',
    schema: Schema.Struct({
      id: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  itemCreated: Events.clientOnly({
    name: 'itemCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
    }),
  }),
  itemUpdated: Events.clientOnly({
    name: 'itemUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      status: Schema.String,
    }),
  }),
}

// Define tables
const tables = {
  users: State.SQLite.table({
    name: 'users',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      email: State.SQLite.text(),
      isActive: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  products: State.SQLite.table({
    name: 'products',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      description: State.SQLite.text(),
      price: State.SQLite.real(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
}

// Define materializers
const materializers = State.SQLite.materializers(events, {
  userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
  userUpdated: ({ id, name, email, isActive }) => {
    const updates: { name?: string; email?: string; isActive?: boolean } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(email) === true) updates.email = email.value
    if (Option.isSome(isActive) === true) updates.isActive = isActive.value
    return tables.users.update(updates).where({ id })
  },
  todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
  todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
  productCreated: ({ id, name, description, price }) =>
    tables.products.insert({ id, name, description, price, createdAt: new Date() }),
  productUpdated: ({ id, name, description, price }) => {
    const updates: { name?: string; description?: string; price?: number } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(description) === true) updates.description = description.value
    if (Option.isSome(price) === true) updates.price = price.value
    return tables.products.update(updates).where({ id })
  },
  itemCreated: () => [], // Item events don't have a corresponding table
  itemUpdated: () => [], // Item events don't have a corresponding table
})

// Create state
const state = State.SQLite.makeState({ tables, materializers })

// Create the store schema
export const schema = makeSchema({ events, state })

export { tables }
```

### `patterns/effect/store-setup/utils.ts`

```ts filename="patterns/effect/store-setup/utils.ts"
import { Atom } from '@effect-atom/atom'

import { queryDb } from '@livestore/livestore'

import { StoreTag } from './atoms.ts'
import { tables } from './schema.ts'

// Common query atoms that can be reused
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))

// Common types for optimistic updates
export type PendingTodo = { id: string; text: string; completed: boolean }
export type PendingUser = { id: string; name: string; email: string }

// Common pending state atoms
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
```

#### Derived state

Create computed atoms based on LiveStore queries. When using the non-unsafe API, handle the Result type:


## `patterns/effect/derived-example/derived.ts`

```ts filename="patterns/effect/derived-example/derived.ts"
import { Atom } from '@effect-atom/atom'
import { Result } from '@effect-atom/atom-react'

import { todosQueryAtom } from '../store-setup/utils.ts'

// Derive statistics from todos
export const todoStatsAtom = Atom.make((get) => {
  const todos = get(todosQueryAtom) // Result wrapped

  return Result.map(todos, (todoList) => ({
    total: todoList.length,
    completed: todoList.filter((t) => t.completed).length,
    pending: todoList.filter((t) => !t.completed).length,
  }))
})
```

### `patterns/effect/store-setup/atoms.ts`

```ts filename="patterns/effect/store-setup/atoms.ts"
import { AtomLivestore } from '@effect-atom/atom-livestore'
import { unstable_batchedUpdates } from 'react-dom'

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from '@livestore/adapter-web/worker?worker'

import { schema } from './schema.ts'

export { schema } from './schema.ts'

// Create a persistent adapter with OPFS storage
const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

// Define the store as a service tag
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
  schema,
  storeId: 'default',
  adapter,
  batchUpdates: unstable_batchedUpdates, // React batching for performance
}) {}
```

### `patterns/effect/store-setup/schema.ts`

```ts filename="patterns/effect/store-setup/schema.ts"
import { Option } from 'effect'

import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// Define event payloads
export const events = {
  userCreated: Events.clientOnly({
    name: 'userCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      email: Schema.String,
    }),
  }),
  userUpdated: Events.clientOnly({
    name: 'userUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      email: Schema.optionalWith(Schema.String, { as: 'Option' }),
      isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
    }),
  }),
  productCreated: Events.clientOnly({
    name: 'productCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      description: Schema.String,
      price: Schema.Number,
    }),
  }),
  productUpdated: Events.clientOnly({
    name: 'productUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      description: Schema.optionalWith(Schema.String, { as: 'Option' }),
      price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
    }),
  }),
  todoCreated: Events.clientOnly({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  todoToggled: Events.clientOnly({
    name: 'todoToggled',
    schema: Schema.Struct({
      id: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  itemCreated: Events.clientOnly({
    name: 'itemCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
    }),
  }),
  itemUpdated: Events.clientOnly({
    name: 'itemUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      status: Schema.String,
    }),
  }),
}

// Define tables
const tables = {
  users: State.SQLite.table({
    name: 'users',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      email: State.SQLite.text(),
      isActive: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  products: State.SQLite.table({
    name: 'products',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      description: State.SQLite.text(),
      price: State.SQLite.real(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
}

// Define materializers
const materializers = State.SQLite.materializers(events, {
  userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
  userUpdated: ({ id, name, email, isActive }) => {
    const updates: { name?: string; email?: string; isActive?: boolean } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(email) === true) updates.email = email.value
    if (Option.isSome(isActive) === true) updates.isActive = isActive.value
    return tables.users.update(updates).where({ id })
  },
  todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
  todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
  productCreated: ({ id, name, description, price }) =>
    tables.products.insert({ id, name, description, price, createdAt: new Date() }),
  productUpdated: ({ id, name, description, price }) => {
    const updates: { name?: string; description?: string; price?: number } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(description) === true) updates.description = description.value
    if (Option.isSome(price) === true) updates.price = price.value
    return tables.products.update(updates).where({ id })
  },
  itemCreated: () => [], // Item events don't have a corresponding table
  itemUpdated: () => [], // Item events don't have a corresponding table
})

// Create state
const state = State.SQLite.makeState({ tables, materializers })

// Create the store schema
export const schema = makeSchema({ events, state })

export { tables }
```

### `patterns/effect/store-setup/utils.ts`

```ts filename="patterns/effect/store-setup/utils.ts"
import { Atom } from '@effect-atom/atom'

import { queryDb } from '@livestore/livestore'

import { StoreTag } from './atoms.ts'
import { tables } from './schema.ts'

// Common query atoms that can be reused
export const todosQueryAtom = StoreTag.makeQuery(queryDb(tables.todos))
export const todosQueryUnsafeAtom = StoreTag.makeQueryUnsafe(queryDb(tables.todos))
export const usersQueryAtom = StoreTag.makeQuery(queryDb(tables.users))
export const productsQueryAtom = StoreTag.makeQuery(queryDb(tables.products))

// Common types for optimistic updates
export type PendingTodo = { id: string; text: string; completed: boolean }
export type PendingUser = { id: string; name: string; email: string }

// Common pending state atoms
export const pendingTodosAtom = Atom.make<PendingTodo[]>([])
export const pendingUsersAtom = Atom.make<PendingUser[]>([])
```

#### Batch operations

Perform multiple commits efficiently (commits are synchronous):


## `patterns/effect/batch-example/batch.ts`

```ts filename="patterns/effect/batch-example/batch.ts"
import { Effect } from 'effect'

import { StoreTag } from '../store-setup/atoms.ts'
import { events } from '../store-setup/schema.ts'

// Bulk update atom for batch operations
export const bulkUpdateAtom = StoreTag.runtime.fn<string[]>()(
  Effect.fn(function* (ids, get) {
    const store = get(StoreTag.storeUnsafe)
    if (store == null) return

    // Commit multiple events synchronously
    for (const id of ids) {
      store.commit(events.itemUpdated({ id, status: 'processed' }))
    }
  }),
)
```

### `patterns/effect/store-setup/atoms.ts`

```ts filename="patterns/effect/store-setup/atoms.ts"
import { AtomLivestore } from '@effect-atom/atom-livestore'
import { unstable_batchedUpdates } from 'react-dom'

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from '@livestore/adapter-web/worker?worker'

import { schema } from './schema.ts'

export { schema } from './schema.ts'

// Create a persistent adapter with OPFS storage
const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

// Define the store as a service tag
export class StoreTag extends AtomLivestore.Tag<StoreTag>()('StoreTag', {
  schema,
  storeId: 'default',
  adapter,
  batchUpdates: unstable_batchedUpdates, // React batching for performance
}) {}
```

### `patterns/effect/store-setup/schema.ts`

```ts filename="patterns/effect/store-setup/schema.ts"
import { Option } from 'effect'

import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// Define event payloads
export const events = {
  userCreated: Events.clientOnly({
    name: 'userCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      email: Schema.String,
    }),
  }),
  userUpdated: Events.clientOnly({
    name: 'userUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      email: Schema.optionalWith(Schema.String, { as: 'Option' }),
      isActive: Schema.optionalWith(Schema.Boolean, { as: 'Option' }),
    }),
  }),
  productCreated: Events.clientOnly({
    name: 'productCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      description: Schema.String,
      price: Schema.Number,
    }),
  }),
  productUpdated: Events.clientOnly({
    name: 'productUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.optionalWith(Schema.String, { as: 'Option' }),
      description: Schema.optionalWith(Schema.String, { as: 'Option' }),
      price: Schema.optionalWith(Schema.Number, { as: 'Option' }),
    }),
  }),
  todoCreated: Events.clientOnly({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  todoToggled: Events.clientOnly({
    name: 'todoToggled',
    schema: Schema.Struct({
      id: Schema.String,
      completed: Schema.Boolean,
    }),
  }),
  itemCreated: Events.clientOnly({
    name: 'itemCreated',
    schema: Schema.Struct({
      id: Schema.String,
      name: Schema.String,
      metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
    }),
  }),
  itemUpdated: Events.clientOnly({
    name: 'itemUpdated',
    schema: Schema.Struct({
      id: Schema.String,
      status: Schema.String,
    }),
  }),
}

// Define tables
const tables = {
  users: State.SQLite.table({
    name: 'users',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      email: State.SQLite.text(),
      isActive: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  products: State.SQLite.table({
    name: 'products',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      name: State.SQLite.text(),
      description: State.SQLite.text(),
      price: State.SQLite.real(),
      createdAt: State.SQLite.datetime(),
    },
  }),
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean(),
      createdAt: State.SQLite.datetime(),
    },
  }),
}

// Define materializers
const materializers = State.SQLite.materializers(events, {
  userCreated: ({ id, name, email }) => tables.users.insert({ id, name, email, isActive: true, createdAt: new Date() }),
  userUpdated: ({ id, name, email, isActive }) => {
    const updates: { name?: string; email?: string; isActive?: boolean } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(email) === true) updates.email = email.value
    if (Option.isSome(isActive) === true) updates.isActive = isActive.value
    return tables.users.update(updates).where({ id })
  },
  todoCreated: ({ id, text, completed }) => tables.todos.insert({ id, text, completed, createdAt: new Date() }),
  todoToggled: ({ id, completed }) => tables.todos.update({ completed }).where({ id }),
  productCreated: ({ id, name, description, price }) =>
    tables.products.insert({ id, name, description, price, createdAt: new Date() }),
  productUpdated: ({ id, name, description, price }) => {
    const updates: { name?: string; description?: string; price?: number } = {}
    if (Option.isSome(name) === true) updates.name = name.value
    if (Option.isSome(description) === true) updates.description = description.value
    if (Option.isSome(price) === true) updates.price = price.value
    return tables.products.update(updates).where({ id })
  },
  itemCreated: () => [], // Item events don't have a corresponding table
  itemUpdated: () => [], // Item events don't have a corresponding table
})

// Create state
const state = State.SQLite.makeState({ tables, materializers })

// Create the store schema
export const schema = makeSchema({ events, state })

export { tables }
```

### Best practices

1. **Use `StoreTag.makeQuery` for queries**: This ensures proper Effect integration and error handling
2. **Leverage Effect services**: Integrate business logic through Effect services for better testability
3. **Handle loading states**: Use `Result.builder` pattern for consistent loading/error UI
4. **Batch React updates**: Always provide `batchUpdates` for better performance
5. **Label queries**: Add descriptive labels to queries for better debugging
6. **Type safety**: Let TypeScript infer types from schemas rather than manual annotations

### Real-World Example

For a comprehensive example of LiveStore with Effect Atom in action, check out [Cheffect](https://github.com/tim-smart/cheffect) - a recipe management application that demonstrates:
- Complete Effect service integration
- AI-powered recipe extraction using Effect services
- Complex query patterns with search and filtering
- Worker-based persistence with OPFS
- Production-ready error handling and logging