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

OptionTypeDefaultDescription
slug*string--URL-safe identifier for the global
label*string--Display label in admin UI
descriptionstring--Description shown in admin UI
groupstring--Sidebar group heading
tableName*string--Convex table name for storage
fields*VextroFieldsInput--Field definitions using f builders
accessVextroGlobalAccessConfig--Permission strings for read/update operations
hooksGlobalHooks--Global-level hooks (beforeChange, afterChange, beforeRead, afterRead)
sidebarConfigVextroGlobalSidebarConfig--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+S to save, Escape to 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.

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",
  },
});
PermissionControls
readWho can view the global document in the admin UI and via queries
updateWho 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

HookFiresCan modify data?
beforeChangeBefore update writeYes -- return modified data
afterChangeAfter update writeYes -- return a patch
beforeReadBefore locale mergeYes -- return modified doc
afterReadAfter field hooksYes -- 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",
              });
            }
          },
        ],
      },
    }),
  },
});
HookFiresReturn
beforeChangeBefore the field value is writtenModified value or undefined to keep original
afterChangeAfter the field value is writtenvoid
afterReadAfter the field value is readModified value or undefined to keep original
beforeDuplicateBefore field value is copied during duplicationModified 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

AspectCollectionsGlobals
Document countMany documents per collectionOne document per global
List viewFull list with filtering, sorting, searchNo list view
Status workflowDraft/published/scheduled/trashed (content type)No status workflow
Sidebar displayGrouped under collection groupsGrouped under "Globals" heading
URL pattern/collections/{slug} then /collections/{slug}/{id}/globals/{slug}
HooksbeforeChange, afterChange, beforeDelete, afterDelete, beforeRead, afterReadbeforeChange, afterChange, beforeRead, afterRead (no delete hooks)
VersioningConfigurable per collectionNot currently supported
EditorReactive Svelte island with live sync, auto-save, conflict detectionSame (shared composables)
Previous
Collections