> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noresponse.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Check account creation with a one-file template

> Copy one file into Better Auth, Firebase or Amazon Cognito. It assesses each new account with NoResponse and refuses the account creation when the decision is block.

Some auth providers cannot call a hosted endpoint. They run code that you deploy instead. For three of them, NoResponse publishes one file that you copy into your project. The file calls `POST /v1/assessments` with your API key and acts on the account's decision.

| Auth provider | Where the file runs | When it runs | On `block` | On `held` |
| - | - | - | - | - |
| Better Auth | A plugin in your auth server | Before Better Auth stores the user, for every account creation method | Refuses with 403 | Lets the user through |
| Firebase | A blocking function, `beforeUserCreated` | Before Firebase saves the user | Refuses the account creation | Creates the user disabled |
| Amazon Cognito | A Pre sign-up Lambda trigger | Before Cognito creates the user | Refuses the account creation | Lets the user through |

All three files work in the same way:

* **They fail open.** A missing key, an error, a slow answer or an unexpected reply lets the user through. The file writes one line to the log with the reason.
* **They refuse only on `block`.** The decision is `account.risk.decision`. `allow`, `step_up`, `review` and a 202 `pending` answer let the user through. Use [late decisions](https://docs.noresponse.org/guides/late-results.md) to act on them later.
* **They send no secret to another host.** The key goes only to an `https` URL, and the files follow no redirect.
* **They drop a refused optional value.** If the API refuses the IP, phone or name of the user, the file sends the request once more without that value.

Each file sends `auth_context` with `transport` `provider_hook`, so NoResponse knows which provider made the call. The file is the only code that you add. It needs no package beyond the SDK of your auth provider.

> [!NOTE]
>
> Every file needs a NoResponse API key with the `assess` scope. Use your test key (`nrk_test_`) first. Then the [magic emails](https://docs.noresponse.org/testing.md#magic-identifiers) give a known decision, and each file refuses `block@test.noresponse.org`.

## Better Auth

The plugin assesses each new user in `databaseHooks.user.create.before`. Better Auth calls this hook for email and password, social and magic link account creation. A refused user gets 403 with your message, and Better Auth stores no user. The plugin throws `APIError`, because a hook that returns `false` breaks social account creation.

Better Auth has no user id at this point. NoResponse thus gives the assessment an account id of its own, `pre:` and then the assessment id.

### 1. Add the key

Set `NORESPONSE_API_KEY` in the environment of your auth server.

### 2. Copy the plugin

Save `noresponse.ts` next to your Better Auth configuration.

> **noresponse.ts**
>
> ```ts title="noresponse.ts" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> /**
>  * NoResponse risk check at account creation, as a Better Auth plugin.
>  *
>  * Assesses every new user before Better Auth stores it, for every sign-up
>  * method (email and password, social, magic link), and refuses the sign-up
>  * only when the account's decision is "block". Any failure, slow answer or
>  * unexpected reply lets the user through, so NoResponse can never stop your
>  * users creating accounts on its own.
>  *
>  *   import { noresponse } from "./noresponse";
>  *   export const auth = betterAuth({ plugins: [noresponse()] });
>  *
>  * Environment: NORESPONSE_API_KEY (a key with the assess scope), and
>  * optionally NORESPONSE_BASE_URL. Copy this one file; it needs no package
>  * beyond better-auth.
>  */
> import type { BetterAuthOptions, BetterAuthPlugin } from "better-auth";
> import { APIError } from "better-auth/api";
>
> export interface NoResponseOptions {
>   /** Defaults to process.env.NORESPONSE_API_KEY. */
>   apiKey?: string;
>   /** Defaults to process.env.NORESPONSE_BASE_URL, then https://api.noresponse.org. */
>   baseUrl?: string;
>   /** How long a sign-up waits for NoResponse, in milliseconds. Default 3000, at most 8000. */
>   timeoutMs?: number;
>   /** What a refused user sees. */
>   message?: string;
> }
>
> const PROVIDER = "better-auth";
> const ISSUER = "https://better-auth.com";
> const DEFAULT_TIMEOUT_MS = 3000;
> const MAX_TIMEOUT_MS = 8000;
>
> export function noresponse(options: NoResponseOptions = {}): BetterAuthPlugin {
>   return {
>     id: "noresponse",
>     init(context) {
>       return {
>         options: {
>           databaseHooks: {
>             user: {
>               create: {
>                 // Throw to refuse: returning false breaks OAuth sign-ups
>                 // (better-auth/better-auth#11880).
>                 before: async (user, endpoint) => {
>                   const headers = endpoint?.request?.headers ?? endpoint?.headers;
>                   const verdict = await assess({
>                     apiKey: options.apiKey ?? process.env.NORESPONSE_API_KEY,
>                     baseUrl: options.baseUrl ?? process.env.NORESPONSE_BASE_URL,
>                     timeoutMs: options.timeoutMs,
>                     idempotencyKey: `nr-better-auth-${crypto.randomUUID()}`,
>                     body: {
>                       flow: "account_creation",
>                       identifiers: pick({ email: user.email, name: user.name?.trim(), ip: clientIP(headers, context.options) }),
>                       auth_context: { provider: PROVIDER, issuer: ISSUER, verified: false, transport: "provider_hook", hook_point: "pre_registration" },
>                       client_context: pick({ user_agent: headers?.get("user-agent")?.slice(0, 512) }),
>                     },
>                   });
>                   if (verdict.decision === "block") {
>                     throw new APIError("FORBIDDEN", { message: options.message ?? "We could not create your account. Please contact support." });
>                   }
>                 },
>               },
>             },
>           },
>         },
>       };
>     },
>   };
> }
>
> // The client IP, read the way Better Auth reads it for rate limits: the
> // configured headers (x-forwarded-for by default), and only a header that
> // holds one address, so a client cannot prepend an address of its choice.
> function clientIP(headers: Headers | undefined, auth: BetterAuthOptions): string | undefined {
>   const config = auth.advanced?.ipAddress;
>   if (!headers || config?.disableIpTracking) return undefined;
>   for (const name of config?.ipAddressHeaders ?? ["x-forwarded-for"]) {
>     const addresses = (headers.get(name) ?? "").split(",").map((part) => part.trim()).filter(Boolean);
>     if (addresses.length === 1) return addresses[0];
>   }
>   return undefined;
> }
>
> // ---- The NoResponse call. The Firebase and Cognito files hold the same logic. ----
>
> type Decision = "allow" | "step_up" | "review" | "block" | "held";
>
> interface Verdict {
>   decision?: Decision;
>   assessmentId?: string;
> }
>
> interface Call {
>   apiKey: string | undefined;
>   baseUrl: string | undefined;
>   timeoutMs: number | undefined;
>   idempotencyKey: string;
>   body: Record<string, unknown> & { identifiers: Record<string, unknown> };
> }
>
> // The members a request can do without. When the API refuses one of them
> // (a private IP, a phone that is not E.164), the call is sent once more
> // without it.
> const OPTIONAL = new Set(["identifiers.ip", "identifiers.name", "identifiers.phone"]);
>
> async function assess(call: Call): Promise<Verdict> {
>   if (!call.apiKey) return allowed("NORESPONSE_API_KEY is not set");
>   const url = `${(call.baseUrl || "https://api.noresponse.org").replace(/\/+$/, "")}/v1/assessments`;
>   if (!url.startsWith("https://") && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])[:/]/.test(url)) return allowed("base URL must be https");
>   const timeoutMs = Math.min(Math.max(1, Math.floor(call.timeoutMs ?? DEFAULT_TIMEOUT_MS)), MAX_TIMEOUT_MS);
>   const deadline = Date.now() + timeoutMs;
>   let body = call.body;
>   for (let attempt = 0; attempt < 2; attempt++) {
>     let response: Response;
>     let answer: any;
>     try {
>       response = await fetch(url, {
>         method: "POST",
>         headers: {
>           authorization: `Bearer ${call.apiKey}`,
>           "content-type": "application/json",
>           accept: "application/json",
>           "idempotency-key": attempt === 0 ? call.idempotencyKey : `${call.idempotencyKey}.2`,
>         },
>         body: JSON.stringify(body),
>         redirect: "error", // never resend the key to another host
>         signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
>       });
>       answer = await response.json().catch(() => undefined);
>     } catch (error) {
>       return allowed(error instanceof Error && error.name === "TimeoutError" ? `timeout after ${timeoutMs} ms` : "network error");
>     }
>     if (response.status === 200 || response.status === 202) {
>       const decision = answer?.account?.risk?.decision;
>       const assessmentId = typeof answer?.id === "string" ? answer.id : undefined;
>       if (!decision) return allowed(`no decision yet (status ${response.status})`, assessmentId);
>       console.log(`noresponse ${PROVIDER}: ${decision}, assessment ${assessmentId ?? "none"}`);
>       return { decision, assessmentId };
>     }
>     const refused: string[] = Array.isArray(answer?.invalid_params) ? answer.invalid_params.map((param: any) => param?.field) : [];
>     if (response.status !== 400 || attempt > 0 || refused.length === 0 || !refused.every((field) => OPTIONAL.has(field))) {
>       return allowed(`status ${response.status}`);
>     }
>     const identifiers = { ...body.identifiers };
>     for (const field of refused) delete identifiers[field.slice("identifiers.".length)];
>     body = { ...body, identifiers };
>   }
>   return allowed("refused twice");
> }
>
> function allowed(reason: string, assessmentId?: string): Verdict {
>   console.log(`noresponse ${PROVIDER}: allowed without a decision (${reason})${assessmentId ? `, assessment ${assessmentId}` : ""}`);
>   return { assessmentId };
> }
>
> function pick(values: Record<string, unknown>): Record<string, unknown> {
>   return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== ""));
> }
> ```

### 3. Add it to your configuration

```ts title="auth.ts" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { betterAuth } from "better-auth";
import { noresponse } from "./noresponse";

export const auth = betterAuth({
  // ...your database and methods
  plugins: [noresponse()],
});
```

The plugin reads the IP of the user from the headers that Better Auth uses for rate limits. That is `x-forwarded-for`, or `advanced.ipAddress.ipAddressHeaders` when you set it. A header with more than one address gives no IP. With `advanced.ipAddress.disableIpTracking`, the plugin sends no IP.

## Firebase

The blocking function runs before Firebase Authentication saves a new user. Blocking functions need Firebase Authentication with Identity Platform. Firebase allows a blocking function 7 seconds and refuses the account creation after that. The function thus stops the call to NoResponse after 5 seconds.

The function sends the Firebase user id as the `account_id`. On `held`, it creates the user disabled. Your late-result handler enables the user when the final decision is `allow`.

### 1. Add the key

```bash title="Store the key" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
firebase functions:secrets:set NORESPONSE_API_KEY
```

### 2. Copy the function

Save `noresponse.ts` in your functions folder and export the function from your entry file.

```ts title="index.ts" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
export { noresponseBeforeCreate } from "./noresponse";
```

> **noresponse.ts**
>
> ```ts title="noresponse.ts" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> /**
>  * NoResponse risk check at account creation, as a Firebase blocking function.
>  *
>  * Runs before Firebase Authentication saves a new user, for every sign-up
>  * method that triggers blocking functions. A "block" decision refuses the
>  * sign-up, and a "held" decision creates the user disabled, so the user
>  * cannot log in until your late-result handler enables it. Any failure,
>  * slow answer or unexpected reply lets the user through, so NoResponse can
>  * never stop your users creating accounts on its own.
>  *
>  * Blocking functions need Firebase Authentication with Identity Platform.
>  * Firebase allows the function 7 seconds and fails the sign-up after that,
>  * so the call to NoResponse stops after 5 seconds.
>  *
>  *   firebase functions:secrets:set NORESPONSE_API_KEY
>  *   export { noresponseBeforeCreate } from "./noresponse";
>  *
>  * Copy this one file; it needs no package beyond firebase-functions.
>  */
> import { createHash } from "node:crypto";
> import { defineSecret } from "firebase-functions/params";
> import { beforeUserCreated, HttpsError } from "firebase-functions/v2/identity";
>
> const apiKey = defineSecret("NORESPONSE_API_KEY");
>
> const PROVIDER = "firebase";
> const DEFAULT_TIMEOUT_MS = 5000;
> const MAX_TIMEOUT_MS = 6500;
>
> export const noresponseBeforeCreate = beforeUserCreated({ secrets: [apiKey] }, async (event) => {
>   const user = event.data;
>   if (!user) return;
>   const verdict = await assess({
>     apiKey: apiKey.value(),
>     baseUrl: process.env.NORESPONSE_BASE_URL,
>     timeoutMs: Number(process.env.NORESPONSE_TIMEOUT_MS) || undefined,
>     // A retry of the same event gets the same assessment.
>     idempotencyKey: `nr-firebase-${createHash("sha256").update(event.eventId).digest("hex").slice(0, 40)}`,
>     body: {
>       account_id: user.uid,
>       flow: "account_creation",
>       identifiers: pick({ email: user.email, phone: user.phoneNumber, name: user.displayName?.trim(), ip: event.ipAddress }),
>       auth_context: {
>         provider: PROVIDER,
>         issuer: `https://securetoken.google.com/${process.env.GCLOUD_PROJECT ?? ""}`,
>         verified: false,
>         transport: "provider_hook",
>         hook_point: "pre_registration",
>       },
>       client_context: pick({ user_agent: event.userAgent?.slice(0, 512), locale: event.locale }),
>     },
>   });
>   if (verdict.decision === "block") {
>     throw new HttpsError("permission-denied", "We could not create your account. Please contact support.");
>   }
>   if (verdict.decision === "held") {
>     return { disabled: true };
>   }
>   return;
> });
>
> // ---- The NoResponse call. The Better Auth and Cognito files hold the same logic. ----
>
> type Decision = "allow" | "step_up" | "review" | "block" | "held";
>
> interface Verdict {
>   decision?: Decision;
>   assessmentId?: string;
> }
>
> interface Call {
>   apiKey: string | undefined;
>   baseUrl: string | undefined;
>   timeoutMs: number | undefined;
>   idempotencyKey: string;
>   body: Record<string, unknown> & { identifiers: Record<string, unknown> };
> }
>
> // The members a request can do without. When the API refuses one of them
> // (a private IP, a phone that is not E.164), the call is sent once more
> // without it.
> const OPTIONAL = new Set(["identifiers.ip", "identifiers.name", "identifiers.phone"]);
>
> async function assess(call: Call): Promise<Verdict> {
>   if (!call.apiKey) return allowed("NORESPONSE_API_KEY is not set");
>   const url = `${(call.baseUrl || "https://api.noresponse.org").replace(/\/+$/, "")}/v1/assessments`;
>   if (!url.startsWith("https://") && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])[:/]/.test(url)) return allowed("base URL must be https");
>   const timeoutMs = Math.min(Math.max(1, Math.floor(call.timeoutMs ?? DEFAULT_TIMEOUT_MS)), MAX_TIMEOUT_MS);
>   const deadline = Date.now() + timeoutMs;
>   let body = call.body;
>   for (let attempt = 0; attempt < 2; attempt++) {
>     let response: Response;
>     let answer: any;
>     try {
>       response = await fetch(url, {
>         method: "POST",
>         headers: {
>           authorization: `Bearer ${call.apiKey}`,
>           "content-type": "application/json",
>           accept: "application/json",
>           "idempotency-key": attempt === 0 ? call.idempotencyKey : `${call.idempotencyKey}.2`,
>         },
>         body: JSON.stringify(body),
>         redirect: "error", // never resend the key to another host
>         signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
>       });
>       answer = await response.json().catch(() => undefined);
>     } catch (error) {
>       return allowed(error instanceof Error && error.name === "TimeoutError" ? `timeout after ${timeoutMs} ms` : "network error");
>     }
>     if (response.status === 200 || response.status === 202) {
>       const decision = answer?.account?.risk?.decision;
>       const assessmentId = typeof answer?.id === "string" ? answer.id : undefined;
>       if (!decision) return allowed(`no decision yet (status ${response.status})`, assessmentId);
>       console.log(`noresponse ${PROVIDER}: ${decision}, assessment ${assessmentId ?? "none"}`);
>       return { decision, assessmentId };
>     }
>     const refused: string[] = Array.isArray(answer?.invalid_params) ? answer.invalid_params.map((param: any) => param?.field) : [];
>     if (response.status !== 400 || attempt > 0 || refused.length === 0 || !refused.every((field) => OPTIONAL.has(field))) {
>       return allowed(`status ${response.status}`);
>     }
>     const identifiers = { ...body.identifiers };
>     for (const field of refused) delete identifiers[field.slice("identifiers.".length)];
>     body = { ...body, identifiers };
>   }
>   return allowed("refused twice");
> }
>
> function allowed(reason: string, assessmentId?: string): Verdict {
>   console.log(`noresponse ${PROVIDER}: allowed without a decision (${reason})${assessmentId ? `, assessment ${assessmentId}` : ""}`);
>   return { assessmentId };
> }
>
> function pick(values: Record<string, unknown>): Record<string, unknown> {
>   return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== ""));
> }
> ```

### 3. Deploy and register it

Run `firebase deploy --only functions`. Then open **Authentication**, then **Settings**, then **Blocking functions**, and select `noresponseBeforeCreate` for **Before account creation**.

## Amazon Cognito

The trigger runs before Cognito creates a new user. Cognito allows a trigger 5 seconds and can try it again. The trigger thus stops the call to NoResponse after 4 seconds, and each new try gets the same assessment. The trigger does not assess a user that an administrator creates with `AdminCreateUser`.

Cognito cannot create a user disabled from this trigger. On `held`, the trigger thus lets the user through. Your late-result handler disables the user with `AdminDisableUser` when the final decision is `block`.

The Cognito event carries no client IP. To send one, put it in `ClientMetadata` as `noresponse_ip` on the `SignUp` call from your backend.

### 1. Deploy the function

Put `pre-sign-up.mjs` and `template.yaml` in one folder. Then deploy them with the AWS SAM CLI and give the ARN of your user pool and your key.

```bash title="Deploy" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
sam deploy --guided --template-file template.yaml
```

> **pre-sign-up.mjs**
>
> ```js title="pre-sign-up.mjs" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> /**
>  * NoResponse risk check at account creation, as an Amazon Cognito Pre
>  * sign-up Lambda trigger (Node.js 22).
>  *
>  * Runs before Cognito creates a self-service or federated user. A "block"
>  * decision refuses the sign-up. Cognito cannot create a user disabled from
>  * this trigger, so "held" lets the user through and your late-result handler
>  * disables it (AdminDisableUser) if the final decision is block. Any failure,
>  * slow answer or unexpected reply lets the user through, so NoResponse can
>  * never stop your users creating accounts on its own. Users that an
>  * administrator creates (AdminCreateUser) are not assessed.
>  *
>  * Cognito allows the trigger 5 seconds and may retry it, so the call to
>  * NoResponse stops after 4 seconds, and a retry gets the same assessment.
>  * The event carries no client IP: to send one, pass it from your backend as
>  * ClientMetadata { "noresponse_ip": "..." } on the SignUp call.
>  *
>  * Environment: NORESPONSE_API_KEY (a key with the assess scope), and
>  * optionally NORESPONSE_BASE_URL and NORESPONSE_TIMEOUT_MS.
>  * Copy this one file; it needs no package.
>  */
> import { createHash } from "node:crypto";
>
> const PROVIDER = "cognito";
> const DEFAULT_TIMEOUT_MS = 4000;
> const MAX_TIMEOUT_MS = 4500;
>
> export const handler = async (event) => {
>   if (event.triggerSource === "PreSignUp_AdminCreateUser") return event;
>   const attributes = event.request?.userAttributes ?? {};
>   const verdict = await assess({
>     apiKey: process.env.NORESPONSE_API_KEY,
>     baseUrl: process.env.NORESPONSE_BASE_URL,
>     timeoutMs: Number(process.env.NORESPONSE_TIMEOUT_MS) || undefined,
>     // Cognito's retries of one sign-up get the same assessment.
>     idempotencyKey: `nr-cognito-${createHash("sha256").update(`${event.userPoolId}/${event.userName}`).digest("hex").slice(0, 40)}`,
>     body: {
>       account_id: event.userName,
>       flow: "account_creation",
>       identifiers: pick({
>         email: attributes.email,
>         phone: attributes.phone_number,
>         name: attributes.name?.trim(),
>         ip: event.request?.clientMetadata?.noresponse_ip,
>       }),
>       auth_context: {
>         provider: PROVIDER,
>         issuer: `https://cognito-idp.${event.region}.amazonaws.com/${event.userPoolId}`,
>         verified: false,
>         transport: "provider_hook",
>         hook_point: "pre_registration",
>       },
>     },
>   });
>   if (verdict.decision === "block") {
>     throw new Error("We could not create your account. Please contact support.");
>   }
>   return event;
> };
>
> // ---- The NoResponse call. The Better Auth and Firebase files hold the same logic. ----
>
> // The members a request can do without. When the API refuses one of them
> // (a private IP, a phone that is not E.164), the call is sent once more
> // without it.
> const OPTIONAL = new Set(["identifiers.ip", "identifiers.name", "identifiers.phone"]);
>
> async function assess(call) {
>   if (!call.apiKey) return allowed("NORESPONSE_API_KEY is not set");
>   const url = `${(call.baseUrl || "https://api.noresponse.org").replace(/\/+$/, "")}/v1/assessments`;
>   if (!url.startsWith("https://") && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])[:/]/.test(url)) return allowed("base URL must be https");
>   const timeoutMs = Math.min(Math.max(1, Math.floor(call.timeoutMs ?? DEFAULT_TIMEOUT_MS)), MAX_TIMEOUT_MS);
>   const deadline = Date.now() + timeoutMs;
>   let body = call.body;
>   for (let attempt = 0; attempt < 2; attempt++) {
>     let response;
>     let answer;
>     try {
>       response = await fetch(url, {
>         method: "POST",
>         headers: {
>           authorization: `Bearer ${call.apiKey}`,
>           "content-type": "application/json",
>           accept: "application/json",
>           "idempotency-key": attempt === 0 ? call.idempotencyKey : `${call.idempotencyKey}.2`,
>         },
>         body: JSON.stringify(body),
>         redirect: "error", // never resend the key to another host
>         signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
>       });
>       answer = await response.json().catch(() => undefined);
>     } catch (error) {
>       return allowed(error instanceof Error && error.name === "TimeoutError" ? `timeout after ${timeoutMs} ms` : "network error");
>     }
>     if (response.status === 200 || response.status === 202) {
>       const decision = answer?.account?.risk?.decision;
>       const assessmentId = typeof answer?.id === "string" ? answer.id : undefined;
>       if (!decision) return allowed(`no decision yet (status ${response.status})`, assessmentId);
>       console.log(`noresponse ${PROVIDER}: ${decision}, assessment ${assessmentId ?? "none"}`);
>       return { decision, assessmentId };
>     }
>     const refused = Array.isArray(answer?.invalid_params) ? answer.invalid_params.map((param) => param?.field) : [];
>     if (response.status !== 400 || attempt > 0 || refused.length === 0 || !refused.every((field) => OPTIONAL.has(field))) {
>       return allowed(`status ${response.status}`);
>     }
>     const identifiers = { ...body.identifiers };
>     for (const field of refused) delete identifiers[field.slice("identifiers.".length)];
>     body = { ...body, identifiers };
>   }
>   return allowed("refused twice");
> }
>
> function allowed(reason, assessmentId) {
>   console.log(`noresponse ${PROVIDER}: allowed without a decision (${reason})${assessmentId ? `, assessment ${assessmentId}` : ""}`);
>   return { assessmentId };
> }
>
> function pick(values) {
>   return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== ""));
> }
> ```

> **template.yaml**
>
> ```yaml title="template.yaml" theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> # Deploys pre-sign-up.mjs as a Pre sign-up trigger function and lets one user
> # pool invoke it. Attach it to the pool afterwards (see README.md).
> #   sam deploy --guided --template-file template.yaml
> AWSTemplateFormatVersion: "2010-09-09"
> Transform: AWS::Serverless-2016-10-31
> Description: NoResponse risk check at account creation for an Amazon Cognito user pool.
>
> Parameters:
>   UserPoolArn:
>     Type: String
>     Description: ARN of the user pool that calls this trigger.
>   NoResponseApiKey:
>     Type: String
>     NoEcho: true
>     Description: A NoResponse API key with the assess scope.
>
> Resources:
>   PreSignUpFunction:
>     Type: AWS::Serverless::Function
>     Properties:
>       CodeUri: .
>       Handler: pre-sign-up.handler
>       Runtime: nodejs22.x
>       Architectures: [arm64]
>       # Cognito waits 5 s for a trigger; the NoResponse call stops after 4 s.
>       Timeout: 5
>       MemorySize: 256
>       Environment:
>         Variables:
>           NORESPONSE_API_KEY: !Ref NoResponseApiKey
>
>   CognitoInvokePermission:
>     Type: AWS::Lambda::Permission
>     Properties:
>       Action: lambda:InvokeFunction
>       FunctionName: !GetAtt PreSignUpFunction.Arn
>       Principal: cognito-idp.amazonaws.com
>       SourceArn: !Ref UserPoolArn
>
> Outputs:
>   PreSignUpFunctionArn:
>     Description: Select this function as the Pre sign-up trigger of the user pool.
>     Value: !GetAtt PreSignUpFunction.Arn
> ```

### 2. Attach the trigger

In the Cognito console, open your user pool, then **Extensions**, then **Add Lambda trigger**. Select **Sign-up**, then **Pre sign-up trigger**, and select the function.

> [!WARNING]
>
> Do not attach the trigger with `aws cognito-idp update-user-pool` alone. That command sets every setting that you do not include back to its default.

## Tests

Each file has tests in its own folder of the NoResponse repository. They run the file with the real SDK of its auth provider against a scripted NoResponse endpoint. The tests cover each decision, each failure and the retry without a refused value.

| Template | Checked against NoResponse |
| - | - |
| Better Auth | A real Better Auth server. It refused `block@test.noresponse.org` and created `allow@test.noresponse.org`. |
| Firebase | The Firebase Authentication and Functions emulators. They refused `block@test.noresponse.org` and created `allow@test.noresponse.org`. |
| Amazon Cognito | The tests only. We did not run it in a Cognito user pool yet. |

> **[Admission and late decisions](https://docs.noresponse.org/guides/late-results.md)**
>
> Act on a decision that changes after the account exists.

> **[Test your integration](https://docs.noresponse.org/testing.md)**
>
> The magic emails that give each decision with a test key.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.