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.
| Path | When to use | Runs on |
|---|---|---|
| Server Component reads PrismaPrismaThe TypeScript ORM HiveCFM uses to talk to Postgres. The schema lives at packages/database/schema.prisma. directly | Rendering 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 route | Interactive 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.tshivecfm-core/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/actions.tshivecfm-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 usedfetch(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.
Read next
- Backend / Server Actions — the server side of what we just called.
- Backend / API routes — REST routes in depth.