Adopt in existing project

Setup Ents in an existing Convex project

Starting a new project from scratch? Follow the setup guide instead.

Use this guide if you have a lot of existing code using ctx.db which you want to keep while you migrate to Ents.

Install the NPM library

npm install convex-ents convex-helpers

This also installs convex-helpers, used in step 3.

Modify your schema

Update your schema file. These changes will have no effect on your existing code using ctx.db.

  • Replace defineSchema with defineEntSchema
  • Replace all defineTable with defineEnt
  • Add entDefinitions as shown

Example changes:

convex/schema.ts
- import { defineSchema, defineTable } from "convex/server";
+ import { defineEnt, defineEntSchema, getEntDefinitions } from "convex-ents";
import { v } from "convex/values";
 
- const schema = defineSchema({
+ const schema = defineEntSchema({
-  messages: defineTable({
+  messages: defineEnt({
    text: v.string(),
  }),
-  users: defineTable({
+  users: defineEnt({
    name: v.string(),
  }),
});
 
export default schema;
 
+ export const entDefinitions = getEntDefinitions(schema);

For more details on declaring the schema see Ent Schema.

Setup custom functions

Add a functions.ts file with the following contents:

Click to show
convex/functions.ts
import { entsTableFactory } from "convex-ents";
import {
  customAction,
  customCtx,
  customMutation,
  customQuery,
} from "convex-helpers/server/customFunctions";
import { GenericDatabaseReader, GenericDatabaseWriter } from "convex/server";
import { DataModel } from "./_generated/dataModel";
import {
  internalMutation as baseInternalMutation,
  internalQuery as baseInternalQuery,
  mutation as baseMutation,
  query as baseQuery,
} from "./_generated/server";
import { entDefinitions } from "./schema";
 
type LegacyTables = "messages" | "users";
 
export const query = customQuery(
  baseQuery,
  customCtx(async (ctx) => {
    return {
      table: entsTableFactory(ctx, entDefinitions),
      db: ctx.db as unknown as GenericDatabaseReader<
        Pick<DataModel, LegacyTables>
      >,
    };
  }),
);
 
export const internalQuery = customQuery(
  baseInternalQuery,
  customCtx(async (ctx) => {
    return {
      table: entsTableFactory(ctx, entDefinitions),
      db: ctx.db as unknown as GenericDatabaseReader<
        Pick<DataModel, LegacyTables>
      >,
    };
  }),
);
 
export const mutation = customMutation(
  baseMutation,
  customCtx(async (ctx) => {
    return {
      table: entsTableFactory(ctx, entDefinitions),
      db: ctx.db as GenericDatabaseWriter<Pick<DataModel, LegacyTables>>,
    };
  }),
);
 
export const internalMutation = customMutation(
  baseInternalMutation,
  customCtx(async (ctx) => {
    return {
      table: entsTableFactory(ctx, entDefinitions),
      db: ctx.db as GenericDatabaseWriter<Pick<DataModel, LegacyTables>>,
    };
  }),
);

Update the LegacyTables type with an enumeration of your existing tables which you want to keep using with ctx.db.

Add helper types

Add a types.ts file with the following contents:

Click to show
convex/types.ts
import { GenericEnt, GenericEntWriter } from "convex-ents";
import { CustomCtx } from "convex-helpers/server/customFunctions";
import { TableNames } from "./_generated/dataModel";
import { mutation, query } from "./functions";
import { entDefinitions } from "./schema";
 
export type QueryCtx = CustomCtx<typeof query>;
export type MutationCtx = CustomCtx<typeof mutation>;
 
export type Ent<TableName extends TableNames> = GenericEnt<
  typeof entDefinitions,
  TableName
>;
export type EntWriter<TableName extends TableNames> = GenericEntWriter<
  typeof entDefinitions,
  TableName
>;

Use custom functions to read and write ents

You can now replace function constructors from _generated with the custom ones you just set up. This will have no impact on your existing functions:

convex/messages.ts
import { v } from "convex/values";
- import { mutation, query } from "./_generated/server";
+ import { mutation, query } from "./functions";
 
export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("messages");
 
+    // You can now use ctx.table as well:
+    return await ctx.table("messages");
  },
});

Similarly replace QueryCtx and MutationCtx with imports from ./types.

You can now use ctx.table for new code and to replace existing code.

For more details see Reading Ents and Writing Ents.

Adopt Ents features

Convex Ents maintain invariants, such as enforcing unique fields, leaving no dangling references, etc. To make sure these invariants are not violated, you should not acccess a table with any Ents specific definitions (unique fields, edges) via the built-in ctx.db API.

To prevent ctx.db access to some of the tables in your schema, amend your functions.ts file. In this example we remove access to "messages" from ctx.db:

convex/functions.ts
- type LegacyTables = "messages" | "users";
+ type LegacyTables = "users";

(optional) Finish moving to ctx.table

Once you're no longer using ctx.db in any of your code, you can remove it from your custom functions setup:

convex/functions.ts
 
      // For query and internalQuery:
+      db: undefined,
-      db: ctx.db as unknown as GenericDatabaseReader<
-        Pick<DataModel, LegacyTables>
-      >,
 
      // For mutation and internalMutation:
+      db: undefined,
-      db: ctx.db as GenericDatabaseWriter<
-        Pick<DataModel, LegacyTables>
-      >,
 

For more details see Configuring Functions.