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
| PayloadCMS | Vextro | Notes |
|---|---|---|
| Collection config | defineVextroCollection() | Same idea: define fields and get an admin UI |
| Global config | defineVextroGlobal() | 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 functions | Permission strings + role arrays | Simpler but less flexible |
Local API (payload.find()) | Convex queries (ctx.db.query()) | Direct database access |
REST API (/api/posts) | No REST -- call Convex functions directly | Type-safe function calls |
| GraphQL API | Not applicable | Convex has no GraphQL layer |
depth parameter | Explicit relationship resolution | You choose what to populate |
| Plugins | Plugins (planned) | Extensibility system |
| Admin panel (React) | Admin panel (Astro + Svelte) | Different frontend stack |
payload.config.ts | createVextroClient() + collection files | Configuration is more distributed |
| Express middleware | Convex function context (ctx) | No HTTP server to configure |
| MongoDB/Postgres | Convex database | Real-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 hook | Vextro hook |
|---|---|
beforeChange | beforeChange (collection or field) |
afterChange | afterChange (collection or field) |
beforeDelete | beforeDelete |
afterDelete | afterDelete |
beforeRead | beforeRead |
afterRead | afterRead (collection or field) |
beforeValidate | Not needed (Convex validates automatically) |
afterError | Not 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