BackendAPI Routes

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 receivers

Versioning — 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 v1 endpoint for a bugfix, stay in v1.
  • 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 endpoints

The 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 prefixHow auth worksTypical 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:

  1. Parse the JSON body. If request.json() throws, return 400 with the parse error.
  2. Validate environmentId against ZEnvironmentId (a zod schema).
  3. Validate the response body against ZResponseInputV2 — zod returns structured field-level errors that the handler passes back in a 400.
  4. Enrich from headers — user agent (UAParser), country (CF-IPCountry / X-Vercel-IP-Country / CloudFront-Viewer-Country).
  5. Authz checks — feature gate for contact identification, survey validity, license validity, quota.
  6. Create the response via createResponseWithQuotaEvaluation (Prisma write + quota recount).
  7. Enqueue the pipelinesendToPipeline posts a job that will fan out to the Hub for embeddings, sentiment, webhooks.
  8. 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.