Getting Started

Coming from PayloadCMS

If you have used PayloadCMS, many Vextro concepts will feel familiar. Both frameworks generate admin UIs from schema definitions. The main differences come from the backend -- Vextro runs on Convex instead of MongoDB/Postgres, and this changes how data flows, how hooks work, and how you query content.

Concept mapping

PayloadCMSVextroNotes
Collection configdefineVextroCollection()Same idea: define fields and get an admin UI
Global configdefineVextroGlobal()Singleton documents, same concept
Field configs (array of objects)f field builders (object keys)Vextro uses { name: f.text() } instead of [{ name: "title", type: "text" }]
type: "text"f.text()Type-safe builder instead of string type
type: "richText" (Lexical/Slate)f.richText() (TipTap)Different editor, same purpose
type: "relationship"f.id("tableName")References a Convex document ID
type: "upload"f.upload() / f.image()Built-in storage adapters
type: "blocks"f.blocks()Flexible content composition
type: "tabs" (layout)f.tabs()Layout-only, no data storage
type: "row" (layout)f.row()Side-by-side field layout
type: "group"f.group()Nested object
type: "array"f.array()Repeating field groups
Hooks (beforeChange, etc.)Hooks (beforeChange, etc.)Same names, different execution model
Access control functionsPermission strings + role arraysSimpler but less flexible
Local API (payload.find())Convex queries (ctx.db.query())Direct database access
REST API (/api/posts)No REST -- call Convex functions directlyType-safe function calls
GraphQL APINot applicableConvex has no GraphQL layer
depth parameterExplicit relationship resolutionYou choose what to populate
PluginsPlugins (planned)Extensibility system
Admin panel (React)Admin panel (Astro + Svelte)Different frontend stack
payload.config.tscreateVextroClient() + collection filesConfiguration is more distributed
Express middlewareConvex function context (ctx)No HTTP server to configure
MongoDB/PostgresConvex databaseReal-time, transactional, hosted

Key differences

No REST or GraphQL

PayloadCMS auto-generates REST and GraphQL APIs from your collections. Vextro does not -- because Convex does not need them. Your frontend calls Convex functions directly with full type safety. There are no endpoints to configure, no authentication middleware to write, and no API versioning to manage.

// PayloadCMS: HTTP request
const response = await fetch("/api/posts?where[status][equals]=published");
const { docs } = await response.json();

// Vextro/Convex: direct function call
const posts = await convex.query(api.public.listPublishedPosts);

See Querying Data for framework-specific examples.

Real-time subscriptions

PayloadCMS data is fetched on demand. To see updates, you poll or use webhooks. With Convex, every query is a live subscription. When data changes, all connected clients update automatically. No WebSocket setup required.

Atomic transactions

PayloadCMS hooks can encounter partial failures -- a beforeChange might succeed but the database write fails, or an afterChange throws after data is already saved. Convex mutations are atomic transactions: if anything throws, everything rolls back. This is simpler to reason about.

Permission strings vs access functions

PayloadCMS access control uses functions that can return boolean or a query filter:

// PayloadCMS
access: {
  read: ({ req }) => {
    if (req.user.role === "admin") return true;
    return { status: { equals: "published" } };
  },
}

Vextro uses permission strings matched against user roles:

// Vextro
access: {
  read: "view:articles",
  update: "edit:articles",
}

For row-level filtering (like "only show published posts to non-admins"), use scopes. See Access Control for the full API.

Object-key fields vs arrays

PayloadCMS defines fields as an array of objects with name properties. Vextro defines fields as an object with named keys:

// PayloadCMS
fields: [
  { name: "title", type: "text", required: true },
  { name: "slug", type: "text" },
]

// Vextro
fields: {
  title: f.text({ required: true }),
  slug: f.slug({ sourceField: "title" }),
}

The Vextro approach provides better TypeScript inference and eliminates the possibility of duplicate field names.

Missing hooks and why

If you are looking for certain PayloadCMS hooks and cannot find them in Vextro, here is why:

afterError

PayloadCMS has afterError because operations can fail mid-flight, leaving partial state. Convex mutations are atomic transactions -- if anything throws, everything rolls back. There is no partial state to clean up, so there is no need for an error recovery hook.

beforeValidate

PayloadCMS runs beforeValidate before field validation. Convex validates function arguments at the platform level before any application code runs. The f field builders generate these validators automatically. By the time your beforeChange hook executes, arguments are already validated.

beforeOperation / afterOperation

In PayloadCMS, these wrap the entire HTTP request lifecycle. Convex functions are not HTTP handlers -- they are atomic units of work. The function itself is the operation. Use beforeChange for pre-write logic and afterChange for post-write logic.

Auth hooks (afterLogin, afterLogout, afterRefresh, etc.)

PayloadCMS bundles its own auth system with these hooks. Vextro delegates authentication to Better Auth, which has its own hook and event system. Auth lifecycle hooks live in Better Auth's configuration, not in Vextro.

See the Hooks page for the complete list of available hooks and their execution model.

Migration path

1. Map your collections

For each PayloadCMS collection config, create a defineVextroCollection() call. Map field types using the table above. The f builders cover all common PayloadCMS field types.

2. Define your schema

Use the collection .table property to generate Convex table definitions. Add them to your convex/schema.ts. Run npx convex dev to create the tables.

3. Move your hooks

PayloadCMS hooks translate almost directly:

PayloadCMS hookVextro hook
beforeChangebeforeChange (collection or field)
afterChangeafterChange (collection or field)
beforeDeletebeforeDelete
afterDeleteafterDelete
beforeReadbeforeRead
afterReadafterRead (collection or field)
beforeValidateNot needed (Convex validates automatically)
afterErrorNot needed (atomic transactions)

The main difference: Vextro hooks receive a Convex ctx instead of a PayloadCMS req. Use ctx.db for database access and ctx.scheduler for background jobs.

4. Write frontend queries

Replace payload.find() / REST calls with Convex query functions. See Querying Data for examples in Astro, Svelte, and Next.js.

5. Set up access control

Replace PayloadCMS access functions with Vextro's permission strings and getUserRoles callback. See Access Control.

6. Migrate data

Use Vextro's Import & Export feature to load existing content via CSV or JSON. For large datasets, write a Convex action that reads from your old database and inserts into Convex.

Next steps

  • Key Concepts -- understand the Convex execution model
  • Quick Start -- build your first Vextro collection
  • Hooks -- full hook reference with execution order diagrams
  • Access Control -- configure permissions and scopes
Previous
Quick Start