Getting Started
Coming from Directus
If you have used Directus, Vextro's approach will feel both familiar and different. Both generate admin UIs from data definitions and let you manage arbitrary tables through a dashboard. The biggest difference: Directus wraps an existing SQL database, while Vextro is built natively on Convex -- a real-time, transactional backend that replaces your database, API layer, and WebSocket server.
Concept mapping
| Directus | Vextro | Notes |
|---|---|---|
| Data Studio | Vextro admin panel | Auto-generated CRUD UI from schema |
| Collections (auto-introspected) | defineVextroCollection() | Vextro defines collections in code; Directus introspects SQL tables |
| Virtual collections (shared table) | Virtual collections | Both can present filtered views of a shared table |
| Runtime collection creation | createCollection mutation | Vextro can create collections at runtime without code deploys |
| Singletons | defineVextroGlobal() | Single-document settings, same concept |
| Fields (DB columns) | f field builders | Vextro fields produce both schema validators and admin UI config |
| Display templates | useAsTitle + list columns | Vextro uses useAsTitle for document labels, listConfig.columns for list views |
| Interfaces (custom inputs) | Custom components + field registry | Both support overriding the default input for a field type |
| Displays (custom renderers) | Relationship renderers + plugins | Vextro uses a plugin-based renderer system |
| Flows (automation) | Hooks + Convex scheduled functions | Vextro hooks are code-first; Convex schedulers replace cron/trigger flows |
| Roles & permissions | access config + getUserRoles callback | Permission strings matched against user roles |
| Shares | Not applicable | Vextro does not have a public link-sharing system |
| Translations | localization config | Per-collection locale configuration with field-level control |
| Insights (dashboards) | Not built-in | Vextro focuses on content management, not analytics dashboards |
| Extensions (modules, layouts, etc.) | Plugins (planned) | Extensibility via the plugin API |
| REST / GraphQL APIs | No HTTP API -- call Convex functions directly | Type-safe function calls replace REST endpoints |
| SQL database (Postgres, MySQL, etc.) | Convex database | Real-time, transactional, hosted |
Key differences
Code-first vs database-first
Directus discovers collections by introspecting your SQL schema. You create tables in the database and Directus generates the UI.
Vextro works the opposite way: you define collections in TypeScript with defineVextroCollection(), and Vextro generates both the Convex schema and the admin UI from a single source of truth.
// Vextro: define once, get schema + admin UI
export const products = defineVextroCollection({
label: "Products",
tableName: "products",
collectionType: "content",
fields: {
name: f.text({ required: true }),
sku: f.text({ required: true }),
price: f.number({ required: true }),
description: f.richText(),
category: f.select({ options: ["electronics", "clothing", "food"] }),
},
});
// Use products.table in your Convex schema
export default defineSchema({
products: products.table,
}); Virtual collections bridge the gap
If you prefer the Directus-style approach of managing existing tables without code changes, Vextro's virtual collections provide exactly that. You can point virtual collections at any existing Convex table and get a full admin UI without writing field builders.
// Manage an existing table without defineVextroCollection
const userManagement = {
slug: "users",
label: "Users",
collectionType: "system",
tableName: "users",
listConfig: {
columns: ["displayName", "email", "role"],
searchableFields: ["displayName", "email"],
},
access: {
read: "admin:users:read",
update: "admin:users:write",
},
}; You can also create virtual collections at runtime through the admin UI, just like adding a collection in Directus.
Real-time by default
In Directus, real-time updates require configuring WebSocket connections and subscribing to specific collections. With Convex, every query is a live subscription automatically. When data changes, all connected clients update instantly -- no WebSocket setup, no polling, no configuration.
No REST or GraphQL layer
Directus generates REST and GraphQL endpoints for every collection. Vextro does not -- because Convex does not need them. Your frontend calls Convex functions directly with full type safety:
// Directus: REST API
const response = await fetch("/items/products?filter[category][_eq]=electronics");
// Vextro/Convex: direct function call
const products = await convex.query(api.products.listByCategory, { category: "electronics" }); Atomic transactions
Directus hooks can encounter partial failures since they operate on a traditional SQL database. Convex mutations are atomic transactions -- if anything throws, everything rolls back. This eliminates an entire class of data consistency bugs.
Trait-based collection behavior
Directus collections all behave the same way -- the database schema determines everything. Vextro collections have configurable traits that control which automatic features apply:
// A collection with status workflow, timestamps, and versioning
defineVextroCollection({
collectionType: "content",
traits: { statusWorkflow: true, versioning: true },
// ...
});
// A collection with just timestamps, no status workflow
defineVextroCollection({
collectionType: "operational",
traits: { statusWorkflow: false, autoTimestamps: true },
// ...
}); Directus Flows vs Vextro Hooks
Directus Flows are a visual automation builder with triggers, operations, and conditions. Vextro takes a code-first approach with hooks and Convex scheduled functions.
| Directus Flows | Vextro equivalent |
|---|---|
| Event trigger (item.create, item.update) | beforeChange / afterChange hooks |
| Schedule trigger (cron) | Convex cron jobs (crons.ts) |
| Webhook trigger | Webhook configuration in admin |
| Send email operation | afterChange hook + email service |
| Run script operation | Hook functions (full Node.js) |
| Condition operation | Logic in hook functions |
// Vextro: hook equivalent of a Directus Flow
defineVextroCollection({
// ...
hooks: {
afterChange: [
async ({ data, operation, ctx }) => {
if (operation === "create") {
// Send notification, update related records, etc.
await ctx.scheduler.runAfter(0, api.notifications.send, {
message: `New product created: ${data.name}`,
});
}
},
],
},
}); Migration path
1. Map your collections
For each Directus collection, create a defineVextroCollection() call with the equivalent fields. If you have tables you want to manage without defining field builders, use virtual collections instead.
2. Define your schema
Use the collection .table property to generate Convex table definitions. Add them to convex/schema.ts. Run npx convex dev to create the tables.
3. Move your Flows to hooks
Replace Directus Flows with Vextro hooks for event-driven logic, and Convex cron jobs for scheduled tasks. See Hooks for the complete API.
4. Migrate permissions
Replace Directus role/permission configurations with Vextro's access config and getUserRoles callback. See Access Control.
5. 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 SQL database and inserts into Convex.
6. Write frontend queries
Replace Directus SDK / REST calls with Convex query functions. See Querying Data for examples.
Next steps
- Key Concepts -- understand the Convex execution model
- Quick Start -- build your first Vextro collection
- Virtual Collections -- manage existing tables without field builders
- Hooks -- replace Directus Flows with code-first automation