NestJS
Serve every customer domain from one NestJS app with a middleware that checks the Apexly signature and resolves the tenant.
How requests arrive
Apexly forwards each visitor's request to your origin with the original Host header, X-Forwarded-Proto: https, and X-Apexly-Domain-Id, X-Apexly-App-Id, X-Apexly-Timestamp and X-Apexly-Signature. Map the host to a tenant, and refuse requests for hosts you don't serve yourself unless Apexly signed them.
Verify the signature
X-Apexly-Signature is the hex HMAC-SHA256 of <timestamp>.<host> keyed with your signing secret (whsec_…). This helper is tested against the shared vectors on every build.
// src/apexly/verify-origin.ts
import { createHmac, timingSafeEqual } from "node:crypto";
/** True when X-Apexly-Signature is valid for this Host (the edge signs `${timestamp}.${host}`). */
export function verifyApexlyOrigin(
host: string,
timestamp: string | undefined,
signature: string | undefined,
secret: string,
now = Math.floor(Date.now() / 1000),
): boolean {
if (!timestamp || !signature || !/^\d{1,12}$/.test(timestamp)) return false;
if (Math.abs(now - Number(timestamp)) > 300) return false;
const name = host.replace(/:\d+$/, "").toLowerCase();
const expected = createHmac("sha256", secret).update(`${timestamp}.${name}`).digest();
const got = Buffer.from(signature, "hex");
return got.length === expected.length && timingSafeEqual(got, expected);
}A middleware runs it before every route and puts the tenant on the request:
// src/apexly/tenant.middleware.ts
import { Injectable, MisdirectedException, NestMiddleware } from "@nestjs/common";
import type { NextFunction, Request, Response } from "express";
import { TenantsService } from "../tenants/tenants.service";
import { verifyApexlyOrigin } from "./verify-origin";
const OWN_HOSTS = new Set(["app.hireflow.com", "localhost:3000"]);
@Injectable()
export class TenantMiddleware implements NestMiddleware {
constructor(private readonly tenants: TenantsService) {}
async use(req: Request, _res: Response, next: NextFunction) {
const host = req.headers.host ?? "";
if (!OWN_HOSTS.has(host)) {
const ok = verifyApexlyOrigin(
host,
req.header("x-apexly-timestamp"),
req.header("x-apexly-signature"),
process.env.APEXLY_SIGNING_SECRET!,
);
if (!ok) throw new MisdirectedException("Unknown host");
req["tenant"] = await this.tenants.byHost(host.replace(/:\d+$/, ""));
}
next();
}
}// src/app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TenantMiddleware).forRoutes("*");
}
}Allow your customers' hostnames
NestJS does not check the Host header, so no allow-list is needed. If you read req.protocol or build absolute URLs, trust one proxy hop so X-Forwarded-Proto is honoured: app.set("trust proxy", 1) on the Express adapter.
Receive webhooks
Webhook bodies must be verified before they are parsed. Create the app with rawBody: true and verify req.rawBody with verifyApexlySignature from the Webhooks page.
// src/apexly/webhooks.controller.ts
@Controller("webhooks/apexly")
export class ApexlyWebhooksController {
@Post()
@HttpCode(204)
receive(@Req() req: RawBodyRequest<Request>, @Headers("apexly-signature") sig?: string) {
if (!verifyApexlySignature(sig, req.rawBody!, process.env.APEXLY_SIGNING_SECRET!)) {
throw new BadRequestException();
}
const event = JSON.parse(req.rawBody!.toString("utf8"));
if (event.type === "domain.active") this.tenants.markLive(event.data.domain.metadata.customer);
}
}Try it
Run the Quickstart with a test key to watch a domain walk to active, and use the sandbox origin to see the headers your app will receive.