# 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.

```ts check="origin"
// 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:

```ts
// 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();
  }
}
```

```ts
// 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](/docs/webhooks#verify-the-signature) page.

```ts
// 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](/docs/quickstart) with a test key to watch a domain walk to `active`, and use the [sandbox origin](/docs/guides/test-mode) to see the headers your app will receive.

- [Webhooks](/docs/webhooks)
- [Test mode](/docs/guides/test-mode)
- [Next.js guide](/docs/guides/nextjs)
