ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop in the portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
Development
Policies
Handlers
API Keys
Rate Limiting
Caching
MCP Server
MCP Gateway
AI Gateway
    IntroductionGetting StartedSource ControlUniversal API
    Providers
    Teams
    Apps
      OverviewManaging AppsPolicy ChainsCustom PoliciesFallbacksUsage Limits
    Policies
    Cookbooks
    Integrations
Developer Portal
Monetization
GraphQL
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Apps

Quickstart: Write a Custom Policy

The AI Gateway's built-in policies cover model access, budgets, caching, guardrails, and tracing—but your gateway can run any policy you can write in TypeScript. A custom policy lives in your gateway's repository, is declared in config/policies.json, and from then on appears in the portal's Add Policy dialog like any built-in policy. Apps add it to their policy chains, and it runs on every request for those apps.

This quickstart builds a content filter that blocks prompts containing banned terms. By the end, one app on your gateway rejects a prompt containing "acme-secret-project" with a 400 response, while other apps are unaffected.

Prerequisites

  • An AI Gateway project connected to a Git repository, with a provider, a team, and an app—the Getting Started guide covers this
  • A local clone of the gateway's repository
  1. Write the policy module

    In your clone of the gateway repository, create a modules/ directory next to config/ if it doesn't exist yet, and add modules/content-filter.ts. A policy is a function that receives the request, a context, and the options configured for it—and returns the request to continue the chain, or a Response to answer immediately:

    Code
    import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; interface ContentFilterOptions { blockedTerms: string[]; } export default async function contentFilter( request: ZuploRequest, context: ZuploContext, options: ContentFilterOptions, ): Promise<ZuploRequest | Response> { const body = await request.clone().json(); const text = JSON.stringify(body.messages ?? body.input ?? ""); const match = options.blockedTerms.find((term) => text.toLowerCase().includes(term.toLowerCase()), ); if (match) { context.log.warn( `Blocked request from app ${request.user?.sub}: matched "${match}"`, ); return new Response( JSON.stringify({ error: { message: "This request was blocked by your content policy.", type: "invalid_request_error", }, }), { status: 400, headers: { "content-type": "application/json" } }, ); } return request; }

    The calling app is available on request.user when the request's API key resolves to one—sub is the app's name—so a policy can log, branch, or report per app. It can be undefined, so use optional chaining. The example inspects chat and response payloads; embeddings requests pass through unfiltered.

  2. Declare the policy in config/policies.json

    The repository already contains config/policies.json with the built-in policy declarations. Add one more entry to its policies array—the entry's name is how the policy appears in the portal. The declaration makes the policy available for apps to select; it doesn't run for any app yet:

    Code
    { "name": "content-filter", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/content-filter)", "options": { "blockedTerms": ["acme-secret-project"] } } }
  3. Deploy

    Commit both files and push to your default branch:

    TerminalCode
    git add modules/content-filter.ts config/policies.json git commit -m "Add content-filter policy" git push

    The next production deploy puts the new policy on the menu—with GitHub, the push itself deploys; see Source Control for the other Git providers.

  4. Add the policy to an app's chain

    In the Zuplo Portal, open Apps, select your app, and open its Policies tab. Click Add Policy—content-filter now appears alongside the built-in policies. Add it, drag it to where in the chain it should run, and save.

    Where in the chain?

    If the chain has the Budgets and Costs policy, placing the filter before it means blocked requests aren't counted against the app's budget; placing it after means they are. The same reasoning applies to any policy that can answer a request itself.

    The change applies within about a minute—no deploy.

  5. Test it

    Send a prompt containing a blocked term through the app, using the URL and API key from the app page (the URL below is a stand-in for it):

    TerminalCode
    curl https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e/v1/chat/completions \ -H "Authorization: Bearer YOUR_APP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5-mini", "messages": [{"role": "user", "content": "Tell me about acme-secret-project"}] }'

    The gateway answers with the policy's 400 response. Send a harmless prompt and the request flows through to the provider as usual. Apps that don't include content-filter in their chains are unaffected.

Per-app settings

An app's chain entry can override the declared options completely. In the portal, edit the entry's options to give one app its own list:

Code
{ "blockedTerms": ["acme-secret-project", "codename-falcon"] }

An entry that overrides options replaces the declaration's entire options object —fields aren't merged. An entry without options inherits the declaration's options exactly.

Configure credentials

If a policy needs a credential—say it calls an external moderation API—set it in the declaration's options, and let chain entries inherit it:

Code
{ "name": "my-moderation-policy", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/my-moderation-policy)", "options": { "apiKey": "your-moderation-api-key" } } }

Leave the chain entry's options out so it inherits the declaration's values—an entry that sets its own options replaces them completely.

Beyond filtering

A custom policy can do more than block requests—the cookbooks walk through complete recipes:

  • Route models dynamically. Read the live model catalog and select the model per request—for example, always the cheapest active model. See Dynamic model routing.
  • Choose fallbacks from code. Enrich the model selection with a dynamically chosen backup. See Custom fallback logic.
  • Enrich or annotate. Add headers, log structured events, or call out to other services.

Next steps

  • Policy Chains: execution order, options inheritance, and the built-in policies
  • Policy Templates: roll a custom policy out to every new app in a team
  • Source Control: how repository changes deploy
Edit this page
Last modified on August 5, 2026
Policy ChainsFallbacks
On this page
  • Prerequisites
  • Per-app settings
  • Configure credentials
  • Beyond filtering
  • Next steps
TypeScript
JSON
JSON
JSON