Skip to main content
Node actions are like regular actions, but they are executed in the Node.js runtime. To keep Node actions separate from the rest of your Convex API, they must be defined in their own spec and impl files: nodeSpec.ts and nodeImpl.ts.

Node Spec

A node spec uses Spec.makeNode() and GroupSpec.makeNode() to define groups, and FunctionSpec.publicNodeAction() or FunctionSpec.internalNodeAction() to define functions. By convention, node action specs and impls go in a node/ subdirectory of your confect/ directory, using the standard .spec.ts and .impl.ts suffixes.
confect/node/email.spec.ts
import { FunctionSpec, GroupSpec } from "@confect/core";
import { Schema } from "effect";

export const email = GroupSpec.makeNode("email").addFunction(
  FunctionSpec.publicNodeAction({
    name: "send",
    args: Schema.Struct({
      to: Schema.String,
      subject: Schema.String,
      body: Schema.String,
    }),
    returns: Schema.Null,
  }),
);
Your node spec must be the default export of a nodeSpec.ts file in your confect/ directory.
confect/nodeSpec.ts
import { Spec } from "@confect/core";
import { email } from "./node/email.spec";

export default Spec.makeNode().add(email);

Node Impl

A node impl uses nodeApi from _generated/nodeApi instead of api, and Impl.make(nodeApi) to construct the implementation. Each function impl contains a handler that runs in the Node.js runtime, where you have access to NodeContext services, which are accessible via the @effect/platform package.
confect/node/email.impl.ts
import { FunctionImpl, GroupImpl } from "@confect/server";
import { Command } from "@effect/platform";
import { Console, Duration, Effect, Layer } from "effect";
import nodeApi from "../_generated/nodeApi";

const send = FunctionImpl.make(
  nodeApi,
  "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 const email = GroupImpl.make(nodeApi, "email").pipe(
  Layer.provide(send),
);
Your node impl must be the default export of a nodeImpl.ts file in your confect/ directory.
confect/nodeImpl.ts
import { Impl } from "@confect/server";
import { Layer } from "effect";
import nodeApi from "./_generated/nodeApi";
import { email } from "./node/email.impl";

export default Impl.make(nodeApi).pipe(
  Layer.provide(email),
  Impl.finalize,
);

Calling Node Actions

Node actions are exposed under the node namespace in your refs.
import { useAction } from "@confect/react";
import refs from "../confect/_generated/refs";

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

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