# Policies

> Rules about who may use what, checked against your code

A policy is a rule about your app that Encore checks every time it reads your code. "Only services in `billing/payments` may write to databases holding card data." "A service may not call into another domain's services." "Every public endpoint must be exposed on the `api` gateway." You write the rule once, and Encore checks every service, resource and use of a resource against it, including the ones added after the rule was written.

Policies are written in [CEL](https://cel.dev), a small expression language, over a model of your app that Encore builds from your code: its services, resources and endpoints, which services use which resources and how, and which [domains](#domains) and [tags](#tags) everything belongs to. They live in your `encore.app.json`, or in the `encore.domain.json` of a domain.

> **Important**
>
> A broken policy never stops `encore run`. Your app keeps running, and Encore reports what its policies find next to it: in the terminal, in `encore policy check`, and to AI agents through the MCP server.

## An example

A payments team keeps card data in a database tagged `data: pci`, and wants only its own services to write to it:

```json
-- encore.app.json --
{
  "id": "shop",
  "policies": [
    {
      "name": "pci-writes",
      "subjects": ["resource_link"],
      "match": "self.resource.effective_tags.data == 'pci' && self.access >= Access.WRITE",
      "require": "within('billing/payments', self.service.domain)",
      "message": "{self.service.name} writes PCI data in {self.resource.name}"
    }
  ]
}
```

The policy applies to every `resource_link`, which is one service's use of one resource. `match` picks the uses it is about: writes to anything tagged `data: pci`. `require` says what each of those must make true: the service doing the writing is in the `billing/payments` domain.

When a `reports` service, which is in no domain, starts writing to the `ledger` database, Encore reports:

```
error[ENC0061]: policy violation in `pci-writes`: reports writes PCI data in ledger
 --> reports/api.py:5:11
  |
5 |     await ledger.exec("update entries set amount = 0")
  |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reports writes ledger here
  |
 ::: encore.app.json:8:46
  |
8 |       "require": "within('billing/payments', self.service.domain)",
  |                                              ------------------- no domain
  |
 ::: reports/encore.service.json:1:11
  |
1 | { "name": "reports", "lang": "python" }
  |           --------- reports is in no domain
```

It points at the line of code that breaks the policy, underlines the part of `require` that came out false with its value, and shows where that value comes from.

## Domains

A domain is a part of your app with a name: a directory with an `encore.domain.json` in it, and everything below that directory. Services, resources, frontends and gateways belong to the domain around the directory they are declared in.

```json
-- billing/payments/encore.domain.json --
{
  "name": "payments",
  "description": "Card payments and the ledger.",
  "tags": { "data": "pci" }
}
```

| Field | Required | Description |
|---|---|---|
| `name` | Yes | The domain's name, unique among its siblings. Lowercase letters, digits and `-`. |
| `description` | No | What the domain is for. |
| `tags` | No | The domain's [tags](#tags), which everything in it gets. |
| `policies` | No | The domain's own [policies](#a-domain-s-policies). |

Domains nest. A domain's id is its name, after the names of the domains it is in: `billing/payments` for a `payments` domain inside a `billing` domain. The id comes from the names, not the directory names, so the `payments` domain above could be in a directory called anything.

Your app itself is the root domain, with the id `""`. Everything that is in no declared domain is in the root, so every service and resource always has a domain.

A domain can't be declared inside a service's directory, though a service directory can be the root of one. Domains don't change how your app builds or runs: they exist for policies, and for Encore's tools to group your app by.

## Tags

A tag is a key and a value, like `data: pci` or `team: payments`, that says something about a part of your app. Domains, services, frontends, gateways and resources can all have tags.

A domain's tags go in its `encore.domain.json`, and a service's in its `encore.service.json`:

```json
-- reports/encore.service.json --
{
  "name": "reports",
  "lang": "python",
  "tags": { "team": "analytics" }
}
```

A resource's tags go in its declaration:

```python
-- billing/payments/vault/db.py --
from encore.storage import postgres

ledger = postgres.Database("ledger", tags={"data": "pci", "tier": "core"})
```

Every resource declaration takes `tags=`: databases, buckets, topics, subscriptions, Redis databases, cron jobs, secrets, configs, metrics and context fields. The dict must be written out in the declaration, as a literal, because Encore reads it from the code without running it.

Keys are lowercase letters, digits, `_` and `-`, start with a letter, and are at most 63 characters long. Values follow the same rules but may be empty. Something can have at most 32 tags, and keys starting with `encore` are reserved.

Each part of your app has two sets of tags in the policy model:

- `tags`: its own, as declared.
- `effective_tags`: the tags of the domains it is in, merged from the outermost to the innermost, then its own. The nearer one wins when two set the same key.

The `ledger` database above has `data: pci` among its effective tags because its domain sets it, without declaring it itself. Policies usually read `effective_tags`.

A tag something doesn't have reads as the empty string, so `self.tags.data == 'pci'` is simply false for something with no `data` tag. To test whether a tag is set at all, use `has(self.tags.data)`.

## Writing a policy

A policy is an object in the `policies` list of `encore.app.json` or of an `encore.domain.json`:

| Field | Required | Description |
|---|---|---|
| `name` | Yes | Unique among the file's policies. Lowercase letters, digits and `-`, starting with a letter. |
| `subjects` | Yes | The kinds of thing the policy judges. See [Subjects](#subjects). |
| `match` | No | A CEL expression choosing which of those things the policy applies to. Without it, the policy applies to all of them. |
| `require` | Yes | A CEL expression each thing the policy applies to must make true. |
| `effect` | No | `"deny"`, the default, or `"warn"`. See [Effects](#effects). |
| `message` | No | What a violation says. See [Messages](#messages). |

Encore checks a policy when it reads it, before judging anything by it. A field the subject doesn't have, a misspelled function, comparing an enum with a string, or an expression that isn't true or false is reported as `ENC0060`, with a suggestion where there is one, and the policy is left out until it is fixed.

### Subjects

A policy judges one or more kinds of subject, each in turn as `self`:

| Subject | `self` is |
|---|---|
| `domain` | A domain, the root among them. |
| `service` | A service. |
| `frontend` | A frontend. |
| `gateway` | A gateway. |
| `resource` | A resource: a database, bucket, topic, subscription, Redis database, cron job, secret, config, metric or context field. |
| `endpoint` | An endpoint of a service. |
| `resource_link` | One service's use of one resource: every use in its code, together. |
| `service_link` | One service calling another. |
| `subscription_link` | One service subscribing to a topic. |
| `exposure_link` | One endpoint exposed on one gateway: reachable from outside the app through it. |

A policy can name several kinds of subject, like `["service", "resource"]`. Then `self` has only the fields all of them share, here `name`, `domain`, `tags`, `effective_tags` and `location`. Encore tells you which kinds have a field you use when not all of them do.

### Variables

Every expression reads three variables:

| Variable | What it is |
|---|---|
| `self` | The subject being judged. |
| `app` | The whole app: `app.services`, `app.resources` and `app.domains` by name or id, `app.endpoints`, and every kind of link. |
| `domain` | The domain the policy belongs to: the one whose `encore.domain.json` declares it, or the root for a policy in `encore.app.json`. |

### What a policy can read

The most useful fields of each subject:

| Subject | Fields |
|---|---|
| Domain | `id`, `name`, `parent`, `ancestry` (the ids of the domains it is in and its own, the root first), `depth`, `tags`, `effective_tags` |
| Service | `name`, `language`, `domain`, `tags`, `effective_tags`, `root_dir`, `workers`, `gateways` (those any of its endpoints is exposed on), `serves_traffic`, `hosts_frontend`, `hosts_gateway` |
| Frontend | `name`, `language`, `service` (the one hosting it, if any), `domain`, `tags`, `effective_tags` |
| Gateway | `name`, `default`, `service` (the one running its middleware, if any), `domain`, `tags`, `effective_tags`, `origins_with_credentials`, `origins_without_credentials`, `any_origin_with_credentials` |
| Resource | `id` (like `postgres_database:ledger`), `kind`, `name`, `domain`, `tags`, `effective_tags`, and its kind's own settings: `bucket.public`, `pubsub_topic.exactly_once`, `pubsub_subscription.max_retries`, `secret.scope`, and so on |
| Endpoint | `service`, `name`, `protocol`, `methods`, `path`, `gateways`, `sensitive` |
| Resource link | `service`, `resource`, `operations` (what the service does with it), `access` (the highest access those need), `locations`, `crossing` |
| Service link | `caller`, `callee`, `locations`, `crossing` |
| Subscription link | `service`, `subscription`, `topic`, `crossing` |
| Exposure link | `gateway`, `endpoint`, `crossing` |

Everything also has a `location`, with the `file`, `line` and `column` it is declared at.

Every link has a `crossing`: the domain boundaries it crosses, from the side that acts to the side acted on. For a service in `billing/payments` using a resource in `identity`, `crossing.common` is the root, the innermost domain holding both; `crossing.exits` is `billing/payments` then `billing`; and `crossing.enters` is `identity`. A link inside one domain crosses nothing, so `self.crossing.enters.size() == 0`.

Enumerations are written by their type and name:

| Enumeration | Values |
|---|---|
| `Access` | `READ`, `WRITE`, `ADMIN`, in that order, so `self.access >= Access.WRITE` holds for writes and schema changes. |
| `Operation` | `POSTGRES_READ`, `POSTGRES_WRITE`, `POSTGRES_ADMIN`, `BUCKET_LIST`, `BUCKET_READ`, `BUCKET_WRITE`, `BUCKET_DELETE`, `BUCKET_SIGNED_UPLOAD_URL`, `TOPIC_PUBLISH`, `SUBSCRIPTION_RECEIVE`, `REDIS_READ`, `REDIS_WRITE`, `SECRET_READ`, `CONFIG_READ`, `METRIC_RECORD`, and more. |
| `Resource.Kind` | `POSTGRES_DATABASE`, `BUCKET`, `PUBSUB_TOPIC`, `PUBSUB_SUBSCRIPTION`, `REDIS_DATABASE`, `CRON_JOB`, `SECRET`, `CONFIG`, `METRIC`, `CONTEXT_FIELD` |
| `Resource.Scope` | `SERVICE`, `SHARED`, `GLOBAL` |
| `Service.Language` | `GO`, `TYPESCRIPT`, `RUST`, `PYTHON` |
| `Endpoint.Protocol` | `HTTP`, `GRAPHQL`, `GRPC`, `PASSTHROUGH`, `FRAMEWORK` |

`encore policy eval` [shows](#trying-an-expression) any field's value for any subject.

### Functions

Policies can use CEL's standard functions, like `size()`, `startsWith()`, `matches()`, `in`, `all()` and `exists()`, and its strings extension. Encore adds three:

| Function | What it does |
|---|---|
| `within(id, d)` | Whether the domain `d` is the domain `id` or is inside it. `within('billing', self.service.domain)` holds for a service in `billing` or `billing/payments`. |
| `glob(pattern, s)` | Whether `s` matches `pattern`, where `*` matches any run of characters and `?` any one. `glob('internal-*', self.name)`. |
| `reaches(s)` | The resources the service `s` can reach: those it uses, and those reached by the services it calls and by the subscribers of the topics it publishes to. |

### Effects

A policy with `"effect": "deny"`, the default, reports what breaks it as an error. `encore policy check` exits with an error when there is one, so a CI step can stop a change that breaks a policy.

A policy with `"effect": "warn"` reports what breaks it as a warning, and stops nothing. It is the way to introduce a rule to an app that doesn't keep it yet.

### Messages

A violation's message is the policy's `message`, with each `{expression}` in it replaced by the expression's value for the subject that broke it:

```json
"message": "{self.service.name} writes PCI data in {self.resource.name}"
```

Strings are written as they are, enum values by their names (`WRITE`), and lists joined by `, `. Write `{{` and `}}` for literal braces. Without a `message`, a violation says what broke the policy, like `reports using database ledger`.

## A domain's policies

A domain can declare policies of its own, in its `encore.domain.json`. They judge only what is in the domain, and the links with an end in it: a domain's policy over `service_link` sees calls into the domain from outside as well as calls out of it. In its expressions, `domain` is the domain itself, so the same policy reads the same in any domain:

```json
-- billing/encore.domain.json --
{
  "name": "billing",
  "policies": [
    {
      "name": "no-calls-out",
      "subjects": ["service_link"],
      "match": "within(domain.id, self.caller.domain)",
      "require": "within(domain.id, self.callee.domain)",
      "effect": "warn",
      "message": "{self.caller.name} calls {self.callee.name}, outside {domain.id}"
    }
  ]
}
```

The app's own policies, in `encore.app.json`, judge everything.

## More examples

Only the `api` gateway exposes endpoints to the internet:

```json
{
  "name": "one-gateway",
  "subjects": ["exposure_link"],
  "require": "self.gateway.name == 'api'"
}
```

No public buckets, outside the `marketing` domain:

```json
{
  "name": "private-buckets",
  "subjects": ["resource"],
  "match": "self.kind == Resource.Kind.BUCKET && !within('marketing', self.domain)",
  "require": "!self.bucket.public"
}
```

Every service says which team owns it:

```json
{
  "name": "owned",
  "subjects": ["service"],
  "require": "has(self.effective_tags.team)",
  "effect": "warn",
  "message": "{self.name} has no team tag"
}
```

A service that handles personal data can't reach a database tagged `analytics`:

```json
{
  "name": "pii-stays-out-of-analytics",
  "subjects": ["service"],
  "match": "self.effective_tags.data == 'pii'",
  "require": "!reaches(self).exists(r, r.effective_tags.data == 'analytics')"
}
```

Only schema migrations change a database's schema, so no service code needs admin access:

```json
{
  "name": "no-admin",
  "subjects": ["resource_link"],
  "require": "self.access < Access.ADMIN"
}
```

## Checking your app

`encore run` checks your app against its policies each time it reads your code, and shows what they find under the app's output. In the terminal UI, press `p` to see every finding. With `--json`, each report is an event of kind `policy`.

`encore policy check` checks the app once, prints each finding, and exits with an error if a `deny` policy is broken or a policy can't be read:

```shell
$ encore policy check
```

`encore policy ls` lists the app's policies, where each is declared, and how many things break it.

When a policy can't be decided for something, because an expression fails on it, the policy counts as broken by it. The usual cause is reading a key a map doesn't have, like `app.services['billing']` in an app with no `billing` service: test for it first, with `has()` or `in`.

### Trying an expression

`encore policy eval` evaluates one expression, the way a policy's `match` or `require` would, and prints its value:

```shell
$ encore policy eval 'self.access' --subject 'resource_link:reports->postgres_database:ledger'
"WRITE"
(Access)
```

The subject is named the way `encore policy check` names it: `service:billing`, `resource:postgres_database:ledger`, `domain:billing/payments`, `service_link:reports->billing`. Without `--subject`, the expression can read only `app` and `domain`. Use `--domain` to evaluate it as a policy of that domain would be.

### For AI agents

Encore's [MCP server](/docs/mcp) has the tools `policy_check`, `policy_list` and `policy_eval`, which do what the commands above do. Its other tools, like the ones that call an endpoint or run tests, add a warning to their output while a policy is broken, so an agent finds out it broke one without being stopped.
