# Blocking hooks (before create, before sign-in)
Source: https://sovaria.in/docs/auth-blocking-hooks

Run your own check inside sign-up and sign-in: refuse a user with your own message or add custom claims to their tokens, from a webhook or one of your functions.

A blocking hook is your code in the middle of an auth flow, like Firebase's blocking functions. Sovaria calls it and **waits for the answer** before it creates the user or signs them in. Use it to allow only your company's domain, block users your fraud check flags, require an invite, or stamp a tenant id or plan into the user's tokens.

(Diagram: Where the hooks run)

| Hook | Runs | Not called for |
|---|---|---|
| `before_create` | Before a new app user is stored: email/password sign-up, and the first sign-in with a social or SSO provider. | Anonymous users, users your team creates (console, admin API), SCIM provisioning, imports. |
| `before_sign_in` | After the password, code, link, passkey or provider was checked — before any session, token or MFA challenge. | Anonymous sign-ins. |

## What your hook answers

| Answer | Effect |
|---|---|
| `{"allow": false, "message": "Use your work email"}` (or `{"deny": true}`) | Refused: the app gets `403 AUTH_HOOK_DENIED` with your message (up to 200 characters) — the SDKs show it to the user as is. |
| `{"claims": {"tenant": "acme", "tier": "gold"}}` | Allowed, and the claims are stored on the user and added under `ctx` in every token from now on (refreshes too). Send `null` for a claim to remove it. |
| Anything else (`{}`, an empty body, `{"allow": true}`) | Allowed. |

- Claims Sovaria owns are dropped from your answer: `roles`, `permissions`, `org_id`, `project_id`, `env_id`, `aal`, `sub`, `email` and the other identity markers. Claims stay under 1,000 bytes.
- Use the claims in row policies like any other: `auth.jwt() -> 'ctx' ->> 'tenant'`.

## What your hook receives

auth.before_sign_in:
```json
{
  "id": "4b0c…",
  "type": "auth.before_sign_in",
  "project_id": "c0ffee00-…",
  "timestamp": "2026-10-23T09:12:44.120Z",
  "user": { "id": "8d1e…", "email": "ada@acme.com", "email_verified": true, "provider": "pwd" },
  "context": { "method": "pwd", "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 …", "aal": "aal1" }
}
```

`before_create` has no `user.id` yet and adds `display_name`. `id` is unique per call and `timestamp` lets you refuse a replayed request (accept only the last few minutes).

## A webhook

Sovaria `POST`s the event to your HTTPS URL with `X-Sovaria-Signature: sha256=<hex>`, the HMAC-SHA-256 of the raw body with the hook's secret (shown once when you save the hook; rotate it any time). Check it before trusting the body.

Node (Express):
```js
import crypto from 'node:crypto'

app.post('/hooks/before-sign-in', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.HOOK_SECRET).update(req.body).digest('hex')
  const got = req.get('X-Sovaria-Signature') ?? ''
  if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) return res.sendStatus(401)
  const event = JSON.parse(req.body)
  if (!event.user.email.endsWith('@acme.com')) return res.json({ allow: false, message: 'Use your Acme email.' })
  res.json({ claims: { tenant: 'acme' } })
})
```

Python (FastAPI):
```python
import hashlib, hmac, json, os
from fastapi import FastAPI, Request, Response

app = FastAPI()

@app.post("/hooks/before-create")
async def before_create(request: Request):
    raw = await request.body()
    expected = "sha256=" + hmac.new(os.environ["HOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(request.headers.get("x-sovaria-signature", ""), expected):
        return Response(status_code=401)
    event = json.loads(raw)
    if await is_flagged(event["user"]["email"]):
        return {"allow": False, "message": "We couldn't create your account."}
    return {}
```

## One of your functions

Point the hook at a function of the project instead of a URL. It is invoked with `{ "event": {...}, "signature": "sha256=…" }` — the HMAC of the event as canonical JSON (sorted keys, no spaces) — so it can tell Sovaria's call from someone invoking it directly, and returns the same answer.

Function:
```ts
import { createHmac } from 'node:crypto'

const canonical = (v: unknown): string => JSON.stringify(v, (_k, x) =>
  x && typeof x === 'object' && !Array.isArray(x) ? Object.fromEntries(Object.entries(x).sort()) : x)

export default async function handler({ event, signature }: { event: any; signature: string }) {
  const expected = 'sha256=' + createHmac('sha256', process.env.HOOK_SECRET!).update(canonical(event)).digest('hex')
  if (signature !== expected) return { allow: false, message: 'Unsigned call' }
  return event.user.email.endsWith('@acme.com') ? { claims: { tenant: 'acme' } } : { allow: false, message: 'Use your Acme email.' }
}
```

## Timeouts and failures

- Each hook has a timeout (default 2 s, at most 5 s — staff can change the cap in the Control Center).
- **Fail closed** (the default): if the hook errors, times out or answers something unreadable, the sign-up or sign-in is refused with `503 AUTH_HOOK_UNAVAILABLE`. Right for allow-lists and fraud checks — but if a fail-closed `before_sign_in` hook goes down, nobody can sign in until it is back.
- **Fail open**: the flow continues without your claims. Right for enrichment.
- Refusals and failures are written to the project's audit log. Use **Test** in the console (or `sov auth-hooks test`) after every change.
- If a broken hook locks your users out and you can't reach the console, Sovaria support can switch the project's hooks off; sign-ups and sign-ins then run without them.

## Set one up

Console:
```text
Authentication › Auth hooks › Blocking hooks › Set up
```

JavaScript:
```js
const hook = await sov.auth.blockingHooks.set('before_sign_in', {
  url: 'https://api.example.com/hooks/before-sign-in', timeoutMs: 1500, fail: 'closed',
})
console.log(hook.secret)   // shown once
await sov.auth.blockingHooks.test('before_sign_in', { email: 'ada@acme.com' })
```

Python:
```python
hook = await sov.set_blocking_hook("before_create", function="check-signup", fail="closed")
print(hook["secret"])  # shown once
```

CLI:
```bash
sov auth-hooks set before-sign-in --url https://api.example.com/hooks/before-sign-in --timeout-ms 1500
sov auth-hooks set before-create --function check-signup --fail open
sov auth-hooks test before-create --email ada@acme.com
sov auth-hooks list
```

HTTP:
```bash
curl -X PUT $URL/api/v1/auth/blocking-hooks/before-sign-in -H "Authorization: Bearer $TEAM_TOKEN" \\
  -H 'X-Sovaria-Project-ID: <project>' -d '{"target":"webhook","url":"https://api.example.com/h","timeout_ms":1500,"fail":"closed","enabled":true}'
# → {..., "secret": "whsec_…"}   (only when the secret is new or rotate_secret is true)
```

Only the project's owners and admins can see and change hooks (they receive your users' email addresses).

## Plans

|  | Free | Pro | Ultra | Team | Enterprise |
|---|---|---|---|---|---|
| Blocking before-create / before-sign-in auth hooks | — | ✓ | ✓ | ✓ | ✓ |

On a plan without blocking hooks, saved hooks stay but don't run. Event hooks (notifications after the fact) are on every plan: see [Auth event hooks](https://sovaria.in/docs/auth-hooks).
