BackendServer Actions

Server Actions

A Server Action is a function that runs on the server but is invoked from a client component as if it were a local function call. The Next.jsNext.jsReact framework used by HiveCFM Core. Handles routing, server rendering, and API routes in one bundle. compiler rewrites the call into a signed POST under the hood.

If you squint, a Server Action is roughly analogous to an ASP.NET Core minimal-API endpoint that is auto-bound to the calling page — no routing to configure, no DTO to wire, no HTTP client on the client. You just import the function and call it.

The mechanics

Two pieces mark a function as a Server Action:

  1. The directive "use server" at the top of the file (or inside the function).
  2. An async exported function.
// app/(app)/.../actions.ts
"use server";
 
export async function archiveSurvey(id: string) {
  // runs on the Node server
}

On the client:

"use client";
import { archiveSurvey } from "./actions";
 
<button onClick={() => archiveSurvey(id)}>Archive</button>

The Next.jsNext.jsReact framework used by HiveCFM Core. Handles routing, server rendering, and API routes in one bundle. compiler sees the cross-boundary import, strips the function body out of the client bundle, and replaces the call with a fetch to an internal endpoint.

When to use a Server Action vs an API route

You want to…Use
Submit a form or button click from the HiveCFM UIServer Action
Accept a POST from the SurveySurveyThe core HiveCFM object. A definition of questions, logic, and targeting — shown to respondents to collect feedback. Widget or SDKAPI route under api/v2/client/...
Accept a request from a third party, a partner, or a webhookAPI route
Return data that needs to be cached by an external CDNAPI route
Mutate DB + invalidate the current pageServer Action (call revalidatePath)

Rule of thumb: if the caller is HiveCFM itself, use a Server Action; if the caller is anything else, use an API route.

The HiveCFM convention — authenticatedActionClient

Raw "use server" functions work, but HiveCFM wraps them in an action client that handles auth, zod validation, and structured error return. Pattern:

// apps/web/app/(app)/environments/[environmentId]/actions.ts
"use server";
 
import { z } from "zod";
import { ZId } from "@hivecfm/types/common";
import { ZProjectUpdateInput } from "@hivecfm/types/project";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
 
const ZCreateProjectAction = z.object({
  organizationId: ZId,
  data: ZProjectUpdateInput,
});
 
export const createProjectAction = authenticatedActionClient
  .schema(ZCreateProjectAction)
  .action(
    withAuditLogging("created", "project", async ({ ctx, parsedInput }) => {
      // ctx.user is guaranteed non-null here
      await checkAuthorizationUpdated({ userId: ctx.user.id, ... });
      return createProject(parsedInput.organizationId, parsedInput.data);
    }),
  );

What the chain buys you:

  • authenticatedActionClient — rejects the call with a 401 if the NextAuthNextAuthThe auth library HiveCFM Core uses to handle sessions, OAuth providers, and credentials. session is missing, and populates ctx.user.
  • .schema(...) — zod validates the input; invalid calls short-circuit with field-level errors.
  • .action(...) — the business logic, with a typed parsedInput.
  • withAuditLogging(...) — writes an audit row for enterprise plans.
  • checkAuthorizationUpdated(...) — RBAC: does this user have permission on this organisation/environment?

This is the .NET analogue of stacking [Authorize], [ValidateModel], and an audit filter on a controller action — except it is composed with plain function wrappers instead of attributes.

Real examples in the repo

Grep "use server" in hivecfm-core/apps/web/ to find them all. A handful worth reading:

  • apps/web/app/(app)/environments/[environmentId]/actions.ts — create project, update organisation.
  • apps/web/app/(app)/environments/[environmentId]/workspace/integrations/actions.ts — connect/disconnect integrations.
  • apps/web/app/setup/organization/create/actions.ts — the first-run organisation bootstrap.
  • apps/web/app/(app)/(onboarding)/lib/onboarding.ts — onboarding step transitions.

Cache invalidation after a mutation

An action that changes data should tell Next.jsNext.jsReact framework used by HiveCFM Core. Handles routing, server rendering, and API routes in one bundle. what to re-render:

import { revalidatePath, revalidateTag } from "next/cache";
 
// Re-render every instance of this route pattern
revalidatePath("/environments/[environmentId]/surveys", "page");
 
// Or: invalidate everything tagged "surveys" (if the read used fetch with a tag)
revalidateTag("surveys");

Without the call, the server component will still render yesterday’s data because Next.jsNext.jsReact framework used by HiveCFM Core. Handles routing, server rendering, and API routes in one bundle. cached the render.

⚠️

Never put sensitive branching on a value the client sends. Actions run on the server, but the client still provides the arguments. Always re-check authorisation inside the action, even if the button is only shown to admins in the UI.