Internal functions

internalQuery / internalMutation / internalAction — server-only functions never exposed to the client.

Last updated:

Every query, mutation, and action you export is public by default, reachable from clients through the generated api. An internal function is identical except for its visibility: it is never exposed on api, the Durable Object's external RPC path rejects it, and it is reachable only server-side via ctx.runQuery / ctx.runMutation / ctx.runAction against the internal reference.

Use them for privileged logic you don't want a client to call directly: post-webhook writes, scheduled jobs, fan-out helpers, anything that assumes a trusted caller.

import { internalMutation, internalQuery, v } from "@/lunora/_generated/server";

export const byStripeId = internalQuery.input({ stripeId: v.string() }).query(async ({ ctx, args }) => {
    return ctx.db
        .query("customers")
        .withIndex("by_stripe", (q) => q.eq("stripeId", args.stripeId))
        .unique();
});

export const applyStripeSync = internalMutation.input({ id: v.id("customers"), remote: v.object({ plan: v.string() }) }).mutation(async ({ ctx, args }) => {
    await ctx.db.patch(args.id, { plan: args.remote.plan });
});

The three builders mirror their public counterparts and produce the same ctx. internalQuery gets a read-only QueryCtx, internalMutation a transactional MutationCtx, and internalAction the side-effectful ActionCtx.

PublicInternalctx
queryinternalQueryQueryCtx
mutationinternalMutationMutationCtx
actioninternalActionActionCtx

Calling internal functions

Internal functions are addressed through the generated internal namespace (the sibling of api), and can only be invoked from another function's ctx:

import { action, v } from "@/lunora/_generated/server";
import { internal } from "@/lunora/_generated/api";

export const syncCustomer = action.input({ stripeId: v.string() }).action(async ({ ctx, args }) => {
    const customer = await ctx.runQuery(internal.customers.byStripeId, args);
    const remote = await (await ctx.fetch(`https://api.stripe.com/v1/customers/${args.stripeId}`)).json();

    await ctx.runMutation(internal.customers.applyStripeSync, { id: customer!._id, remote });
});

There is no client equivalent: internal.* references exist only in server-side generated code. A client that tries to invoke an internal function by name is rejected at the RPC boundary, so visibility is a real security boundary, not just a naming convention.

Internal functions and scheduling

The scheduler dispatches a function server-side, so its target is almost always internal. Schedule an internal* reference from a mutation or action:

import { mutation, v } from "@/lunora/_generated/server";
import { internal } from "@/lunora/_generated/api";

export const enqueueReport = mutation.input({ accountId: v.id("accounts") }).mutation(async ({ ctx, args }) => {
    // Build the report off the request path — and don't expose the builder to clients.
    await ctx.scheduler.runAfter(0, internal.reports.build, args);
});

The same applies to cron jobs (cronJobs) and connection-lifecycle hooks (onConnect / onDisconnect), which are registered as internal mutations the runtime dispatches on a schedule or socket event rather than via a client call.

See also