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, 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 and tags everything belongs to. They live in your encore.app.json, or in the encore.domain.json of a domain.

Take care

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:

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.

billing/payments/encore.domain.json
{ "name": "payments", "description": "Card payments and the ledger.", "tags": { "data": "pci" } }
FieldRequiredDescription
nameYesThe domain's name, unique among its siblings. Lowercase letters, digits and -.
descriptionNoWhat the domain is for.
tagsNoThe domain's tags, which everything in it gets.
policiesNoThe domain's own 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:

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

A resource's tags go in its declaration:

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:

FieldRequiredDescription
nameYesUnique among the file's policies. Lowercase letters, digits and -, starting with a letter.
subjectsYesThe kinds of thing the policy judges. See Subjects.
matchNoA CEL expression choosing which of those things the policy applies to. Without it, the policy applies to all of them.
requireYesA CEL expression each thing the policy applies to must make true.
effectNo"deny", the default, or "warn". See Effects.
messageNoWhat a violation says. See 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:

Subjectself is
domainA domain, the root among them.
serviceA service.
frontendA frontend.
gatewayA gateway.
resourceA resource: a database, bucket, topic, subscription, Redis database, cron job, secret, config, metric or context field.
endpointAn endpoint of a service.
resource_linkOne service's use of one resource: every use in its code, together.
service_linkOne service calling another.
subscription_linkOne service subscribing to a topic.
exposure_linkOne 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:

VariableWhat it is
selfThe subject being judged.
appThe whole app: app.services, app.resources and app.domains by name or id, app.endpoints, and every kind of link.
domainThe 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:

SubjectFields
Domainid, name, parent, ancestry (the ids of the domains it is in and its own, the root first), depth, tags, effective_tags
Servicename, language, domain, tags, effective_tags, root_dir, workers, gateways (those any of its endpoints is exposed on), serves_traffic, hosts_frontend, hosts_gateway
Frontendname, language, service (the one hosting it, if any), domain, tags, effective_tags
Gatewayname, default, service (the one running its middleware, if any), domain, tags, effective_tags, origins_with_credentials, origins_without_credentials, any_origin_with_credentials
Resourceid (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
Endpointservice, name, protocol, methods, path, gateways, sensitive
Resource linkservice, resource, operations (what the service does with it), access (the highest access those need), locations, crossing
Service linkcaller, callee, locations, crossing
Subscription linkservice, subscription, topic, crossing
Exposure linkgateway, 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:

EnumerationValues
AccessREAD, WRITE, ADMIN, in that order, so self.access >= Access.WRITE holds for writes and schema changes.
OperationPOSTGRES_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.KindPOSTGRES_DATABASE, BUCKET, PUBSUB_TOPIC, PUBSUB_SUBSCRIPTION, REDIS_DATABASE, CRON_JOB, SECRET, CONFIG, METRIC, CONTEXT_FIELD
Resource.ScopeSERVICE, SHARED, GLOBAL
Service.LanguageGO, TYPESCRIPT, RUST, PYTHON
Endpoint.ProtocolHTTP, GRAPHQL, GRPC, PASSTHROUGH, FRAMEWORK

encore policy eval shows 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:

FunctionWhat 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:

"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:

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:

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

No public buckets, outside the marketing domain:

{ "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:

{ "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:

{ "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:

{ "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:

$ 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:

$ 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 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.