> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livry.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Restricting Who May Fetch a Theme: Origins, IP Ranges and CORS

> A Theme can restrict which browser origins and which IP ranges may fetch it. This page explains what each check is worth and how CORS behaves.

A Theme has **three independent checks**, and all of them must pass:

| Check | Against | Forgeable? |
| - | - | - |
| IP address | The Theme's allowed IP ranges | **No** — written by the platform in front of Livry |
| Origin | The Theme's allowed origins | **Yes**, by anything that is not a browser |
| Signature | The Theme's mode and keys | No |

An empty list switches its check off. A Theme nobody has configured restricts nothing.

<Warning>
  **Be clear-eyed about what each one buys.**

  The **origin** check reads headers a browser sets and a page's script cannot, so it stops another
  *website* embedding your theme. It stops nothing that is not a browser — `curl` sends whatever
  headers it likes.

  The **IP** check reads an address a client cannot forge. It is the one that constrains servers.

  Neither is a substitute for [signing](/serving/signed-urls) if the theme itself is sensitive.
</Warning>

## Origin allowlist

List the origins allowed to fetch the Theme:

```text theme={null}
https://app.northwind.example
https://*.northwind.example
```

The check reads the request's `Origin` header. If there is no `Origin`, it falls back to `Referer`.

<Note>
  **`Origin` wins over `Referer` when both are present.** `Origin` is the browser's statement of who
  is asking; a `Referer` can name a page the request merely passed through.

  A sandboxed frame sends the literal string `null` as its origin. That is **refused**, not treated
  as missing — otherwise a sandboxed page on any site could take the missing-origin path below.
</Note>

### Requests that name no origin

This is the case that catches people. A `<link rel="stylesheet">` without `crossorigin` sends **no
`Origin` header** — the only signal is `Referer`, and a page with `Referrer-Policy: no-referrer`
withholds that too. A server-side render, a build step or a `curl` sends neither.

So the Theme chooses:

| Policy | Behaviour |
| - | - |
| **Allow non-browsers** *(default)* | Refused if the request looks like a browser; allowed otherwise. |
| **Refuse** | Every request without an origin is refused — server-side and build-time fetches included. |
| **Allow** | Only a request naming a *disallowed* origin is refused. |

"Looks like a browser" means it carried a `Sec-Fetch-*` header — every current engine sends them and
no page script can set them. So a browser page that stripped its referrer is refused, while your
server fetching at render time is let through.

<Note>
  Browsers too old to send `Sec-Fetch-*` (Safari before 16.4) are indistinguishable from servers and
  pass. This setting is ignored entirely when the Theme lists no origins — with no allowlist there is
  nothing to be missing from.
</Note>

## IP allowlist

CIDR ranges, IPv4 and IPv6:

```text theme={null}
203.0.113.0/24
2001:db8::/32
```

If the platform could not report a trustworthy client address, the request **does not match** — null
never matches an allowlist.

This is the right check for a server-side integration: your renderer fetches from known egress
addresses, and nothing else can.

<Warning>
  Do not put an IP allowlist on a Theme fetched **by browsers**. Your users' addresses are not
  knowable, and every one of them will be refused.
</Warning>

## CORS

`Access-Control-Allow-Origin` follows the **origin check alone**, not the overall verdict:

| Theme | Header |
| - | - |
| Lists no origins | `Access-Control-Allow-Origin: *` |
| Lists origins, and yours is listed | Your origin, echoed back, plus `Vary: Origin` |
| Lists origins, and yours is not | No CORS header |

That split is deliberate: **a listed origin refused for another reason — its address, its signature —
can still read the `403` from script**, rather than seeing an opaque CORS failure with nothing in it.

Only the `Origin` header is echoed. An origin matched through `Referer` came from a no-cors request,
which reads no CORS header anyway.

### Preflights

A preflight is answered on the allowlists alone, before any signature check and without reading the
file — a preflight carries no credentials by definition, and the real request that follows is checked
in full.

## What a refusal tells you

**`403`, and nothing else.** The response never says which check refused.

That is on purpose: telling a caller which check failed tells them which header to forge. Which check
refused is recorded in Livry's own telemetry — if you are stuck, [ask us](/help/troubleshooting).

## Restricted Themes are not shared-cached

<Note>
  A Theme that restricts by origin or by address is served with cache directives that **forbid a
  shared cache from storing it**. A CDN or corporate proxy holding a copy would answer the next
  caller without running any of these checks.

  The cost is that a restricted Theme is slower than an unrestricted one. If you want CDN caching and
  access control together, use [signing](/serving/signed-urls) — a signed URL is safe to cache,
  because holding it *is* the credential.
</Note>

## Choosing

| Your situation | Use |
| - | - |
| Public product, theme is not secret | Nothing. `public` mode, no allowlists. Fastest and most cacheable. |
| Browser app, want to stop other sites embedding it | Origin allowlist. |
| Server-side rendering from known egress IPs | IP allowlist. |
| Theme itself is confidential (an unannounced brand) | **Signing.** Allowlists are a supplement, not a substitute. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.