# 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/src/main.rs:6:5
  |
6 |     LEDGER.execute("update entries set amount = 0", &[]).await;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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": "rust" }
  |           --------- 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": "rust",
  "tags": { "team": "analytics" }
}
```

A resource's tags go in its declaration:

```rust
-- billing/payments/vault/src/db.rs --
use encore::storage::postgres::Database;

pub static LEDGER: Database = Database::new("ledger").tag("data", "pci").tag("tier", "core");
```

Every resource declaration takes `.tag(key, value)`, once per tag: databases, buckets, topics, subscriptions, Redis databases, cron jobs, secrets, configs, metrics and context fields. Keys and values must be string literals, because Encore reads them 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.
