FrontendState and Data

State and Data

There are three ways data moves between the browser and Postgres in HiveCFM. Pick the right one for the shape of the work.

PathWhen to useRuns on
Server Component reads PrismaPrismaThe TypeScript ORM HiveCFM uses to talk to Postgres. The schema lives at packages/database/schema.prisma. directlyRendering a page.Server (Node)
Server Action ("use server")Form submits and mutations triggered from the UI.Server (Node)
API route under app/api/...Third-party callers, the SurveySurveyThe core HiveCFM object. A definition of questions, logic, and targeting — shown to respondents to collect feedback. Widget, webhooks, mobile SDK.Server (Node)
Client fetch to an API routeInteractive widgets that need to re-read data without a nav.Client → Server

There is no GraphQL, no tRPC — the primitives above carry everything.

Reading on the server

The default pattern is: server component, PrismaPrismaThe TypeScript ORM HiveCFM uses to talk to Postgres. The schema lives at packages/database/schema.prisma. call, render. No fetch, no JSON, no CORS.

// app/(app)/environments/[environmentId]/surveys/page.tsx (shape)
import { getSurveys } from "@/lib/survey/service";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
 
export default async function Page({ params }) {
  const { environmentId } = await params;
  await getEnvironmentAuth(environmentId);       // auth + authz
  const surveys = await getSurveys(environmentId); // Prisma under the hood
  return <SurveyList surveys={surveys} />;
}

SurveyList is a client component that takes surveys as props. The data crossed the server/client boundary exactly once, as serialised JSON in the HTML stream.

The .NET parallel: this is what an ASP.NET Razor page would do — read in the handler, render the view. The difference is that SurveyList can also re-render on the client as state changes, without another trip to the server.

Writing with Server Actions

A Server Action is a function you can call from a client component as if it were local. The framework turns it into a POST under the hood. Mark the file with "use server" at the top:

// app/(app)/environments/[environmentId]/actions.ts
"use server";
 
import { revalidatePath } from "next/cache";
import { archiveSurvey } from "@/lib/survey/service";
 
export async function archiveSurveyAction(surveyId: string) {
  await archiveSurvey(surveyId);
  revalidatePath("/environments/[environmentId]/surveys", "page");
}

Client side:

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

Real examples in the repo:

  • hivecfm-core/apps/web/app/(app)/environments/[environmentId]/actions.ts
  • hivecfm-core/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/actions.ts
  • hivecfm-core/apps/web/app/setup/organization/create/actions.ts

Server Actions are roughly analogous to an ASP.NET Core endpoint auto-bound to the page — no routing, no DTOs, no HTTP client. See Backend / Server Actions for the full story, including authorisation.

Writing with API routes

When the caller is not the HiveCFM web UI — a JS SDK, a mobile app, the SurveySurveyThe core HiveCFM object. A definition of questions, logic, and targeting — shown to respondents to collect feedback. Widget, a webhook receiver — an API route is the right seam. Routes live under app/api/... and export HTTP verb handlers:

// app/api/v2/client/[environmentId]/responses/route.ts (excerpt)
export const POST = async (request: Request, context: Context): Promise<Response> => {
  const params = await context.params;
  const responseInput = await request.json();
  const parsed = ZResponseInputV2.safeParse({ ...responseInput, environmentId: params.environmentId });
  if (!parsed.success) return responses.badRequestResponse(...);
  // …create response, enqueue pipeline job, return 201
};

See the full walkthrough in Backend / API routes.

Client fetch — the escape hatch

When a client component needs to re-read without a full nav (e.g. a polling dashboard widget), it calls an API route with plain fetch:

const res = await fetch(`/api/v1/environments/${envId}/responses/latest`);
const data = await res.json();

This is the only pattern where the browser talks to your own backend over HTTP. Use it sparingly — if the data changes with a user action, a Server Action + revalidatePath is cleaner.

Cache invalidation

Next.jsNext.jsReact framework used by HiveCFM Core. Handles routing, server rendering, and API routes in one bundle. caches server-rendered pages aggressively. After a mutation:

  • revalidatePath('/environments/[environmentId]/surveys', 'page') — re-renders on next view.
  • revalidateTag('surveys') — if the read used fetch(url, { next: { tags: ['surveys'] } }).
  • router.refresh() (client side) — re-runs the current route’s server component.

The .NET analogy is a response cache with a manual EvictByTag hook — the framework handles the invalidation once you name what changed.