Docs
Docs
Guides

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.

What to read next

Was this helpful?
Edit this page