BETTER-CONVEX

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

PluginWhat it gives youBest for
ratelimitUpstash-style API with Convex-native storage, middleware-friendly guards, and optional React hook UXProtecting 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.api for 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 expose auth.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:

convex/functions/schema.ts
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:

PluginTables injectedPurpose
aggregatePluginaggregate_bucket, aggregate_member, aggregate_extrema, aggregate_rank_tree, aggregate_rank_node, aggregate_statePowers aggregateIndex and rankIndex
migrationPluginmigration_state, migration_runPowers 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.

Next steps

On this page