Skip to content

Policy Engine

The policy engine controls what each authenticated user can do. Rules are evaluated top-to-bottom; the first matching rule wins. Unmatched requests are denied.

Enable policy engine
qql-go serve
--policy-file policies.yaml
--policy-reload # watch file for changes, zero-restart reload
rules:
- match: # who this rule applies to
claims:
role: admin # JWT claim must equal this value
allow: [QUERY, INSERT, CREATE, ALTER, DROP, SCROLL, SELECT, SHOW, EXPLAIN, DELETE, UPDATE]
deny: [] # overrides allow (takes precedence)
collections: ["*"] # glob patterns for allowed collections
- match:
claims:
role: reader
allow: [QUERY, SCROLL, SELECT, SHOW, EXPLAIN]
inject:
where: # single filter injection
field: tenant_id # Qdrant payload field
from_claim: org_id # read value from JWT claim
op: "="
limits:
max_limit: 50 # cap LIMIT value in all queries
FieldDescription
match.claimsMap of JWT claim → value. All must match (AND logic).
match.authenticatedtrue = any valid token, false = no token required
allow: [QUERY, INSERT, CREATE, ALTER, DROP, SCROLL, SELECT, SHOW, EXPLAIN, DELETE, UPDATE]
deny: [DROP, DELETE] # deny takes precedence over allow

Operation types: QUERY, INSERT, CREATE, ALTER, DROP, DELETE, UPDATE, SCROLL, SELECT, SHOW, EXPLAIN.

Glob-pattern allowlist for collection names:

collections: ["tenant_*", "shared_*"] # allow tenant_ and shared_ prefixes
collections: ["*"] # all collections

Inject a single WHERE condition into every query:

inject:
where:
field: tenant_id # Qdrant payload field to filter on
from_claim: org_id # JWT claim to read the value from
op: "=" # operator: "=", "!=", "in", "not_in"

Static value (instead of JWT claim):

inject:
where:
field: access
value: "public"
op: "="

Inject multiple WHERE conditions at once — all are AND'd together:

inject:
filters:
- field: org # tenant isolation
from_claim: org_id
op: "="
- field: team # department scoping
from_claim: department
op: "in"
- field: access # static exclusion
value: "confidential"
op: "!="
limits:
max_limit: 50 # cap LIMIT value — prevents large result dumps
rules:
# Admins can do anything
- match:
claims:
role: admin
allow: [QUERY, INSERT, CREATE, ALTER, DROP, SCROLL, SELECT, SHOW, EXPLAIN, DELETE, UPDATE]
collections: ["*"]
# Readers: read-only, tenant-scoped, limited to 50 results
- match:
claims:
role: reader
allow: [QUERY, SCROLL, SELECT, SHOW, EXPLAIN]
inject:
filters:
- field: org_id
from_claim: org_id
op: "="
- field: access
value: "confidential"
op: "!="
limits:
max_limit: 50
# Unauthenticated users: only explain
- match:
authenticated: false
allow: [EXPLAIN]

With --policy-reload, the gateway watches the policy file with fsnotify and reloads it atomically on any change — no restart, no dropped connections.

Hot reload
qql-go serve --policy-file policies.yaml --policy-reload
Edit policies.yaml gateway picks it up immediatelySection titled “Edit policies.yaml gateway picks it up immediately”