Globals
VexCMS supports globals for content that exists exactly once per project — site settings, primary navigation, a footer — as distinct from collections, which hold many documents. A global is a single flat document reachable by its slug, with its own admin edit page but no list view and no relationship-picker target.
How it works
Section titled “How it works”defineGlobal()builds aGlobalConfig, registered viadefineConfig({ globals: [...] })alongsidecollections.- Every global lives in one shared
vex_globalstable, discriminated by_slug. The API layer flattens the DB row into a flat document on read (getGlobal/findGlobals) and re-nests it on write (updateGlobal) — sositeSettings.siteNamereads as a root-level property alongside the system fields_id,_creationTime,_slug. _id,_creationTime, and_slugare reserved field keys —defineGlobalrejects them both at compile time and at runtime.- Globals accept the same field types as collections (
text,number,checkbox,select,date,url,relationship,upload,array,group,blocks), but can’t be a relationship target — there’s no picker UI for a singleton — and have nouseAsTitle; the admin page title is alwayslabel. - The admin panel auto-renders an edit view for every registered global. No extra wiring
is needed to make a global editable under
/admin.
import { defineConfig, defineGlobal, text, checkbox, relationship } from "@vexcms/core";import { themes } from "./collections/themes";
export const siteSettings = defineGlobal({ slug: "siteSettings", label: "Site Settings", fields: { siteName: text({ label: "Site Name", required: true }), maintenanceMode: checkbox({ label: "Maintenance Mode" }), activeTheme: relationship({ label: "Active Theme", collection: { slug: "themes" } }), }, admin: { group: "Site Builder", icon: "Settings" },});
export default defineConfig({ collections: [themes], globals: [siteSettings],});slug is the lookup key in vex_globals and must be unique across every registered
global. admin.group puts the global under a labeled section in the sidebar; omit it to
leave the global ungrouped.
Querying a global
Section titled “Querying a global”Client components use the same tanstack-query wrappers as collections, imported from
@vexcms/react/client:
"use client";
import { useQuery } from "@tanstack/react-query";import { getGlobal } from "@vexcms/react/client";
export function SiteHeader() { const { data } = useQuery(getGlobal({ slug: "siteSettings" })); if (!data) return null; return <span>{data.siteName}</span>;}Populate a relationship field the same way find/get do:
const { data } = useQuery( getGlobal({ slug: "siteSettings", populate: { activeTheme: true } }),);data?.activeTheme; // resolved theme document | undefinedList every saved global at once — the result is un-narrowed, since each row’s _slug
picks a different shape:
import { findGlobals } from "@vexcms/react/client";
const { data } = useQuery(findGlobals());data?.map((g) => g._slug); // e.g. ["siteSettings", "nav"]On the server — a Convex query/mutation handler, or an RSC loader via fetchQuery — import
from @vexcms/core/server instead, which takes ctx and the resolved config rather than
returning tanstack-query options:
import { query } from "./_generated/server";import { getGlobal } from "@vexcms/core/server";import config from "../vex.config.server";
export const layoutData = query({ handler: async (ctx) => { const settings = await getGlobal({ ctx, config, slug: "siteSettings" }); return { siteName: settings?.siteName }; },});Editing a global
Section titled “Editing a global”Reach for useUpdateGlobal when building a custom editing UI:
"use client";
import { useMutation } from "@tanstack/react-query";import { useUpdateGlobal } from "@vexcms/react/client";
function SiteNameForm() { const updateGlobalMutationFn = useUpdateGlobal(); const mutation = useMutation({ mutationFn: updateGlobalMutationFn });
return ( <button onClick={() => mutation.mutate({ slug: "siteSettings", data: { siteName: "New Name" } })} > Save </button> );}Pass only user field values in data — system keys (_id, _creationTime, _slug) are
stripped server-side if they end up in there.
Access control
Section titled “Access control”When defineAccess is configured, updateGlobal/globals.upsert authorizes writes against
create or update — never read — because a global is a singleton and the verb depends
on whether it has ever been saved: the first write for a slug authorizes as create, every
later one as update. A role that can read a global can no longer write it just from that
grant; create and/or update must be granted explicitly. access.action overrides this
the same way it does everywhere else in the Local API.
The write also merges the payload into the stored document rather than replacing it: an
omitted field is left untouched, not deleted. A save carrying only { siteName: "New Name" }
leaves every other field on siteSettings exactly as it was — the merged result is what gets
validated against the global’s Zod schema, so a partial payload is still rejected if the
document it produces would be invalid.
Field-level permissions apply the same way they do to a collection: an update filter
callback that returns a field map denies on the first changed key it forbids, and a field
resent with its unchanged value is not a violation. See the
Access Control guide for the map shapes
and the apps/www siteSettings.adminTheme worked example.
A structured global: navigation
Section titled “A structured global: navigation”array and group compose on a global exactly the way they do on a collection. This is
the real nav global from the marketing site — an editable, reorderable list of nav links:
import { array, defineGlobal, group, text } from "@vexcms/core";
export const nav = defineGlobal({ slug: "nav", label: "Nav", admin: { icon: "TableProperties" }, fields: { items: array({ items: group({ fields: { title: text(), href: text(), }, }), }), },});Gotchas
Section titled “Gotchas”- Reserved keys.
_id,_creationTime,_slugcan’t be field keys — a compile error on the offending field, plus a thrownErrorat runtime for JS callers. - No relationship target. A global can’t be the
collectionof anotherrelationship()field — there’s no admin picker for a singleton. versions.draftsis parsed but not enforced yet.defineGlobal({ versions: { drafts: true } })type-checks and is stored on the resolved config, but draft/publish workflow for globals isn’t implemented — every read returns the live document regardless of this setting.interfaceNamedefaults fromslug+ aGlobalsuffix ("siteSettings"→"SiteSettingsGlobal") — passinterfaceNameexplicitly if you need a different generated TypeScript name.- Client and server globals helpers are separate entry points with the same names.
@vexcms/react/client(getGlobal,findGlobals,updateGlobal— tanstack-query based) vs.@vexcms/core/server(identical names,ctx-based, called from inside Convex functions). Import from whichever matches where the code runs — the client versions forbidctx, the server versions require it, so mixing them up is a type error. - A
readgrant no longer authorizes a write.updateGlobal/globals.upsertcheckscreate/update, notread— see Access control above. Upgrading from a version that predates this needs the role’s permissions updated explicitly.