API Routes
REST endpoints live under hivecfm-core/apps/web/app/api/. The folder path is the URL. app/api/v2/client/[environmentId]/responses/route.ts serves POST /api/v2/client/<env-id>/responses.
Top-level layout
apps/web/app/api/
├── (internal)/ # Routes that should never be exposed publicly
├── auth/ # NextAuth route handler
├── billing/ # Stripe webhooks, portal redirects
├── cron/ # Scheduled endpoints (Azure scheduler hits these)
├── health/ # Liveness probe
├── ready/ # Readiness probe (checks DB, Redis)
├── v1/ # First-gen REST surface (legacy but supported)
├── v2/ # Current REST surface
├── oidc/ # OIDC discovery
├── integrations/ # Third-party OAuth callbacks
└── webhooks/ # Incoming webhook receiversVersioning — v1 vs v2
HiveCFM keeps both v1 and v2 online. New work goes to v2. v1 is preserved for backward compatibility with self-hosted SDKs and existing integrations.
Pragmatic rule of thumb:
- If you are building a new endpoint, put it under
app/api/v2/.... - If you are touching an existing
v1endpoint for a bugfix, stay inv1. - A breaking change to the shape of an endpoint bumps its version, not the whole API.
The openapi.yml at the repo root describes both versions — it is the single source of truth and is mirrored into the dev hub (see API Explorer).
Convention: the route.ts handler
Every route is a single file that exports one function per HTTP verb:
export const GET = async (req, ctx) => { … };
export const POST = async (req, ctx) => { … };
export const PUT = async (req, ctx) => { … };
export const DELETE = async (req, ctx) => { … };
export const OPTIONS = async () => { … }; // CORS preflight for public endpointsThe ctx argument holds params (a Promise in Next.jsNext.jsReact framework used by HiveCFM Core. Handles routing, server rendering, and API routes in one bundle. 14) for dynamic segments like [environmentId].
Auth patterns by route family
| Route prefix | How auth works | Typical caller |
|---|---|---|
api/v2/client/[environmentId]/... | Environment ID in URL; no user session. Public — the SurveySurveyThe core HiveCFM object. A definition of questions, logic, and targeting — shown to respondents to collect feedback. Widget calls these. | Browser / SDK |
api/v2/management/... | API key in x-api-key header. | Server-to-server scripts |
api/v1/management/... | API key (legacy header layout). | Existing integrations |
api/auth/... | NextAuthNextAuthThe auth library HiveCFM Core uses to handle sessions, OAuth providers, and credentials. session cookie. | Logged-in UI |
api/cron/... | Bearer token matching CRON_SECRET. | Azure scheduler |
api/webhooks/<provider> | Provider-specific HMAC signature in a header. | Stripe, Genesys, Novu, etc. |
There is no global auth middleware that silently gates everything — each route resolves its own auth explicitly at the top. This makes the auth check visible in every handler and is the convention you should follow.
Canonical walkthrough — POST /api/v2/client/:env/responses
File: hivecfm-core/apps/web/app/api/v2/client/[environmentId]/responses/route.ts
This is the endpoint the SurveySurveyThe core HiveCFM object. A definition of questions, logic, and targeting — shown to respondents to collect feedback. Widget calls every time a respondent submits. It is the single most load-bearing write path in the product.
The handler runs these steps, in order:
- Parse the JSON body. If
request.json()throws, return 400 with the parse error. - Validate
environmentIdagainstZEnvironmentId(a zod schema). - Validate the response body against
ZResponseInputV2— zod returns structured field-level errors that the handler passes back in a 400. - Enrich from headers — user agent (
UAParser), country (CF-IPCountry/X-Vercel-IP-Country/CloudFront-Viewer-Country). - Authz checks — feature gate for contact identification, survey validity, license validity, quota.
- Create the response via
createResponseWithQuotaEvaluation(Prisma write + quota recount). - Enqueue the pipeline —
sendToPipelineposts a job that will fan out to the Hub for embeddings, sentiment, webhooks. - Return the created response with
responses.successResponse.
Notice what is not here: no DB-connection-open code, no threading, no DI resolution. The handler imports pure functions from the @hivecfm/* packages and composes them. This is the shape every route follows.
Validation — zod, not DataAnnotations
Every input is validated with a zod schema from @hivecfm/types. A zod schema is both:
- the runtime validator (
.safeParse(...)), and - the TypeScript type (
type T = z.infer<typeof Schema>).
If you are coming from FluentValidation, the mental model is very similar — declarative, chainable — except the zod schema is the C# DTO. No separate class.
Response helpers
Stop writing new Response(JSON.stringify(...), { status: 400 }). Use the helpers in apps/web/app/lib/api/response.ts:
responses.successResponse(data)
responses.badRequestResponse(message, details, cors)
responses.forbiddenResponse(message, cors)
responses.notFoundResponse(resource, id, cors)They set content type, optional CORS, and keep the envelope consistent across routes.
Every public POST route needs an OPTIONS export for CORS preflight. If you skip it, browsers will refuse the call with an opaque “CORS error” and you will spend an hour chasing it.
Read next
- Server Actions — the in-app write path, not reachable from outside.
- Reference / API Explorer — browse the openapi spec interactively.