Global Hooks
/admin/global-hooks — superadmin only, requires an active tenant. Configures tenant-wide beforeRequest hooks: they run before every matching collection operation, and before the auth flows (login, register, refresh, password reset, email verification) — unlike collection hooks, which are scoped to one collection and one specific lifecycle event.
Global hooks always run before any collection-specific hooks for the same request.
Scope
Each global hook applies either:
- to all collections, or
- to a specific list of target collections, chosen explicitly.
Scope doesn't apply to auth flows
The collection scope above only matters for collection operations. For the auth flows, every enabled global hook fires regardless of its target-collection setting — a hook scoped to just posts still runs before a login request. If you only want a hook to run for collection operations and never for auth, that's not currently configurable — it always sees both.
Which tenant's hooks run on an auth flow
Login, register and the recovery routes (/api/auth/password/forgot|reset, /api/auth/verify/request|confirm) run the global hooks of the tenant named in the body's tenant field. Refresh runs those of the tenant the refresh token belongs to. A login without tenant is a login for the default tenant, so it runs the default tenant's hooks (details). Every other request whose tenant is missing, unknown or inactive runs no global hook at all. It never falls back to the default tenant, whose hook target would otherwise see another tenant's tokens and email addresses. A bearer token sent along with an auth request never decides whose hooks run.
File routes: off by default
A global hook runs before every matching collection operation and before the auth flows — but not before the file routes (GET/DELETE /api/collections/{collection}/{id}/files/{field}[/{fileId}]) unless you switch that on per hook.
Also guard file routes (includeFileRoutes) extends the hook to those four routes. It is off by default, and deliberately so: an existing hook was written for collection and auth deliveries, and a guard built to reject what it does not recognize would otherwise start blocking every download the moment it sees a new route type. With failOpen: false, an unreachable hook would turn every image into a 502.
With the switch on, a file request behaves like any other collection route:
- the collection scope applies — a hook limited to three collections does not see downloads from the others;
priority,timeout,failOpenandforwardHeadersare the same settings the hook already uses;- the envelope carries
event: beforeRequest,context.collection,context.recordId(the{id}from the path) andcontext.http.method/path, whiledata.bodyanddata.recordarenull. The record is not loaded:beforeRequestis the upfront filter, not the lifecycle event. A hook that needs the record belongs onbeforeView/beforeUpdate. - a rejection (
continue: false) answers the file request with the hook's status and body; the file is neither delivered nor deleted. A body returned by the hook is ignored here — a download has no request body to mutate. - an unknown collection is still a plain
404, and no hook runs for it: a hook must not become an oracle for which collections exist.
Every delivery shows up in the request log detail view with its event, outcome and duration, so the cost is visible per request.
One hook roundtrip per file request
A beforeRequest hook that itself calls the Paprika API (a guard looking up a device or license record, for example) creates nested calls inside a request Paprika is holding open. With this switch on, that applies to every file request too — and an image list fires many of them in parallel.
Experience from operating a tenant: without a cache in the hook, this does not fit into Paprika's 5-second budget for blocking hooks and ends in failed → 502 on a cold start. If you switch this on, the hook needs its own cache and a timeout below Paprika's budget.
The switch only exists for beforeRequest: collection hooks never receive it (the API drops it), and a definition that carries it on another event is rejected when it is saved.
Configuration
The same fields as collection hooks — URL, method, timeout, HMAC secret, priority, enabled flag, include-schema option, fail-open behavior — configured once and applied across the chosen scope. See Collection Hooks for what each option means, including the URL restrictions (no localhost/private-network targets unless allowlisted per tenant, see Tenants → Editing a tenant) and the 30-second timeout ceiling.
Forwarding request headers
A beforeRequest hook is an external authorization decision, so it often needs to see the headers the client sent to legitimize itself (a device proof, a request signature, an idempotency key, a custom auth scheme). By default it doesn't: context.http.headers carries only the fixed allowlist (Content-Type, User-Agent, Accept, Accept-Language, X-Request-Id).
Forward request headers (forwardHeaders) extends that list per hook. It is empty by default — nothing changes unless you configure it — and names are matched case-insensitively. Everything you list is sent to the hook's configured URL, which may be a third party, so list only what the target actually needs.
Authorization, Cookie, Set-Cookie and Proxy-Authorization are never forwardable: they authenticate the caller against Paprika itself, and a hook target that received one could impersonate them. Saving a hook that lists one of them is rejected with 400, and a definition that carries one anyway (e.g. written straight into the database) still won't leak it at dispatch time.
Since these hooks also gate auth flows, the request/response envelope looks slightly different for an auth-flow delivery: context.collection and context.recordId are null, and data.body is the raw auth payload instead of a collection record body. Credentials are removed from it before delivery: password, the single-use token of the reset and verification routes, and the refreshToken of a refresh — a hook target holding one of them could redeem it before Paprika does.
Forwarding works the same on file routes when Also guard file routes is on: the configured headers are in context.http.headers, the blocked ones never are.
Testing
Same as collection hooks: Test sends a sample payload immediately and shows the resulting status, latency, and response, without affecting real data.
When to use this instead of a collection hook
Reach for a global hook when the same logic needs to apply everywhere (e.g. request logging, a shared auth-side-effect, or a cross-cutting validation), instead of copy-pasting the same hook definition onto every collection individually.