Skip to content

Frequently Asked Questions

Common questions about Verity's philosophy, usage, and comparisons


Philosophy & Concepts

Why no optimistic updates?

Question

Every other data library supports optimistic updates. Why does Verity reject them?

Answer:

Optimistic updates are temporary lies. They show users a state that might not be true, then "correct" it when the server responds. This creates:

  • Flicker: UI updates optimistically, then changes again when truth arrives
  • Mismatch: Optimistic state diverges from server reality
  • Broken trust: Users learn not to trust what they see

Verity's position: The UI should reflect what the server confirms, not what we hope will happen.

Alternative approaches:

Build endpoints that return quickly:

@app.put('/api/todos/<int:id>/toggle')
def toggle_todo(id):
    todo.completed = not todo.completed  # Fast DB write
    db.session.commit()
    return { 'directives': [...], 'result': todo.to_dict() }
# Response time: ~50-100ms

Include updated data in directives:

return {
    'directives': [{
        'op': 'refresh_item',
        'name': 'todo',
        'id': id,
        'result': todo.to_dict()  # ← No refetch needed
    }]
}

Show spinners during work:

<button 
  @click="toggleTodo(todo)"
  :disabled="isTogglingtodo.id"
>
  <span x-show="!isToggling">Toggle</span>
  <span x-show="isToggling" class="spinner"></span>
</button>

See: Philosophy


What's the difference between truth-state and view-state?

Question

I keep mixing these up. How do I know which is which?

Answer:

Use the smell tests:

Smell Test Truth-State View-State
Reload test Persists after reload Resets after reload
Multi-client test Other clients must see it Local to one client
Server test Originates from server Never leaves browser
Tab test Shared across tabs Independent per tab

Examples:

✅ Todo items
✅ User profile
✅ Order status
✅ Inventory count
✅ Permissions

✅ Which menu is open
✅ Selected tab
✅ Form draft (before submit)
✅ Scroll position
✅ Filter UI state

Rule of thumb: If a coworker on another device needs to see it, it's truth-state.

See: Truth-State vs View-State


Why does the server author directives?

Question

Can't the client just figure out what to invalidate after a mutation?

Answer:

The server has domain knowledge the client doesn't:

Example: Archive a project

@app.put('/api/projects/<int:id>/archive')
def archive_project(id):
    project = Project.query.get(id)
    project.archived = True

    # Server knows cascading effects:
    affected_tasks = []
    for task in project.tasks:
        task.archived = True
        affected_tasks.append(task.id)

    # Server tells client exactly what changed:
    return {
        'directives': [
            { 'op': 'refresh_item', 'name': 'project', 'id': id },
            { 'op': 'refresh_collection', 'name': 'projects' },
            *[{ 'op': 'refresh_item', 'name': 'task', 'id': tid } 
              for tid in affected_tasks],
            { 'op': 'refresh_collection', 'name': 'tasks' }
        ]
    }

If the client guessed: - It wouldn't know about cascading task updates - It might forget to refresh affected collections - Logic would be duplicated in every client

Server-authored directives: - ✅ Centralize invalidation logic - ✅ Ensure all clients stay consistent - ✅ Handle complex domain relationships - ✅ Allow server-side changes without client updates

See: Directives Concept


Comparison to Alternatives

How is Verity different from TanStack Query?

Question

TanStack Query has caching, refetching, and invalidation. What does Verity add?

Key Differences:

Aspect TanStack Query Verity
Invalidation Client-defined (manual invalidateQueries) Server-authored (directives)
Philosophy Optimistic updates encouraged Server truth only
Multi-client sync Not built-in (need external solution) Built-in (SSE fan-out)
Levels Not supported First-class (with conversions)
Push updates Not built-in Built-in (SSE directives)

Example: Delete a todo

const mutation = useMutation({
  mutationFn: (id) => fetch(`/api/todos/${id}`, { method: 'DELETE' }),
  onSuccess: (data, id) => {
    // Client must know what to invalidate:
    queryClient.invalidateQueries({ queryKey: ['todos'] })
    queryClient.invalidateQueries({ queryKey: ['todo', id] })
    queryClient.invalidateQueries({ queryKey: ['todoCount'] })
    // What if we forget one? 🤷
  }
})
async function deleteTodo(id) {
  const res = await fetch(`/api/todos/${id}`, { method: 'DELETE' })
  const { directives } = await res.json()
  // Server tells us exactly what changed:
  await registry.applyDirectives(directives)
}

Server returns:

{
  "directives": [
    { "op": "refresh_collection", "name": "todos" },
    { "op": "refresh_collection", "name": "todoCount" }
  ]
}

When to use TanStack Query: - Optimistic updates are critical to your UX - You prefer client-side invalidation control - No multi-client sync needed

When to use Verity: - Server truth is paramount - Multi-client/multi-tab sync needed - Prefer server-authored invalidation contract

See: Why Verity?


How is Verity different from htmx?

Question

htmx also keeps the server as the source of truth. How is Verity different?

Key Differences:

Aspect htmx Verity
Transport Server sends HTML Server sends data
View coupling Server dictates DOM structure Server sends semantic directives
Framework support Mostly vanilla HTML Alpine, React, Vue, Svelte
Client state Mostly server-driven Clear truth/view separation
Caching Browser HTTP cache Smart in-memory cache

Example: Update a todo

<!-- Server returns HTML fragment -->
<div hx-put="/api/todos/42" 
     hx-target="#todo-42"
     hx-swap="outerHTML">
  <input type="checkbox" checked>
  <span>Buy milk</span>
</div>

Server must return:

<div id="todo-42">
  <input type="checkbox" checked>
  <span>Buy milk - DONE</span>
</div>

// Server returns directive
await fetch('/api/todos/42', { method: 'PUT', ... })
// → { directives: [{ op: "refresh_item", name: "todo", id: 42 }] }

// Client renders however it wants:
<div x-data="{ todo: $verity.item('todo', 42) }">
  <input type="checkbox" :checked="todo.state.value.completed">
  <span x-text="todo.state.value.title"></span>
</div>

htmx strengths: - Perfect for server-rendered apps - Minimal JavaScript - Progressive enhancement

Verity strengths: - Decouples server from view structure - Supports rich client-side frameworks - Multi-client sync built-in

See: Migration from htmx | Why Verity?


Can I use Verity with GraphQL?

Question

I'm using GraphQL. Does Verity work with it?

Answer: Yes! Verity is protocol-agnostic.

Implementation:

registry.registerCollection('todos', {
  fetch: async (params) => {
    const query = `
      query GetTodos($status: String) {
        todos(status: $status) {
          id
          title
          completed
        }
      }
    `

    const res = await fetch('/graphql', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query,
        variables: { status: params.status }
      })
    })

    const data = await res.json()
    return { items: data.data.todos }
  }
})

Directives over GraphQL subscriptions:

registry.configureDirectiveSource({
  type: 'graphql-subscription',
  connect: ({ applyDirectives, clientId }) => {
    const subscription = graphqlClient.subscribe({
      query: `
        subscription {
          directives {
            op
            name
            id
            params
            source
          }
        }
      `
    })

    subscription.subscribe({
      next: ({ data }) => {
        const directives = data.directives
        if (directives.source !== clientId) {
          applyDirectives([directives])
        }
      }
    })

    return () => subscription.unsubscribe()
  }
})

Usage & Patterns

How do I handle pagination?

Question

Should each page be a separate collection, or one collection with page params?

Answer: Use params for pagination:

registry.registerCollection('todos', {
  fetch: async (params = {}) => {
    const url = new URL('/api/todos', window.location.origin)
    url.searchParams.set('page', params.page || 1)
    url.searchParams.set('limit', params.limit || 20)

    const res = await fetch(url)
    const data = await res.json()

    return {
      items: data.todos,
      meta: {
        page: data.page,
        totalPages: data.totalPages,
        total: data.total
      }
    }
  }
})

// Usage
const page1 = registry.collection('todos', { page: 1 })
const page2 = registry.collection('todos', { page: 2 })

Each page is cached separately. When directives arrive:

{
  "op": "refresh_collection",
  "name": "todos"
  // No params → refreshes ALL pages
}

Or target specific page:

{
  "op": "refresh_collection",
  "name": "todos",
  "params": { "page": 1 }
  // Only refreshes page 1
}

How do I handle infinite scroll / load more?

Question

Should I append to the existing collection or create new ones?

Answer: Two approaches:

const todos = ref([])
const currentPage = ref(1)

async function loadMore() {
  const handle = registry.collection('todos', { 
    page: currentPage.value 
  })

  await handle.state.loading
  todos.value.push(...handle.state.items)
  currentPage.value++
}

Caveat: Directives won't update appended items. Need manual refresh logic.

registry.registerCollection('todos', {
  fetch: async (params = {}) => {
    const url = new URL('/api/todos', window.location.origin)
    if (params.cursor) url.searchParams.set('cursor', params.cursor)

    const res = await fetch(url)
    const data = await res.json()

    return {
      items: data.todos,
      meta: {
        nextCursor: data.nextCursor,
        hasMore: data.hasMore
      }
    }
  }
})

// Each cursor is a separate collection
const batch1 = registry.collection('todos', { cursor: null })
const batch2 = registry.collection('todos', { cursor: 'abc123' })

Recommendation: Use cursor-based approach for proper directive handling.


Should I use one registry or multiple?

Question

Can I create multiple registries? When would I do that?

Answer: One registry per app is typical.

Multiple registries only if: - Different SSE audiences (e.g., admin panel vs user dashboard) - Isolated module boundaries (micro-frontends) - Different staleness/caching policies

Example: Multi-tenant app

// User-specific registry
const userRegistry = createRegistry({
  sse: { audience: `user-${userId}` }
})

// Admin-specific registry
const adminRegistry = createRegistry({
  sse: { audience: 'admin' },
  memory: { stalenessMs: 5000 }  // More aggressive for admin
})

Most apps: One shared registry is simpler and sufficient.


How do I handle authentication?

Question

Where do I include auth tokens?

Answer: In your fetch functions:

registry.registerCollection('todos', {
  fetch: async (params) => {
    const res = await fetch('/api/todos', {
      credentials: 'include'  // ← Send cookies
    })
    return res.json()
  }
})

// SSE with credentials
createRegistry({
  sse: {
    url: '/api/events',
    withCredentials: true  // ← Send cookies
  }
})
const getAuthToken = () => localStorage.getItem('auth_token')

registry.registerCollection('todos', {
  fetch: async (params) => {
    const res = await fetch('/api/todos', {
      headers: {
        'Authorization': `Bearer ${getAuthToken()}`
      }
    })
    return res.json()
  }
})
registry.registerCollection('todos', {
  fetch: async (params) => {
    const res = await fetch('/api/todos', {
      headers: {
        'X-API-Key': window.API_KEY,
        'X-User-ID': window.currentUserId
      }
    })
    return res.json()
  }
})

Verity doesn't manage auth—it's just a data layer. Use your existing auth strategy.


Debugging & Troubleshooting

My directives aren't being applied. Why?

Question

I'm emitting directives from the server but the client isn't updating.

Debug checklist:

  1. Check directive format:
    {
      "op": "refresh_collection",  // ✅ Correct
      "name": "todos"
    }
    

Not:

{
  "type": "refresh",  // ❌ Wrong field name
  "collection": "todos"
}

  1. Check if directive is returned:

    const res = await fetch('/api/todos', { method: 'POST', ... })
    const payload = await res.json()
    console.log(payload.directives)  // Should be an array
    

  2. Check if directive is applied:

    await registry.applyDirectives(payload.directives)
    // Did you forget to call this?
    

  3. Check SSE connection (for push path):

  4. Open devtools: Ctrl+Shift+D
  5. Go to SSE panel
  6. Check connection status (should be green ✓)
  7. Check if messages are arriving

  8. Check source field:

  9. Directives with source matching clientId are ignored
  10. This prevents double-application
  11. Make sure server includes X-Verity-Client-ID header in SSE

  12. Check check function:

    registry.registerCollection('todos', {
      fetch: /* ... */,
      check: (directive) => {
        console.log('Check function called:', directive)
        return true  // Make sure this returns true
      }
    })
    

See: Devtools Debugging


Data looks stale. How do I force refresh?

Question

The cache shows old data. How do I refresh it?

Manual refresh:

// Refresh a collection
const todos = registry.collection('todos', { status: 'active' })
await todos.refresh()

// Refresh an item
const user = registry.item('user', { userId: 123 })
await user.refresh()

Check staleness settings:

// Make data refresh more aggressively
registry.registerCollection('todos', {
  fetch: /* ... */,
  stalenessMs: 10000  // 10 seconds instead of default 60 seconds
})

Force resync (all collections):

await registry.resync()

Use devtools to inspect: - Open devtools: Ctrl+Shift+D - Go to Truth panel - Check stale indicator (yellow dot = stale)

See: State Model


How do I debug SSE connection issues?

Question

SSE isn't connecting or keeps disconnecting.

Debug steps:

  1. Check SSE endpoint:

    fetch('/api/events')  // Should return text/event-stream
    

  2. Check CORS (if cross-origin):

    # Server must allow SSE
    @app.route('/api/events')
    def events():
        def generate():
            while True:
                yield f"data: {json.dumps({...})}\n\n"
    
        response = Response(generate(), mimetype='text/event-stream')
        response.headers['Access-Control-Allow-Origin'] = 'https://example.com'
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        return response
    

  3. Check authentication:

    createRegistry({
      sse: {
        url: '/api/events',
        withCredentials: true  // ← Include cookies
      }
    })
    

  4. Use devtools:

  5. Open devtools: Ctrl+Shift+D
  6. Go to SSE panel
  7. Check connection status
  8. Check error messages
  9. Check reconnection attempts

  10. Check browser console: Look for SSE-related errors

See: Devtools SSE Panel


Performance & Optimization

How do I reduce unnecessary fetches?

Question

Verity is making too many requests. How do I optimize?

Strategies:

registry.registerCollection('todos', {
  fetch: /* ... */,
  stalenessMs: 300000  // 5 minutes instead of 1 minute
})
// Fetch full level once, derive others
registry.registerConversion('todo', 'full', 'expanded', (full) => {
  const { comments, ...expanded } = full
  return expanded
})

registry.registerConversion('todo', 'expanded', 'simplified', (expanded) => {
  const { description, assignee, ...simplified } = expanded
  return simplified
})
# Server includes updated data
return {
    'directives': [{
        'op': 'refresh_item',
        'name': 'todo',
        'id': 42,
        'result': todo.to_dict()  # ← Skip refetch
    }]
}
createRegistry({
  bulk: { delayMs: 200 }  // Wait 200ms to batch requests
})
# Instead of:
{ 'op': 'refresh_collection', 'name': 'todos' }
# (refreshes ALL todo collections)

# Use:
{ 'op': 'refresh_collection', 'name': 'todos', 'params': { 'status': 'active' } }
# (only refreshes that specific collection)

See: Levels & Conversions


Does Verity work with large datasets?

Question

I have thousands of records. Will Verity handle it?

Answer: Yes, with proper pagination and cache limits.

Best practices:

  1. Use pagination:

    registry.registerCollection('todos', {
      fetch: async (params = {}) => {
        const limit = params.limit || 50  // Fetch in chunks
        const page = params.page || 1
        // ... fetch only one page
      }
    })
    

  2. Set cache limits:

    createRegistry({
      memory: {
        maxItemsPerType: 1000  // Limit cached items
      }
    })
    

  3. Use virtual scrolling in UI

  4. Consider server-side search/filtering

Verity doesn't load everything into memory—only what you explicitly fetch and cache.


Production & Deployment

Should I remove devtools in production?

Question

Are devtools safe to leave in production?

Answer: No, remove them.

Reasons: - Adds ~50KB to bundle - Exposes internal state - No benefit to end users

How to remove:

if (process.env.NODE_ENV === 'development') {
  import('verity-dl/devtools/devtools.css')
  import('verity-dl/devtools/devtools.js')
}
if (window.location.hostname === 'localhost') {
  // Load devtools
}
// vite.config.js
export default {
  define: {
    __DEV__: JSON.stringify(process.env.NODE_ENV === 'development')
  }
}

// app.js
if (__DEV__) {
  // Load devtools
}

See: Devtools Production Usage


What's the browser support?

Question

Which browsers does Verity support?

Answer: Modern evergreen browsers.

Core requirements: - fetch API - Promise / async/await - EventSource (for SSE) - ES2018+ features

Supported: - ✅ Chrome 63+ - ✅ Firefox 58+ - ✅ Safari 12+ - ✅ Edge 79+

Not supported: - ❌ IE11 (no native Promise, fetch, or EventSource)

Polyfills: If you need older browser support, include polyfills for fetch, Promise, and EventSource.


Contributing & Community

How can I contribute?

Question

I want to contribute. Where do I start?

Answer: Check out CONTRIBUTING.md!

Areas to contribute: - 📚 Documentation improvements - 🐛 Bug fixes - ✨ Feature requests - 🧪 Test coverage - 📝 Example applications - 🌍 Translations

Process: 1. Open an issue to discuss 2. Fork and create a branch 3. Make your changes 4. Add tests if applicable 5. Submit a pull request


Where can I get help?

Question

I'm stuck. Where can I ask questions?

Resources: - 📖 Documentation - 💬 GitHub Discussions - 🐛 GitHub Issues - 📧 Email: yididev@gmail.com


Still Have Questions?

If your question isn't answered here, please:

  1. Check the complete documentation
  2. Search GitHub Issues
  3. Ask in GitHub Discussions
  4. Open a new issue if you found a bug

We're here to help! 🙌