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

# Protect an action

> Apply your policy to a prepared browser measurement and complete an action once.

The application SDK handles preparation, waiting, challenges and continuation. Your backend supplies the authenticated account, trusted request address, eligibility rule and transactional write.

Use Node 22+ and PostgreSQL 14+. The [complete Next.js example](/integration/nextjs-postgres) includes all files and local fixtures.

## Install

```sh theme={"dark"}
npm install https://heretic.tech/downloads/heretic-hq-integration-0.1.3.tgz pg
```

Set `HERETIC_SITE_KEY`, `HERETIC_SECRET_KEY`, `APP_ORIGIN` and `DATABASE_URL` in the server environment or a local `.env` file. Verify the site's domain in the dashboard.

```sh theme={"dark"}
npx heretic-migrate
npx heretic-check
```

The migration adds integration tables to your database. The check command validates configuration, server credential access, public verification keys and database connectivity without creating a measurement or ceremony. The package README ships with the API definitions and retention operations.

## Connect the server

`createIntegration` from `@heretic-hq/integration/server` accepts:

| Option                              | Your application supplies                                                                                                                   |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `siteKey`, `secretKey`, `appOrigin` | Server configuration.                                                                                                                       |
| `store`                             | `new PostgresStore(pool)` from `@heretic-hq/integration/postgres`, using your `pg.Pool`.                                                    |
| `resolveContext(request)`           | `{ accountId, sessionId, ip }` from your authenticated session and trusted ingress. Anonymous preparation can use null account/session IDs. |
| `actions`                           | Named action definitions with validation, eligibility and commit functions.                                                                 |

Mount `handle(request)` behind `/api/heretic/*`, forwarding a standard Request and returning its Response. The returned `GET` and `POST` handlers can also be mounted directly in a Next.js catch-all route with `runtime = 'nodejs'`.

The IP resolver must match your deployment. Do not read an arbitrary forwarding header unless your ingress overwrites it and direct origin access is restricted appropriately. The adapter does not assume a particular CDN. It binds the signed measurement to your application session by default, using a nonce captured when measurement starts. To require an additional address or request relation, supply `requestBinding({device, context, phase})`; it must return `true`. The application and sensor may observe different IP families. Signature, freshness, session binding and account checks always apply.

Every public route that can perform the protected write must use the same server guard. When replacing an existing endpoint, remove its unguarded write path or route it through the adapter. Changing only the frontend submission leaves an older API callable directly.

## Define the action

Each action has these functions:

| Function                   | Contract                                                                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `validate(input)`          | Optional. Validate and normalize the submitted JSON input. Throw `IntegrationError` with a stable code for invalid input. |
| `resource(input, context)` | Return the shared eligibility/allocation key. Contenders for the same limited resource must use the same key.             |
| `admit(context)`           | Optional. Apply cheap existing account/input checks before creating a pending attempt.                                    |
| `policy(context)`          | Return `{ decision: 'allow', 'challenge', or 'deny', reason? }`. Runs before the write and again after a ceremony.        |
| `commit(context)`          | Write using `context.tx` and return a JSON receipt. Runs only after an allow decision.                                    |

The policy receives validated `evidence`, the server-read `challenge` when one ran, `accountUsed`, and cross-account `matches` for completed actions with that resource key. It also receives the authenticated account, normalized input, attempt ID, receipt time and PostgreSQL transaction client.

A valid signature authenticates a record; it does not grant access. Your policy decides what to do with a contradicted, refused or concealed measurement. A passing ceremony does not override an existing eligibility restriction.

The [reference app](/integration/nextjs-postgres) implements a complete allocation transaction. It refuses prior redemptions, compares identifier observations, requests a ceremony when its rule requires one, and writes the allocation and receipt together.

The [registration example](/integration/account-registration) creates a server-owned pending registration first and uses its ID with the server session as the action context. It creates the account record only in `commit`. This supplies a bound identifier before login without accepting a browser-submitted account ID. Existing single-use [signup/form integrations](/integration/challenge-widget) remain available.

The [checkout example](/integration/protected-checkout) uses the same contract for server-calculated prices, stock allocation and an order with a payment outbox task.

## Submit from the browser

After [initializing the client](/integration/prepare-session):

```js theme={"dark"}
try {
  const outcome = await client.execute('redeem', { code });
  showReceipt(outcome.result);
} catch (error) {
  showMessage(error.code);
  // Retain error.attemptId for an explicit retry of this same action.
}
```

These application display functions handle your result presentation. The SDK supplies waiting and ceremony UI. It never replaces the server's decision with a browser callback.

The SDK keeps pending attempt IDs in sessionStorage for the same authentication context, action and input. If browser storage is unavailable or your application manages retries, pass `{ attemptId: previousAttemptId }` explicitly. Reusing an ID with changed inputs is refused. `client.status()` refreshes the authentication context after login/account changes.

## Complete once

The adapter saves pending attempts and completion receipts in PostgreSQL. It serializes the shared resource before checking policy and committing. Multiple application instances can handle retries; a completed attempt returns its saved receipt.

Use the provided `tx` for every database write that must be atomic with completion. Keep existing unique constraints and business checks. Do not perform an external payment, email or other irreversible API call directly in `commit`; write an outbox entry and use the external service's idempotency mechanism.

No transaction stays open while the visitor completes a measurement or phone ceremony. A timeout does not prove that an action failed to commit. Retry the same attempt and read its receipt.

## Match observations

`matches` identifies other accounts whose completed actions share observed values, with separate reasons such as `probe.ip`, `probe.instance` and `probe.machine`. Equal identifiers are conditional evidence, not proof of a unique person. Identifier comparisons require the same namespace and version. Probe, ceremony-phone and desktop-handoff records remain distinct.

Store policy reasons and results for review. Completed claim history is useful beyond the lifetime of the measurement cookie. Choose retention according to your eligibility rules, retry window and deletion obligations; removing a receipt/history entry removes that entry from future checks.

## Limited resources

The example awards in verified transaction order. Receipt of an earlier request does not reserve inventory. An arrival-order policy needs an explicit qualification deadline, rules for earlier unfinished attempts and protections against reservation abuse. Those allocation choices belong in your application policy.

## Failure states

| Result                       | Application behavior                                                                                                                              |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Measurement required         | SDK joins or starts collection and shows waiting UI.                                                                                              |
| Challenge required           | SDK opens the ceremony and asks your server to read the saved outcome.                                                                            |
| Denied                       | Display the business result. A new challenge does not automatically remove it.                                                                    |
| Dismissed                    | The pending attempt remains. Retry with its ID to resume.                                                                                         |
| Expired attempt              | Start a new attempt after checking existing business state.                                                                                       |
| HTTP 503 or lost response    | Preserve the ID and retry the same attempt.                                                                                                       |
| `challenge-creation-pending` | Creation is unresolved; no second ceremony is automatically minted. Use an explicit restart with the old attempt ID when a fresh check is wanted. |

`client.execute(action, input, { attemptId, restart: true })` atomically retires an unfinished attempt before creating a replacement. If the original already completed, the server returns its receipt. Expose this as an explicit user action; ordinary retries retain the existing attempt.

Use `onEvent` on the server for readiness, elapsed time, outcome and diagnostic reasons. The callback omits tokens and raw account/address values. An `onState` callback supplies browser feedback.


## Related topics

- [Protected checkout](/integration/protected-checkout.md)
- [Clearance](/integration/clearance.md)
- [Next.js](/integration/nextjs.md)
- [Node, Python and PHP](/integration/server-clients.md)
- [Prepare a session](/integration/prepare-session.md)
