`ipAllowlist()` enforces IP rules at `onAuth`, before any gate policy or flow body runs. Internal admin surfaces, staging environments, and webhook endpoints stop unknown clients at the edge with the same typed `Forbidden` denial the [gate element](/docs/elements/gate) produces.

## Quick start

```typescript title="src/app.ts"
import { oke } from "okengine";
import { ipAllowlist } from "okengine/plugins";

export const app = oke({ name: "shop", env: "dev" }).plug(
  ipAllowlist({ allow: ["203.0.113.7", "2001:db8::42"] }),
);
```

A client whose IP is not on the list receives `403` with a typed denial:

```json
{
  "data": null,
  "error": {
    "code": "Forbidden",
    "data": { "reason": "ip_not_allowed", "ip": "198.51.100.9" }
  }
}
```

## Options

| Option              | Type       | Default             | Does                                                                   |
| ------------------- | ---------- | ------------------- | ---------------------------------------------------------------------- |
| `allow`             | `string[]` | — (everyone passes) | Exact IPs permitted — every other client is denied                     |
| `deny`              | `string[]` | — (nobody blocked)  | Exact IPs blocked — checked first, so deny wins on overlap             |
| `header`            | `string`   | `"x-forwarded-for"` | Header carrying the client IP                                          |
| `trustedProxyDepth` | `number`   | `1`                 | Trusted proxies that append XFF; client IP is that many from the right |

```typescript
.plug(ipAllowlist({ deny: ["198.51.100.9"], trustedProxyDepth: 1 }))
```

<Callout type="error">
  Standard reverse proxies **append** to `X-Forwarded-For` — left-side hops are attacker-controlled.
  The plugin trusts the hop `trustedProxyDepth` from the **right** (default `1` = last hop). Set
  this to your real proxy count — wrong depth bypasses the allowlist.
</Callout>

## Notes

| Behavior          | Detail                                                                             |
| ----------------- | ---------------------------------------------------------------------------------- |
| XFF parsing       | Last hop (depth `1`) is the client; left-side spoofed entries are ignored          |
| Missing header    | Denied when `allow` is set (unknown is not allowed); permitted for deny-only rules |
| Deny wins         | An IP in both lists is blocked                                                     |
| Non-HTTP triggers | No-op — there is no client IP outside HTTP                                         |

## Runtime configuration

Block an abusive IP from the database and every instance picks it up on the next sync:

```typescript
const rules = configSource({
  plugin: "ip-allowlist",
  code: { deny: [] },
  db: { store: db },
  kv: cache,
});
const ipRulesSyncClock = clock.every("ip-allowlist.sync", "30s");
on(ipRulesSyncClock, rules.sync());
export const app = oke({ name: "shop", env: "dev" }).plug(ipAllowlist(rules));
```

See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.

## Next

<Cards>
  <Card
    title="Maintenance Mode"
    description="Drain traffic with one flag."
    href="/docs/plugins/maintenance-mode"
  />
  <Card
    title="Gate"
    description="Policies and rate limits after the IP edge."
    href="/docs/elements/gate"
  />
  <Card title="Plugin API" description="Build your own plugin." href="/docs/reference/plugins" />
</Cards>
