AACsearch
Security & Compliance

Origin allow-list

Restrict which web origins may use a browser-facing API key.

Origin allow-list

When you ship an API key in browser code, you must assume that anyone can read it. The origin allow-list is the layer that makes that acceptable: it tells AACsearch to reject any request whose Origin header is not in the list you configured for that key.

This is not CORS. CORS controls whether browsers allow a response to be read. The origin allow-list is a server-side check that rejects the request before the search even runs, regardless of whether the caller is a browser at all.

The browser security model

Browser-side search has three layers of defense, used together:

  1. Origin allow-list — only your domains can use the key.
  2. Scope search only — the key cannot mutate data, even from your own domain.
  3. Tight rate limit per minute — caps abuse from any single client.

For per-user filtering, add a fourth layer: Scoped tokens.

Configuring the allow-list

From the dashboard:

  1. Project → API keys → Create key (or edit an existing one).
  2. In Allowed origins, enter one origin per line. An origin is scheme://host[:port], with no path, no trailing slash, no wildcards in path segments.
  3. Save.

Examples:

https://shop.example.com
https://www.example.com
https://staging.example.com:3000

For local development, add http://localhost:3000 (or whatever port you use). Remove it from the production key once you're shipping.

How matching works

On every public search request, the server reads the Origin request header and checks for an exact, case-sensitive match against the list. If the list is non-empty and the header is missing or unmatched, the request fails with HTTP 403 origin_not_allowed.

If the list is empty, no origin check runs. This is the right default for a server-side key (no browser ever calls it). It is the wrong default for a browser-side key.

Browser sends:                       Server checks:
Origin: https://shop.example.com  →  allowedOrigins.includes("https://shop.example.com") ?
                                       yes → continue
                                       no  → 403 origin_not_allowed

The check is O(n) over a small list. There is no startsWith, no wildcard, no regex. If you need to support *.example.com, list each subdomain.

When to use it

Where the key is usedAllow-list?
Browser (search widget, your own JS)Required. Without it, the key is unrestricted.
Mobile app (native HTTP)Optional — the app does not send a browser Origin.
Server-side workerNot needed. Use a server-side ingest/admin key.
CMS connector (PrestaShop, Bitrix)Not needed. Connector keys (ss_connector_*) are server-side.

What it does not protect against

  • A stolen key. If a request comes from a server-side attacker, they will send no Origin header at all — and your check rejects them. But if the attacker scripts a real browser on an allowed origin (XSS, malicious extension), the Origin header is correct and the request goes through. The downstream layers (search-only scope, rate limit, scoped-token filter) are what protect against that.
  • Cross-origin abuse via a vulnerable extension. Extensions can sometimes inject scripts into your page. CSP and a tight scope reduce the impact.
  • An attacker on your own domain. If your site has XSS, an attacker can call your API from your own origin. Fix the XSS.

Common mistakes

  • Trailing slashes or paths. https://example.com/ does not equal https://example.com. The Origin header never has a path; strip them.
  • Forgetting staging or preview deployments. If your preview environment has a different host (e.g. pr-123.preview.example.com), it will fail the check. Either use a separate key per environment, or list the preview hosts.
  • Hard-coding the same key into the mobile app. A native app sends no Origin, so a strict allow-list rejects it. Use a separate key, or leave the list empty on the mobile key and rely on the other layers.
  • Origin: null. Some sandboxed iframes and file:// pages send Origin: null. null matches only if you explicitly list null — usually you shouldn't.

Example: production setup

Key name:           Frontend Search (prod)
Scopes:             search
Allowed origins:    https://example.com
                    https://www.example.com
Rate limit:         600 / minute / key
Indexes:            products, articles

This key, if leaked from a browser bundle, can only be replayed from example.com or www.example.com. It cannot write data, and bursts above 600 requests per minute are rejected before they reach Typesense.

See also

On this page