Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tim-smart/effect-atom/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The AtomLivestore module provides integration between Effect Atom and Livestore, enabling offline-first, real-time data synchronization with local-first architecture.
Livestore is a local-first database that automatically syncs with your backend. It provides reactive queries, optimistic updates, and conflict resolution out of the box.

Installation

pnpm add @effect-atom/atom-livestore @livestore/livestore

Creating a Livestore client

Use AtomLivestore.Tag to create a special Context.Tag that builds a Livestore instance:
import { AtomLivestore } from "@effect-atom/atom-livestore"
import type { LiveStoreSchema } from "@livestore/livestore"

// Define your schema
interface MySchema extends LiveStoreSchema {
  users: {
    id: string
    name: string
    email: string
  }
  posts: {
    id: string
    userId: string
    title: string
    content: string
  }
}

// Create the Livestore client tag
class MyStore extends AtomLivestore.Tag<MyStore>()("MyStore", {
  schema: {
    users: {
      primaryKey: "id",
      fields: {
        id: { type: "string" },
        name: { type: "string" },
        email: { type: "string" }
      }
    },
    posts: {
      primaryKey: "id",
      fields: {
        id: { type: "string" },
        userId: { type: "string" },
        title: { type: "string" },
        content: { type: "string" }
      },
      indexes: {
        userId: { fields: ["userId"] }
      }
    }
  } satisfies CreateStoreOptions<MySchema>
}) {}

Type signature

interface AtomLiveStore<Self, Id extends string, S extends LiveStoreSchema, Context = {}> {
  // The layer for providing the store
  readonly layer: Atom.Atom<Layer.Layer<Self>>
  
  // Runtime for creating atoms
  readonly runtime: Atom.AtomRuntime<Self>
  
  // Access the store (with Result)
  readonly store: Atom.Atom<Result.Result<Store<S, Context>>>
  
  // Access the store (unsafe, may be undefined)
  readonly storeUnsafe: Atom.Atom<Store<S, Context> | undefined>
  
  // Create a reactive query atom
  readonly makeQuery: <A>(
    query: LiveQueryDef<A> | ((get: Atom.Context) => LiveQueryDef<A>)
  ) => Atom.Atom<Result.Result<A>>
  
  // Create a reactive query atom (unsafe)
  readonly makeQueryUnsafe: <A>(
    query: LiveQueryDef<A> | ((get: Atom.Context) => LiveQueryDef<A>)
  ) => Atom.Atom<A | undefined>
  
  // Commit events to the store
  readonly commit: Atom.Writable<void, {}>
}

Querying data

Create reactive queries that automatically update when data changes:
import { useAtomValue } from "@effect-atom/atom-react"

const userAtom = MyStore.makeQuery({
  table: "users",
  where: { id: "user-123" }
})

function UserProfile() {
  const result = useAtomValue(userAtom)
  
  return Result.builder(result)
    .onInitial(() => <div>Loading...</div>)
    .onSuccess((user) => (
      <div>
        <h1>{user.name}</h1>
        <p>{user.email}</p>
      </div>
    ))
    .render()
}

Mutating data

Commit events to modify data in the store:
import { useAtomSet } from "@effect-atom/atom-react"

function CreateUser() {
  const commit = useAtomSet(MyStore.commit)
  
  const handleCreate = () => {
    commit({
      type: "users.create",
      payload: {
        id: crypto.randomUUID(),
        name: "Alice",
        email: "alice@example.com"
      }
    })
  }
  
  return <button onClick={handleCreate}>Create user</button>
}

Direct store access

Access the Livestore instance directly for advanced operations:
const customAtom = Atom.make((get) => {
  const store = get(MyStore.storeUnsafe)
  
  if (!store) {
    return []
  }
  
  // Use Livestore APIs directly
  const users = store.query({
    table: "users",
    where: { email: { $contains: "@example.com" } }
  })
  
  return users
})

Offline-first features

Automatic synchronization

Livestore automatically syncs changes with your backend:
class MyStore extends AtomLivestore.Tag<MyStore>()("MyStore", {
  schema: mySchema,
  // Configure sync endpoint
  syncUrl: "https://api.example.com/sync",
  // Optional: configure sync behavior
  syncInterval: 5000, // Sync every 5 seconds
  syncOnConnect: true
}) {}

Conflict resolution

Livestore handles conflicts automatically using CRDTs:
Ensure your backend is configured to handle Livestore’s sync protocol. See Livestore documentation for server setup.

Optimistic updates

Changes are applied immediately to the local store, then synced:
function IncrementCounter({ id }: { id: string }) {
  const commit = useAtomSet(MyStore.commit)
  const counter = useAtomValue(MyStore.makeQuery({
    table: "counters",
    where: { id }
  }))
  
  const handleIncrement = () => {
    // This is applied immediately, even while offline
    commit({
      type: "counters.update",
      payload: {
        id,
        value: Result.getOrElse(counter, () => ({ value: 0 })).value + 1
      }
    })
  }
  
  return <button onClick={handleIncrement}>Increment</button>
}

Using with Effect services

Integrate Livestore into your Effect services:
import { Effect } from "effect"

class UserService extends Effect.Service<UserService>()("UserService", {
  dependencies: [MyStore.layer],
  effect: Effect.gen(function*() {
    const store = yield* MyStore
    
    const createUser = (name: string, email: string) =>
      Effect.sync(() => {
        store.commit({
          type: "users.create",
          payload: {
            id: crypto.randomUUID(),
            name,
            email
          }
        })
      })
    
    const getUser = (id: string) =>
      Effect.sync(() =>
        store.query({ table: "users", where: { id } })
      )
    
    return { createUser, getUser } as const
  })
}) {}

// Use in atoms
const createUserAtom = MyStore.runtime.fn(
  Effect.fnUntraced(function*(name: string, email: string) {
    const service = yield* UserService
    yield* service.createUser(name, email)
  })
)

OpenTelemetry integration

Enable tracing for Livestore operations:
import { NodeSdk } from "@opentelemetry/sdk-node"

const sdk = new NodeSdk({
  // Your OpenTelemetry config
})
sdk.start()

class MyStore extends AtomLivestore.Tag<MyStore>()("MyStore", {
  schema: mySchema,
  otelOptions: {
    tracer: sdk.getTracer("my-app")
  }
}) {}

Dynamic configuration

Configure the store dynamically based on atom context:
const userIdAtom = Atom.make("user-123")

class UserStore extends AtomLivestore.Tag<UserStore>()("UserStore", (get) => ({
  schema: mySchema,
  // Access other atoms to configure the store
  syncUrl: `https://api.example.com/sync/${get(userIdAtom)}`
})) {}

Best practices

1
Design your schema carefully
2
Define indexes for fields you’ll query frequently:
3
schema: {
  posts: {
    primaryKey: "id",
    fields: { /* ... */ },
    indexes: {
      userId: { fields: ["userId"] },
      createdAt: { fields: ["createdAt"] }
    }
  }
}
4
Use parameterized queries
5
Leverage Atom.family for reusable, parameterized queries:
6
const postAtom = Atom.family((id: string) =>
  MyStore.makeQuery({ table: "posts", where: { id } })
)
7
Handle loading states
8
Always handle the Initial state for better UX:
9
Result.builder(result)
  .onInitial(() => <Skeleton />)
  .onSuccess((data) => <Content data={data} />)
  .render()
10
Batch commits when possible
11
Group related changes into a single commit for better performance:
12
commit([
  { type: "users.update", payload: { id: "1", name: "Alice" } },
  { type: "posts.create", payload: { id: "2", userId: "1", title: "Hello" } }
])

Resources

Build docs developers (and LLMs) love