#authed custom function

11 messages · Page 1 of 1 (latest)

lucid ingot
#

can you helpe make a custom function with convex-helpers that adds the user identity to the context?

elfin cryptBOT
#

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

#

Yes! You can create a custom function (a drop-in replacement for query/mutation/action) that injects the authenticated user (or identity) into ctx using convex-helpers. Below are two common approaches depending on whether you’re using Convex Auth users or just the raw identity.

  1. Add the authenticated User document to ctx (recommended with Convex Auth)
  • Looks up the current userId using @convex-dev/auth/server, fetches the user doc, and exposes it as ctx.user.
// convex/functions.ts
/* eslint-disable no-restricted-imports */
import { query as rawQuery, mutation as rawMutation, action as rawAction } from "./_generated/server";
/* eslint-enable no-restricted-imports */
import { customCtx, customQuery, customMutation, customAction } from "convex-helpers/server/customFunctions";
import { getAuthUserId } from "@convex-dev/auth/server";

const withUserCtx = customCtx(async (ctx) => {
  const userId = await getAuthUserId(ctx);
  if (userId === null) throw new Error("Authentication required");
  const user = await ctx.db.get(userId);
  if (!user) throw new Error("User not found");
  return { user };
});

export const userQuery = customQuery(rawQuery, withUserCtx);
export const userMutation = customMutation(rawMutation, withUserCtx);
export const userAction = customAction(rawAction, withUserCtx);

// Example usage:
export const addNote = userMutation({
  args: { note: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.insert("notes", { note: args.note, authorId: ctx.user._id });
  },
});

This pattern comes straight from Convex’s authorization examples and adds user to ctx for type-safe access in your handlers. [Authorization; Authz helpers]

#
  1. Add raw identity to ctx (no users table)
  • If you just want the identity from ctx.auth.getUserIdentity(), you can inject it directly:
// convex/functions.ts
/* eslint-disable no-restricted-imports */
import { query as rawQuery, mutation as rawMutation, action as rawAction } from "./_generated/server";
/* eslint-enable no-restricted-imports */
import { customCtx, customQuery, customMutation, customAction } from "convex-helpers/server/customFunctions";

const withIdentityCtx = customCtx(async (ctx) => {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Not authenticated");
  // identity contains subject, issuer, tokenIdentifier, and provider fields (if configured)
  return { identity };
});

export const authQuery = customQuery(rawQuery, withIdentityCtx);
export const authMutation = customMutation(rawMutation, withIdentityCtx);
export const authAction = customAction(rawAction, withIdentityCtx);

// Example usage:
export const myInfo = authQuery({
  args: {},
  handler: async (ctx) => {
    return { userId: ctx.identity.subject, email: ctx.identity.email ?? null };
  },
});

This mirrors the built-in pattern of getting identity in the handler, but centralizes the check and gives you ctx.identity everywhere. [Auth in functions]

#

Notes and tips

  • You can extend ctx with additional fields (e.g., a wrapped db for RLS) using the same customCtx approach. [Custom functions]
  • If you want to enforce usage of your custom builders, add an ESLint no-restricted-imports rule to prevent importing the raw query/mutation/action directly. [ESLint setup]
  • The same approach works for tools/agents if you need auth-aware context there too. [Agents tools]

If you tell me whether you’re using Convex Auth users or another auth provider (e.g., Clerk/Auth0/custom JWT), I can tailor the snippet to your setup.

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

#

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

lucid ingot
#

@elfin crypt is it possible to wrap a custom function inside another custom function?
for example, i already have those "authed" functions
but i have another custom function that adds triggers, how can i add the created functions to that new custom function with triggers?

elfin cryptBOT
#

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

#

Short answer: you can, but the recommended pattern is to avoid stacking custom functions “on top of” each other blindly. Instead, compose them explicitly inside a single custom function (one wrapping layer), or call one wrapper (like triggers.wrapDB) inside the input step of the other. [Authorization in practice; Triggers]

Why: convex-helpers’ custom functions are designed to be composed in one place so it’s obvious how ctx/args are modified, and to avoid confusing, nested side effects. [Authorization in practice; Further extension]

Two good ways to combine “authed” functions with triggers:

  • Single-layer composition (preferred): build one custom function that first wraps ctx with triggers, then adds auth.
/* eslint-disable no-restricted-imports */
import { mutation as baseMutation } from "./_generated/server";
/* eslint-enable no-restricted-imports */
import { customMutation, customCtx } from "convex-helpers/server/customFunctions";
import { Triggers } from "convex-helpers/server/triggers";
import { DataModel } from "./_generated/dataModel";

const triggers = new Triggers<DataModel>();
#

// One custom mutation builder that applies triggers AND auth.
export const authedMutationWithTriggers = customMutation(
baseMutation,
customCtx(async (baseCtx) => {
// First, wrap DB so triggers run atomically with writes
const withTriggersCtx = await triggers.wrapDB(baseCtx); // applies wrapped db
// Then, enforce auth and add identity/user to ctx
const identity = await withTriggersCtx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
return { identity }; // merged into ctx
})
);

// use authedMutationWithTriggers({ args, handler })


This follows the “one wrapping layer” guidance and keeps all ctx modifications in one place. [[Triggers](<https://stack.convex.dev/triggers>); [Authorization in practice](<https://stack.convex.dev/authorization#authorization-in-practice>); [Further extension](<https://stack.convex.dev/custom-functions#further-extension>)]

- Explicitly call triggers inside your existing authed builder:
If you already have authedMutation, inject triggers.wrapDB in its input step before returning the new ctx.

```ts
/* eslint-disable no-restricted-imports */
import { mutation as baseMutation } from "./_generated/server";
/* eslint-enable no-restricted-imports */
import { customMutation } from "convex-helpers/server/customFunctions";
import { Triggers } from "convex-helpers/server/triggers";
import { DataModel } from "./_generated/dataModel";

const triggers = new Triggers<DataModel>();

export const authedMutation = customMutation(baseMutation, {
  args: {},
  input: async (baseCtx) => {
    // Apply triggers explicitly here
    const withTriggersCtx = await triggers.wrapDB(baseCtx);

    // Then auth
    const identity = await withTriggersCtx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
#

// Return updated ctx; using the trigger-wrapped db
return { ctx: { ...withTriggersCtx, identity } };
},
});


This mirrors advice from the Discord thread: prefer using triggers.wrapDB inside your custom function’s input over stacking a custom function on top of another custom function. [[Discord thread](<https://discord.com/channels/1019350475847499849/1301461829708677201>)]

Notes
- Triggers only run if you actually execute writes through the trigger-wrapped ctx.db; hence wrapping must happen before auth or other ctx changes that will use db. [[Triggers](<https://stack.convex.dev/triggers>)]
- If you already exported separate variants (e.g., authedQuery, authedMutation), you can define parallel “…WithTriggers” builders via the “single-layer composition” pattern above to keep imports clear and avoid nested wrappers. [[Authorization in practice](<https://stack.convex.dev/authorization#authorization-in-practice>); [Further extension](<https://stack.convex.dev/custom-functions#further-extension>)]

If you share your current authed custom function signature, I can show the exact minimal diff to add triggers in the input step.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------