Features
Live Editing
Overview
Vextro's editor is built for real-time collaboration. Every document editor and global editor maintains a live Convex WebSocket subscription. When another user saves a change, non-dirty fields update automatically. When two users edit the same field simultaneously, a conflict modal guides resolution. All of this works transparently -- no extra configuration is required to enable the core sync behavior.
The same architecture applies to both collection document editors and global editors.
Real-time sync matters most when multiple editors work on the same content simultaneously: without it, one editor's save silently overwrites another's, creating data loss that's hard to detect and harder to recover from. By propagating changes over a persistent WebSocket connection, Vextro keeps every open editor in sync and surfaces conflicts explicitly rather than letting them happen invisibly.
Live Sync
The useLiveSync composable drives field-level synchronization. It subscribes to the server document via client.onUpdate and calls formState.applySyncUpdate on every push.
What gets updated
When a server push arrives:
- Fields the current user has not modified are updated immediately.
- Fields the current user has modified (dirty fields) are left untouched.
- If the incoming
updatedAttimestamp is not newer than the one already stored locally, the update is skipped entirely. - Updates are skipped while a save is in flight (
saveStatus === 'saving').
The syncVersion counter on FormState increments when field values change due to sync. Components can use this as a {#key} value to remount inputs that cannot handle external value changes (e.g. rich text editors).
Only fields present in the server document are applied. Absent fields are never synthesized with empty defaults -- this prevents overwriting in-progress values such as block arrays that have not yet been persisted.
Conflict tracking during sync
If the server updates a field that the current user has dirty, applySyncUpdate records that a conflict exists but does not advance the local serverUpdatedAt timestamp. This means the next save attempt will detect the conflict via optimistic locking and surface the conflict modal.
Auto-Save
Auto-save fires after a debounce period whenever the form has dirty fields. It is disabled by default and must be opted into.
Collection-level config
// convex/collections/posts.ts
import { defineVextroCollection } from 'vextro';
export const posts = defineVextroCollection({
slug: 'posts',
// ...
autoSave: {
enabled: true,
debounceMs: 2000, // default: 3000
},
}); You can also pass autoSave: true to use the project-level defaults:
autoSave: true, Or disable it explicitly even if a project-level default is set:
autoSave: false, Project-level config
Set project-wide defaults in your VextroConfig. Per-collection config overrides these values.
// convex/vextro.config.ts
import { defineVextroConfig } from 'vextro';
export default defineVextroConfig({
// ...
autoSave: {
enabled: true,
debounceMs: 3000,
},
}); Auto-save options
| Option | Description |
|---|---|
enabled | Turn auto-save on or off. Inherits from project config when not set. |
debounceMs | Milliseconds to wait after the last field change before saving. Default: 3000. |
When to enable auto-save: Collaborative workflows and long-form content benefit most — browser crashes or accidental tab closes can wipe minutes of work without it.
When to keep it disabled: Collections where saves trigger expensive side effects (webhooks, image processing pipelines, third-party syncs) may incur significant cost or latency from frequent auto-saves. Disable it for those collections and let editors save manually.
Tuning debounceMs: Lower values (1000–2000 ms) reduce data loss risk in fast-paced collaborative sessions at the cost of more Convex mutations. Higher values (5000 ms or more) cut server load for single-editor, long-form writing where saving every keystroke adds no value. The default of 3000 ms is a reasonable middle ground for most cases.
Auto-save does not fire for new (unsaved) documents. The user must perform the initial save manually so the document gets an ID.
Save States
The FormState tracks one of six possible save statuses at all times:
| Status | Meaning |
|---|---|
clean | No changes since last save (or initial load). |
dirty | At least one field has been modified. |
saving | A save mutation is in flight. |
saved | Save succeeded. Reverts to clean after ~2 seconds. |
error | Save failed. The saveError field contains a message, and per-field errors may be set. |
conflict | Optimistic locking detected a concurrent modification. The conflict modal is shown. |
The editor header and sidebar reflect these states visually. The save button is disabled while saving is active.
Conflict Detection & Resolution
Vextro uses optimistic locking to detect concurrent edits. Every update mutation sends the expectedUpdatedAt timestamp captured when the document was last loaded or saved. If the server's current updatedAt does not match, the mutation returns { status: 'conflict', currentDocument: { ... } } instead of applying the change.
Optimistic locking was chosen over pessimistic locking deliberately: no editor is ever blocked from opening or editing a document while someone else has it open. In practice, two editors modifying the exact same field within the same save window is rare — optimistic locking handles that rare case explicitly without degrading the common case where concurrent edits touch different fields.
What happens on conflict
formState.saveStatusis set to'conflict'.- A warning toast appears: "Another user has modified this document."
- The
ConflictModalopens with a field-level diff showing:- Server version -- what the server currently has
- Your version -- what you were about to save
- Base version -- what both users started from
Resolution options
Reload server version -- Discards all local changes and replaces the form with the server document. Safe choice when you do not need to keep your edits.
Force save -- Overwrites the server document with your local dirty fields, bypassing expectedUpdatedAt. Use this when you are confident your changes should win.
Force-saving permanently overwrites whatever the other user saved. Coordinate with collaborators before using this option.
Live Document List
VextroLiveDocumentList replaces the static SSR list on collection index pages. It mounts with the server-rendered HTML (no blank flash) and then subscribes to the same Convex query via WebSocket. This means editors see new content from teammates the moment it's published, and deleted documents disappear immediately — eliminating the confusion of stale list data that leads to duplicate work or editing already-deleted records.
Behavior
- New documents slide in with a non-intrusive banner so editors notice without disrupting their workflow.
- Deleted documents fade out rather than disappearing abruptly.
- Updated documents briefly highlight to draw attention to the change.
This behavior is automatic. No configuration is needed beyond having the collection defined.
Live Review Queue
VextroLiveReviewQueue follows the same pattern as the document list. It mounts with SSR data and subscribes to live updates.
- New items slide in with a highlight.
- Stage and approval changes update in-place without a page reload.
This component activates automatically when a collection has workflow configured. See Workflow & Review Queues for setup details.
Navigation Guard
The useNavigationGuard composable prevents accidental data loss when navigating away with unsaved changes. It is active in all document and global editors.
How it works
- Astro view transitions -- Listens to the
astro:before-preparationevent and callswindow.confirmbefore the transition proceeds. - Standard navigation / tab close -- Listens to
beforeunloadand setse.returnValueto trigger the browser's built-in confirmation dialog.
If the form is clean, neither handler fires.
Custom confirmation message
The composable accepts an optional confirmMessage string. The default is:
"You have unsaved changes. Are you sure you want to leave?"
Field Presence
When the Vextro Convex component is configured with a presence component, field-level presence indicators appear in the editor. Each active field shows avatar badges for other editors currently focused on that field.
Setup requires the presence schema, mutations, and config in the Convex component. Refer to the Convex Component documentation for the full setup steps.
Field presence requires the vextro_field_presence table and the fieldPresence configuration block in the Convex component. It is opt-in and has no effect if not configured.