Plugins
Cross-cutting plugins for Better Convex
In this guide, we'll explain how Better Convex plugins work, what's available today, and where the system is headed.
What are plugins?
Plugins are cross-cutting building blocks inspired by better-auth's plugin architecture. A single plugin registration can touch multiple layers of your app at once:
- Schema — inject internal storage tables (e.g.
ratelimit_state,migration_run) - Server/runtime — add guards, hooks, or background processing
- Client — optional client helpers
This means you don't wire up tables, server logic, and client helpers separately. You register a plugin once, and it handles everything.
Available plugins
| Plugin | What it gives you | Best for |
|---|---|---|
ratelimit | Upstash-style API with Convex-native storage, middleware-friendly guards, and optional React hook UX | Protecting expensive mutations, login flows, and public endpoints |
Experimental — more to come
The plugin system is experimental. The core interface (key, tableNames, inject) is stable enough for production use today, but the API surface will grow.
We're working toward a richer plugin contract inspired by better-auth's ctx.api pattern — where plugins can expose their own procedures, extend the ORM context, and be overridden by downstream code. Think of it like auth.api but for any cross-cutting concern: a plugin registers its schema, exposes typed server functions via ctx.api, and optionally ships client helpers.
What's coming:
ctx.apifor plugins — plugins will be able to expose typed procedures (queries, mutations, actions) that integrate with cRPC and the ORM context, similar to how better-auth plugins exposeauth.api.pluginName.*- Extensible and overridable — downstream code will be able to override plugin behavior, swap implementations, or extend plugin-provided procedures
- More first-party plugins — we're evaluating candidates like the official convex components
For now, the plugin system gives you schema injection and table management.
API Reference
Every plugin implements the OrmSchemaPlugin interface:
type OrmSchemaPlugin = {
key: string;
tableNames: readonly string[];
inject: (schema) => schema & Record<string, unknown>;
};You register plugins in defineSchema:
import { ratelimitPlugin } from 'better-convex/plugins/ratelimit';
export default defineSchema(tables, {
plugins: [ratelimitPlugin()],
});Two plugins are built-in and always active — you don't need to register them:
| Plugin | Tables injected | Purpose |
|---|---|---|
aggregatePlugin | aggregate_bucket, aggregate_member, aggregate_extrema, aggregate_rank_tree, aggregate_rank_node, aggregate_state | Powers aggregateIndex and rankIndex |
migrationPlugin | migration_state, migration_run | Powers defineMigration and createMigrationHandlers |
Opt-in plugins like ratelimitPlugin must be registered explicitly.
Important: Each plugin key must be unique. Registering the same plugin twice (e.g. [ratelimitPlugin(), ratelimitPlugin()]) throws a duplicate plugin error. If a plugin tries to inject a table name that's already in your schema, it throws a collision error.