#Triggers in production

14 messages · Page 1 of 1 (latest)

pastel radish
#

Am I doing this correctly? I want to register triggers that fire and upload the latest data to my melisearch cluster only on the production convex:

import {
internalMutation as rawInternalMutation,
mutation as rawMutation,
} from "./_generated/server";
/* eslint-enable no-restricted-imports */
import {
customCtx,
customMutation,
} from "convex-helpers/server/customFunctions";
import { Triggers } from "convex-helpers/server/triggers";
import { internal } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
import { retrier } from "./retrier";
import { isProduction } from "./utils";

const triggers = new Triggers<DataModel>();
const prod = isProduction();
// register a function to run when a ctx.db.insert, ctx.db.patch, ctx.db.replace, or ctx.db.delete changes the "profiles" table
triggers.register("profiles", async (ctx, change) => {
if (prod) {
await retrier.run(
ctx,
internal.customActions.internal.meiliSearch.syncProfile
.syncProfileToMeili,
{
profileId: change.id,
indexName: "profiles",
}
);
}
});

triggers.register("links", async (ctx, change) => {
if (prod) {
const link = await ctx.db.get(change.id);
await retrier.run(
ctx,
internal.customActions.internal.meiliSearch.syncProfile
.syncProfileToMeili,
{
profileId: link.profileId,
indexName: "profiles",
}
);
}
});

export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(
rawInternalMutation,
customCtx(triggers.wrapDB)
);

uncut gulchBOT
#

Hi @pastel radish. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Your approach to registering triggers that only fire in production is generally correct and aligns with the recommended usage of triggers in Convex. Here are some key points from the knowledge sources that support your implementation and a few best practices to consider:

What You're Doing Right

  • Trigger Registration: You are using the Triggers class from convex-helpers to register triggers on the "profiles" and "links" tables. This is the standard way to set up triggers in Convex, as shown in the documentation:

    const triggers = new Triggers<DataModel>();
    triggers.register("users", async (ctx, change) => {
      console.log("user changed", change);
    });
    export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
    export const internalMutation = customMutation(rawInternalMutation, customCtx(triggers.wrapDB));
    

    Database Triggers Example

  • Production-Only Logic: You are using a flag (isProduction()) to ensure that the triggers only perform the Meilisearch sync in production. This is a reasonable pattern for environment-specific behavior.

  • Wrapping Mutations: You are exporting mutation and internalMutation using the customMutation and customCtx(triggers.wrapDB) wrappers. This is required for triggers to run, as triggers only fire when mutations are wrapped this way, not with the default Convex mutations or when data is changed via the dashboard or import tools Best Practices.

Best Practices and Considerations

#
  • Centralized Trigger Registration: It's recommended to register all triggers before exporting your wrapped mutations, ideally in a single file (like functions.ts). This ensures all triggers are active when your mutations run Best Practices.

  • ESLint Enforcement: To avoid accidental imports of the raw mutation or internalMutation, use the no-restricted-imports ESLint rule to enforce importing from your wrapped exports ESLint Setup.

  • Side Effects in Triggers: Triggers run inside the same transaction as the mutation. If your retrier.run or Meilisearch sync can fail or take a long time, be aware that this could impact the mutation's performance or reliability. If you need to do asynchronous or debounced processing, consider using ctx.scheduler to schedule work outside the transaction Asynchronous Debounced Processing.

  • Trigger Limitations: Triggers only run when data is changed via your wrapped mutations. They do not run for changes made directly in the dashboard, via import tools, or if you forget to use the wrapper Best Practices.

Summary

Your code is following the documented pattern for registering and using triggers in Convex, and your production-only logic is a valid approach. Just ensure all mutations that should fire triggers are wrapped, and consider the transactional nature of triggers when performing side effects.

If you have specific concerns about error handling, performance, or edge cases, let me know and I can provide more targeted advice based on the documentation.


#

@pastel radish Kapa.ai is still learning and improving, please let me know how I did by reacting below.

pastel radish
#

@uncut gulch I have a different init.ts file and this is my deployment command npx convex deploy --cmd 'npm run build' && npx convex run init do I need to include anything in init.ts for registering triggers?

uncut gulchBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You do not need to include trigger registration in your init.ts file if you are already registering your triggers at module load time (i.e., at the top level of your main functions file, before exporting your wrapped mutations).

The Convex trigger system works by registering triggers when the module is loaded, not by running a special registration step in init.ts. As long as your trigger registration code (e.g., triggers.register(...)) is executed when your Convex functions are loaded, the triggers will be active for all mutations that use your wrapped mutation exports. This is the recommended and idiomatic approach:

It's recommended to register all triggers before exporting your wrapped mutations, ideally in a single file (like functions.ts). This ensures all triggers are active when your mutations run Best practices.

Your init.ts file is typically used for seeding data or other one-time setup tasks, not for registering triggers. The triggers should be registered in your main functions file, not in init.ts.

So, with your deployment command:

npx convex deploy --cmd 'npm run build' && npx convex run init

you do not need to add anything special to init.ts for triggers, as long as your triggers are registered in your main functions file as described above.

If you ever want to register things like cron jobs at startup, you would do that in init.ts, but not for triggers Statically defined cron jobs.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@pastel radish Kapa.ai is still learning and improving, please let me know how I did by reacting below.

pastel radish
#

This is my current code:

import { internalMutation } from "../../triggers";
export const updateTimestamp = internalMutation({
args: {
id: v.id("profiles"),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.id, { updatedAt: Date.now() });
},
});

This is my triggers.ts:

import {
internalMutation as rawInternalMutation,
mutation as rawMutation,
} from "./_generated/server";
/* eslint-enable no-restricted-imports */
import {
customCtx,
customMutation,
} from "convex-helpers/server/customFunctions";
import { Triggers } from "convex-helpers/server/triggers";
import { internal } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
import { retrier } from "./retrier";
import { isProduction } from "./utils";

const triggers = new Triggers<DataModel>();
// register a function to run when a ctx.db.insert, ctx.db.patch, ctx.db.replace, or ctx.db.delete changes the "profiles" table
triggers.register("profiles", async (ctx, change) => {
console.log("🔥 Profile trigger fired!", { profileId: change.id, isProduction: isProduction() });
if (isProduction()) {
console.log("📤 Syncing profile to MeiliSearch...", change.id);
await retrier.run(
ctx,
internal.customActions.internal.meiliSearch.syncProfile
.syncProfileToMeili,
{
profileId: change.id,
indexName: "profiles",
}
);
console.log("✅ Profile sync completed", change.id);
} else {
console.log("⏭️ Skipping MeiliSearch sync (not in production)");
}
});

triggers.register("links", async (ctx, change) => {
console.log("🔗 Link trigger fired!", { linkId: change.id, isProduction: isProduction() });
if (isProduction()) {
const link = await ctx.db.get(change.id);
if (link) {
console.log("📤 Syncing profile via link to MeiliSearch...", { linkId: change.id, profileId: link.profileId });
await retrier.run(
ctx,
internal.customActions.internal.meiliSearch.syncProfile
.syncProfileToMeili,
{
profileId: link.profileId,
indexName: "profiles",
}
);
console.log("✅ Profile sync via link completed", { linkId: change.id, profileId: link.profileId });
} else {
console.warn("⚠️ Link not found for trigger", change.id);
}
} else {
console.log("⏭️ Skipping MeiliSearch sync (not in production)");
}
});

export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(
rawInternalMutation,
customCtx(triggers.wrapDB)
);

The trigger is not firing and I'm seeing zero logs related to the trigger @uncut gulch

uncut gulchBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your code and the knowledge sources, here are the most likely reasons your trigger is not firing or logging:

1. Trigger Registration Timing and Import Path

Triggers must be registered before you export your wrapped mutation and internalMutation functions, and you must ensure that you are always importing these wrapped versions in your function files—not the raw Convex ones. Your code appears correct in this regard, as you are importing internalMutation from your triggers.ts file.

2. Mutation Wrapping

Triggers only fire if the mutation is wrapped with your custom mutation (i.e., the one exported from your triggers.ts). If you accidentally import internalMutation from ./_generated/server instead of your triggers.ts, triggers will not run. Double-check that all mutations (including updateTimestamp) are using the wrapped version from your triggers.ts file, which you appear to be doing.

Triggers are attached to mutations with custom functions. This works by replacing ctx.db in the mutation with a wrapped version that has the same interface but also calls the trigger functions. Therefore, trigger functions will only run if the mutation is wrapped in the custom function.
That means triggers do not run in these cases:

  • If you forget the wrapper and declare a plain mutation.
  • When data is changed directly in the Convex dashboard.
  • When data is uploaded through npx convex import.
  • When data is uploaded through streaming import.
    Best practices

3. Direct Data Changes

If you are updating the database directly from the dashboard, via import, or any method that does not go through your wrapped mutation, triggers will not fire.

#

4. Console Logging

Logs from triggers will appear in the Convex dashboard logs. Make sure you are checking the correct deployment (dev vs. prod) and the correct logs page. You can filter logs by function name or text to help locate your trigger logs Dashboard Logs.

5. Trigger Not Registered

If your triggers.ts file is not imported anywhere, the triggers will not be registered. However, since you are importing internalMutation from ../../triggers, this should cause the file to be loaded and the triggers to be registered.

6. Other Issues

  • If you are running in a local environment, make sure you are running convex dev and that your code changes are picked up.
  • If you are deploying, ensure your deployment includes the latest code and that you are not accidentally using an old build.

Summary:
Your code structure looks correct, but the most common reasons for triggers not firing are:

  • Not using the wrapped mutation everywhere.
  • Data changes happening outside of wrapped mutations.
  • Not checking the correct logs or deployment.

If you have confirmed all of the above and still see no logs, try adding a simple console.log("Trigger file loaded") at the top of your triggers.ts to confirm the file is being executed at all. If you do not see this log, your file may not be imported as expected.

If you need further help, please provide more details about how you are invoking the mutation and how you are checking the logs.

Best practices
Dashboard Logs

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: