Skip to main content
Node actions are like regular actions, but they are executed in the Node.js runtime, so their handlers can use Node’s built-in modules and npm packages that depend on them. You declare a node action group by building its spec with GroupSpec.makeNode() (rather than GroupSpec.make()); that marks the group as Node-runtime, and Confect adds Convex’s "use node" directive to the generated module accordingly. A node action group is otherwise an ordinary group — the same colocated .spec.ts/.impl.ts files, following the same naming and nesting conventions.

Node Spec

A node spec uses GroupSpec.makeNode() and one of FunctionSpec.publicNodeAction() or FunctionSpec.internalNodeAction() to define functions.
confect/email.spec.ts
import { FunctionSpec, GroupSpec } from "@confect/core";
import * as Schema from "effect/Schema";

export default GroupSpec.makeNode().addFunction(
  FunctionSpec.publicNodeAction({
    name: "send",
    args: () =>
      Schema.Struct({
        to: Schema.String,
        subject: Schema.String,
        body: Schema.String,
      }),
    returns: () => Schema.Null,
  }),
);

Node Impl

A node impl default-imports its sibling spec and passes the database schema from _generated/schema to FunctionImpl.make and GroupImpl.make — exactly like a non-node impl; only the spec differs (GroupSpec.makeNode()). Each handler runs in the Node.js runtime, so it has access to NodeContext services from the @effect/platform package.
confect/email.impl.ts
import { FunctionImpl, GroupImpl } from "@confect/server";
import { Command } from "@effect/platform";
import * as Console from "effect/Console";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import databaseSchema from "./_generated/schema";
import email from "./email.spec";

const send = FunctionImpl.make(
  databaseSchema,
  email,
  "send",
  Effect.fn(function* ({ to, subject, body }) {
    const result = yield* Command.make(
      "echo",
      `Sending email to ${to} with subject ${subject} and body ${body}…`,
    ).pipe(Command.stdout("pipe"), Command.string, Effect.orDie);

    yield* Console.log(result);
    yield* Effect.sleep(Duration.seconds(1));
    yield* Console.log("Email sent!");

    return null;
  }),
);

export default GroupImpl.make(databaseSchema, email).pipe(
  Layer.provide(send),
  GroupImpl.finalize,
);

Calling Node Actions

Node actions are referenced like any other function. A node group at confect/email.spec.ts is reached at refs.public.email.
import { useAction } from "@confect/react";
import refs from "../confect/_generated/refs";

const sendEmail = useAction(refs.public.email.send);

sendEmail({
  to: "user@example.com",
  subject: "Hello",
  body: "Hello from Confect!",
});