Configuration
Globals
Overview
Globals represent singleton content -- data that exists exactly once in your application rather than as a list of documents. Site settings, navigation menus, footer configuration, and social media links are common examples. In the admin UI, globals appear in their own sidebar section and open directly into an editor without a list view.
Vextro stores global metadata in the adminGlobals table inside the Vextro Convex component. The actual data lives in your own Convex tables, one document per global.
Defining a global
Each global needs a slug, label, backing table name, and field definitions.
// convex/globals/siteSettings.ts
import { f } from "vextro";
export const siteSettingsFields = {
siteName: f.text({ required: true, label: "Site Name" }),
tagline: f.text({ placeholder: "Your company tagline" }),
logo: f.image({ relationTo: "media", label: "Site Logo" }),
favicon: f.image({ relationTo: "media", label: "Favicon" }),
socialLinks: f.array({
fields: {
platform: f.select({
options: ["twitter", "github", "linkedin", "youtube", "instagram"],
required: true,
}),
url: f.url({ required: true }),
label: f.text(),
},
rowLabel: "platform",
maxRows: 10,
}),
footerText: f.richText({ output: "html" }),
copyrightHolder: f.text({ required: true }),
maintenanceMode: f.checkbox({ label: "Enable maintenance mode" }),
}; One document per global
Unlike collections, globals do not have a list page. The admin UI links directly to the single document editor. If the document does not exist yet, Vextro creates it on first save.
Config options
| Option | Type | Default | Description |
|---|---|---|---|
slug* | string | -- | URL-safe identifier for the global |
label* | string | -- | Display label in admin UI |
description | string | -- | Description shown in admin UI |
group | string | -- | Sidebar group heading |
tableName* | string | -- | Convex table name for storage |
fields* | VextroFieldsInput | -- | Field definitions using f builders |
access | VextroGlobalAccessConfig | -- | Permission strings for read/update operations |
hooks | GlobalHooks | -- | Global-level hooks (beforeChange, afterChange, beforeRead, afterRead) |
sidebarConfig | VextroGlobalSidebarConfig | -- | Sidebar section order configuration |
Field-level hooks are defined inline on individual field builders using the hooks option (e.g., f.text({ hooks: { beforeChange: [...] } })), not as a top-level global option. They are extracted at build time and stored on the resolved definition's .fieldHooks property. See the Fields overview for details.
Registering globals with the admin module
Pass your global definitions to createVextroAdminModule alongside your collection definitions. Vextro automatically seeds and syncs them whenever the admin shell detects a change.
// convex/globals/index.ts
import { defineVextroGlobal } from "vextro";
export const siteSettings = defineVextroGlobal({
slug: "site-settings",
label: "Site Settings",
description: "Global site configuration",
tableName: "site_settings",
fields: siteSettingsFields,
});
export const navigation = defineVextroGlobal({
slug: "navigation",
label: "Navigation",
description: "Header and footer navigation menus",
tableName: "navigation",
fields: navigationFields,
}); // convex/admin.ts
const admin = createVextroAdminModule({
query,
mutation,
components,
collectionDefinitions,
globalDefinitions: [siteSettings, navigation],
}); Querying globals
The admin module exposes query functions for reading global metadata and documents.
// List all registered globals
const globals = await convex.query(api.admin.listGlobals, {
includeArchived: false,
});
// Get a specific global by slug
const settings = await convex.query(api.admin.getGlobalBySlug, {
slug: "site-settings",
});
// Get fields for a global
const fields = await convex.query(api.admin.listFieldsForGlobal, {
globalId: settings._id,
includeArchived: false,
}); Admin UI behavior
Globals appear in the sidebar under a "Globals" heading, separate from collections. Each global links directly to its editor page at /globals/{slug}.
The global editor is a reactive Svelte island that hydrates on the client. It provides the same editing experience as the collection document editor, including:
- Live sync — field values update in real time via a Convex WebSocket subscription. Only non-dirty fields are updated, so in-progress edits are never overwritten.
- Auto-save — changes are automatically saved after a short debounce period.
- Conflict detection — if another user modifies the document while you have unsaved changes, a conflict modal appears with options to force-save or reload.
- Navigation guard — the browser prompts for confirmation before navigating away with unsaved changes.
- Keyboard shortcuts —
Cmd/Ctrl+Sto save,Escapeto blur the active field.
Auto-save, live sync, conflict detection, and navigation guards work the same way for both globals and collections. See Live Editing for the full guide.
Fields support the same options as collection fields: conditional visibility, sidebar placement, groups, tabs, and validation.
Sidebar section order
You can override the default sidebar section order with sidebarConfig.sectionOrder.
defineVextroGlobal({
slug: "site-settings",
label: "Site Settings",
tableName: "site_settings",
fields: { /* ... */ },
sidebarConfig: {
sectionOrder: ["actions", "_default", "document"],
},
}); Built-in section IDs for globals are "_default", "document", and "actions". The "_default" section contains fields with sidebar: true but no explicit sidebarSection -- these render directly without a collapsible wrapper or heading. Custom sections from sidebarSection field options are also supported. Sections not listed in the array are appended at the end. See Collections - Sidebar section order for full details.
// Example: navigation global with tabs
export const navigationFields = {
...f.tabs({
tabs: [
{
label: "Header",
fields: {
headerLinks: f.array({
fields: {
label: f.text({ required: true }),
href: f.url({ required: true }),
openInNewTab: f.checkbox(),
},
rowLabel: "label",
}),
},
},
{
label: "Footer",
fields: {
footerColumns: f.array({
fields: {
heading: f.text({ required: true }),
links: f.array({
fields: {
label: f.text({ required: true }),
href: f.url({ required: true }),
},
rowLabel: "label",
}),
},
rowLabel: "heading",
}),
},
},
],
}),
}; Reading global data on the frontend
Since globals are stored in regular Convex tables, you query them from any app or framework that can talk to Convex -- Astro frontmatter, React components, Vue, Svelte, or a standalone Node service in a monorepo.
Framework agnostic
Vextro uses Astro for its admin UI because Astro is framework agnostic -- you can bring React, Vue, Svelte, Solid, or any supported framework as interactive islands. See the Astro integrations docs for setup instructions.
// Astro example: src/pages/index.astro
---
import { convex } from "@/lib/convex";
import { api } from "@your-app/convex/_generated/api";
const siteSettings = await convex.query(api.siteSettings.get, {});
const navigation = await convex.query(api.navigation.get, {});
---
<html>
<head>
<title>{siteSettings.siteName}</title>
</head>
<body>
<nav>
{navigation.headerLinks.map((link) => (
<a href={link.href}>{link.label}</a>
))}
</nav>
</body>
</html> The same queries work from any Convex client:
// Any JS/TS context with a Convex client
const siteSettings = await convex.query(api.siteSettings.get, {}); Cache invalidation
When a global is updated, Vextro can trigger cache invalidation using the scheduleCacheInvalidation callback. Use tags like global:site-settings to selectively purge cached SSR pages that depend on global data.
Access control
Permission strings in the access object are checked against the current user's permissions via vextro/auth guards. Globals support read and update operations (no create or delete since globals are singletons).
export const siteSettings = defineVextroGlobal({
slug: "site-settings",
label: "Site Settings",
tableName: "site_settings",
fields: { /* ... */ },
access: {
read: "cms:read",
update: "cms:admin",
},
}); | Permission | Controls |
|---|---|
read | Who can view the global document in the admin UI and via queries |
update | Who can modify the global document; users without this permission see a read-only editor |
Server-side enforcement
Access checks are enforced in Convex mutations and queries through the requireAdminRead and requireAdminWrite callbacks. Client-side permission gating is cosmetic only.
Hooks
Globals support the same hook system as collections. Define hooks on the global definition to run custom logic during reads and writes.
Available hooks
| Hook | Fires | Can modify data? |
|---|---|---|
beforeChange | Before update write | Yes -- return modified data |
afterChange | After update write | Yes -- return a patch |
beforeRead | Before locale merge | Yes -- return modified doc |
afterRead | After field hooks | Yes -- return modified doc |
Globals do not support beforeDelete or afterDelete since globals are singletons and cannot be deleted.
Example
import { defineVextroGlobal, f } from "vextro";
export const siteSettings = defineVextroGlobal({
slug: "site-settings",
label: "Site Settings",
tableName: "site_settings",
fields: {
siteName: f.text({ required: true }),
maintenanceMode: f.checkbox(),
},
hooks: {
beforeChange: [
({ data }) => {
return { ...data, updatedAt: Date.now() };
},
],
afterRead: [
({ doc }) => {
return { ...doc, isLive: !doc.maintenanceMode };
},
],
},
}); Field-level hooks
Field-level hooks are defined inline on individual field builders using the hooks option. They are extracted at build time and stored on the resolved definition's .fieldHooks property -- they are not a top-level defineVextroGlobal input option.
export const siteSettings = defineVextroGlobal({
slug: "site-settings",
label: "Site Settings",
tableName: "site_settings",
fields: {
siteName: f.text({ required: true }),
slug: f.slug({
from: "siteName",
hooks: {
beforeChange: [
({ value, data }) => {
// Auto-generate slug from siteName if empty
if (!value && data.siteName) {
return data.siteName.toLowerCase().replace(/\s+/g, "-");
}
return value;
},
],
},
}),
maintenanceMode: f.checkbox({
hooks: {
afterChange: [
async ({ value, previousValue, ctx }) => {
if (value && !previousValue) {
// Notify when maintenance mode is turned on
await ctx.runMutation(api.notifications.send, {
message: "Maintenance mode enabled",
});
}
},
],
},
}),
},
}); | Hook | Fires | Return |
|---|---|---|
beforeChange | Before the field value is written | Modified value or undefined to keep original |
afterChange | After the field value is written | void |
afterRead | After the field value is read | Modified value or undefined to keep original |
beforeDuplicate | Before field value is copied during duplication | Modified value or undefined to keep original |
Globals and beforeDuplicate
Since globals are singletons, beforeDuplicate field hooks are not invoked for globals. They are included in the type for shared field hook compatibility.
See the Fields overview for more details on inline field hooks.
See Hooks for full details on the hook execution pipeline.
Differences from collections
| Aspect | Collections | Globals |
|---|---|---|
| Document count | Many documents per collection | One document per global |
| List view | Full list with filtering, sorting, search | No list view |
| Status workflow | Draft/published/scheduled/trashed (content type) | No status workflow |
| Sidebar display | Grouped under collection groups | Grouped under "Globals" heading |
| URL pattern | /collections/{slug} then /collections/{slug}/{id} | /globals/{slug} |
| Hooks | beforeChange, afterChange, beforeDelete, afterDelete, beforeRead, afterRead | beforeChange, afterChange, beforeRead, afterRead (no delete hooks) |
| Versioning | Configurable per collection | Not currently supported |
| Editor | Reactive Svelte island with live sync, auto-save, conflict detection | Same (shared composables) |