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:
Include updated data in directives:
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? 🤷
}
})
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:
// 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:
Or target specific page:
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:
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:
- Check directive format:
Not:
-
Check if directive is returned:
-
Check if directive is applied:
-
Check SSE connection (for push path):
- Open devtools:
Ctrl+Shift+D - Go to SSE panel
- Check connection status (should be green ✓)
-
Check if messages are arriving
-
Check
sourcefield: - Directives with
sourcematchingclientIdare ignored - This prevents double-application
-
Make sure server includes
X-Verity-Client-IDheader in SSE -
Check
checkfunction:
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):
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:
-
Check SSE endpoint:
-
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 -
Check authentication:
-
Use devtools:
- Open devtools:
Ctrl+Shift+D - Go to SSE panel
- Check connection status
- Check error messages
-
Check reconnection attempts
-
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:
// 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
})
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:
-
Use pagination:
-
Set cache limits:
-
Use virtual scrolling in UI
-
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:
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:
- Check the complete documentation
- Search GitHub Issues
- Ask in GitHub Discussions
- Open a new issue if you found a bug
We're here to help! 🙌