Getting Started
Installation
Prerequisites
Before installing Vextro, make sure you have the following:
- Node.js 18+ -- Vextro uses modern JavaScript features that require Node 18 or later.
- pnpm (recommended) -- Any package manager works, but the examples use pnpm. npm and yarn are also supported.
- An existing Convex project -- You need a Convex backend with
convex/directory and a deployed (or local dev) Convex instance. If you do not have one yet, follow the Convex quickstart first. - Astro 5+ -- Vextro's admin components are built for Astro 5 and later. Your admin app should be a standalone Astro project or a workspace app in a monorepo.
Install the package
Install vextro (auth is included in the core package):
pnpm add vextro Or with npm:
npm install vextro Or with yarn:
yarn add vextro Scaffold with vextro-init
The fastest way to set up the required files is the init script. Run it from your project root:
npx vextro-init This command creates the following files if they do not already exist:
| File | Purpose |
|---|---|
convex/vextroTables.ts | Convex table definitions for admin metadata (collections, fields, relationships, user preferences) |
convex/lib/collectionRegistry.ts | Collection registry mapping slugs to table names |
convex/admin.ts | Admin CRUD functions (list, create, update, delete documents) |
src/config/admin.ts | Vextro admin configuration (brand name, navigation, auth settings) |
src/lib/convex.ts | Convex HTTP client helper for SSR data fetching |
Existing files are not overwritten
The init script skips any file that already exists. Use --force to overwrite existing files if you want to reset to the defaults.
Install the Convex component
Vextro ships as a Convex component. Register it in your convex/convex.config.ts:
import { defineApp } from "convex/server";
import vextro from "vextro/convex.config";
const app = defineApp();
app.use(vextro);
export default app; Then spread the Vextro tables into your schema. Open convex/schema.ts and add:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { vextroTables } from "./vextroTables";
export default defineSchema({
...vextroTables,
// Your own tables
pages: defineTable({
title: v.string(),
slug: v.string(),
content: v.optional(v.any()),
status: v.union(v.literal("draft"), v.literal("published"), v.literal("archived")),
updatedAt: v.number(),
updatedBy: v.optional(v.string()),
})
.index("by_slug", ["slug"])
.index("by_status", ["status"]),
}); Deploy the schema to Convex:
npx convex dev Configure the Astro integration
Install required Astro integrations
Vextro uses Svelte for interactive components. Install the Astro Svelte integration:
pnpm add @astrojs/svelte svelte Add the integration to your astro.config.ts:
import { defineConfig } from "astro/config";
import svelte from "@astrojs/svelte";
export default defineConfig({
output: "server",
integrations: [svelte()],
}); SSR mode required
Vextro admin pages require server-side rendering. Make sure your Astro config uses output: "server" or output: "hybrid". Static output mode will not work for the admin panel.
Set up the admin config
Edit src/config/admin.ts to configure your admin panel:
import type { VextroConfig } from "vextro";
export const adminConfig: VextroConfig = {
brandName: "My CMS",
basePath: "/",
auth: {
providerId: "credentials",
callbackPath: "/api/auth/callback",
authPath: "/api/auth",
title: "Sign in to the admin panel",
buttonLabel: "Sign in",
},
workspaces: [
{ label: "Content", href: "/" },
],
navigation: [
{ label: "Dashboard", href: "/" },
],
richText: {
allowRelativeLinks: true, // allow relative URLs in link dialogs
toolbar: {
sourceView: true, // enable source view on all rich text fields
},
},
}; Additional config options
The table below covers commonly used VextroConfig options beyond the basics shown above.
| Option | Type | Default | Description |
|---|---|---|---|
brandName* | string | -- | Display name shown in the admin header and login page |
logoUrl | string | -- | URL to a logo image shown in the sidebar header |
basePath | string | "/" | Base path prefix for all admin routes |
presenceLabelFn | (user) => string | initials from userName | Custom function to derive display labels from presence users (shown on field-level presence badges). Receives { userId, userName, userEmail? } and returns a short string (e.g. initials or a first name). |
autoSave.enabled | boolean | true | Whether auto-save is enabled project-wide. Individual collections can override this. |
autoSave.debounceMs | number | 3000 | Milliseconds to wait after the last keystroke before auto-saving. |
accessControl.builtInUI | boolean | true | When false, hides the built-in Access Control section from the sidebar entirely. Useful if you manage users and roles outside the admin UI. |
accessControl.labels.sectionTitle | string | "Access Control" | Override the sidebar section heading. |
accessControl.labels.users | { singular?, plural? } | "User" / "Users" | Override the display labels for the Users link. |
accessControl.labels.roles | { singular?, plural? } | "Role" / "Roles" | Override the display labels for the Roles link. |
scope.defaultScopeFilter | boolean | true | When scope is configured, relationship fields are scope-filtered by default. Set to false to allow cross-scope references by default (individual fields can still opt in with scopeFilter: true). |
export const adminConfig: VextroConfig = {
brandName: "My CMS",
basePath: "/",
// Show initials from the user's first name only in presence badges
presenceLabelFn: ({ userName }) => userName.split(" ")[0]?.[0]?.toUpperCase() ?? "?",
// Slow down auto-save to 5 seconds
autoSave: { enabled: true, debounceMs: 5000 },
// Rename the Access Control section
accessControl: {
builtInUI: true,
labels: {
sectionTitle: "Team",
users: { singular: "Member", plural: "Members" },
roles: { singular: "Role", plural: "Roles" },
},
},
auth: { /* ... */ },
}; Choose an auth provider
Vextro supports two auth providers. Pick the one that fits your project's constraints.
| Better Auth | Clerk | |
|---|---|---|
| Setup effort | More configuration required | Minimal — works out of the box |
| User data ownership | Fully self-hosted in your Convex database | Managed by Clerk's cloud |
| Auth UI | You build or customize the login UI | Pre-built sign-in, sign-up, and profile UI |
| MFA / social logins | Available via plugins you configure | Built-in and enabled from the Clerk dashboard |
| Vendor dependency | None | Clerk |
| Compliance / data residency | Full control — data stays where you host it | Subject to Clerk's data processing terms |
Choose Better Auth when:
- You want full control over user data and auth logic
- You need self-hosted auth for data residency or compliance requirements
- You want to build or fully customize the auth UI
- You are comfortable running your own auth infrastructure
Choose Clerk when:
- You want to move fast with minimal auth setup
- You prefer a managed service handling user management, MFA, and social logins
- You want a polished pre-built auth UI without building it yourself
- You do not mind a vendor dependency for auth
Key trade-off: Better Auth gives you more control at the cost of more setup work. Clerk gets you running faster but introduces a vendor dependency. Both are fully supported — see Authentication (Better Auth) or Authentication (Clerk) for setup details.
Environment variables
Vextro requires the following environment variables:
If using Better Auth
# Convex deployment URL (required)
PUBLIC_CONVEX_URL=https://your-project.convex.cloud
# Better Auth secret (required for auth)
BETTER_AUTH_SECRET=your-random-secret-string
# Better Auth base URL (required)
BETTER_AUTH_URL=http://localhost:4321 If using Clerk
# Convex deployment URL (required)
PUBLIC_CONVEX_URL=https://your-project.convex.cloud
# Clerk credentials (required for auth)
PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
CLERK_JWT_ISSUER_DOMAIN=https://your-app.clerk.accounts.dev For file uploads with S3-compatible storage, add:
S3_BUCKET=your-bucket-name
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_ENDPOINT=https://s3.amazonaws.com # or your R2/MinIO endpoint S3 is optional
If you do not configure S3 credentials, Vextro falls back to Convex file storage for uploads. S3 is only needed if you want external storage or advanced image processing with sharp.
Set up auth middleware
If using Better Auth
Create src/middleware.ts to protect admin routes:
import { createAuthMiddleware } from "vextro/auth/better-auth";
export const onRequest = createAuthMiddleware({
getConvexSiteUrl: () => import.meta.env.PUBLIC_CONVEX_URL,
isAuthRoute: (pathname) =>
pathname.startsWith("/api/auth/") || pathname === "/login",
loginPath: "/login",
setAuthToken: (context, token) => {
context.locals.authToken = token;
},
}); This middleware exchanges session cookies for Convex JWTs on every request. Unauthenticated page requests are redirected to the login page; API routes receive a 401 response instead. See the Authentication (Better Auth) guide for all middleware options.
If using Clerk
Install the Clerk Astro package first:
pnpm add @clerk/astro Then create src/middleware.ts:
import { createClerkMiddleware } from "vextro/auth/clerk/middleware";
export const onRequest = createClerkMiddleware({
secretKey: import.meta.env.CLERK_SECRET_KEY,
isAuthRoute: (pathname) => pathname === "/login",
}); The Clerk middleware verifies the __session cookie (or Authorization: Bearer header) on every request. Unauthenticated page requests are redirected to /login; API routes receive a 401 response.
Next, create convex/auth.config.ts so Convex can validate Clerk JWTs:
import { createClerkConvexAuthConfig } from "vextro/auth/clerk/convex-config";
export default createClerkConvexAuthConfig({
issuerDomain: process.env.CLERK_JWT_ISSUER_DOMAIN!,
}); See the Authentication (Clerk) guide for RBAC modes, permission guards, and all middleware options.
Verify the setup
Start your dev servers:
# Terminal 1: Convex dev server
npx convex dev
# Terminal 2: Astro dev server
pnpm dev Open your admin app in the browser (typically http://localhost:4321). You should see the Vextro login page. After signing in, the admin shell automatically seeds metadata from your collection and global definitions, then loads the dashboard with the sidebar showing your registered collections.
Next steps
- Quick Start -- define your first collection with field builders and see it in the admin panel
- Fields -- explore all available field types and their options