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:

FilePurpose
convex/vextroTables.tsConvex table definitions for admin metadata (collections, fields, relationships, user preferences)
convex/lib/collectionRegistry.tsCollection registry mapping slugs to table names
convex/admin.tsAdmin CRUD functions (list, create, update, delete documents)
src/config/admin.tsVextro admin configuration (brand name, navigation, auth settings)
src/lib/convex.tsConvex 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.

OptionTypeDefaultDescription
brandName*string--Display name shown in the admin header and login page
logoUrlstring--URL to a logo image shown in the sidebar header
basePathstring"/"Base path prefix for all admin routes
presenceLabelFn(user) => stringinitials from userNameCustom 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.enabledbooleantrueWhether auto-save is enabled project-wide. Individual collections can override this.
autoSave.debounceMsnumber3000Milliseconds to wait after the last keystroke before auto-saving.
accessControl.builtInUIbooleantrueWhen 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.sectionTitlestring"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.defaultScopeFilterbooleantrueWhen 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 AuthClerk
Setup effortMore configuration requiredMinimal — works out of the box
User data ownershipFully self-hosted in your Convex databaseManaged by Clerk's cloud
Auth UIYou build or customize the login UIPre-built sign-in, sign-up, and profile UI
MFA / social loginsAvailable via plugins you configureBuilt-in and enabled from the Clerk dashboard
Vendor dependencyNoneClerk
Compliance / data residencyFull control — data stays where you host itSubject 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
Previous
Key Concepts