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
Section titled “Enable”qql-go serve--policy-file policies.yaml--policy-reload # watch file for changes, zero-restart reloadRule Structure
Section titled “Rule Structure”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| Field | Description |
|---|---|
match.claims | Map of JWT claim → value. All must match (AND logic). |
match.authenticated | true = any valid token, false = no token required |
allow / deny
Section titled “allow / deny”allow: [QUERY, INSERT, CREATE, ALTER, DROP, SCROLL, SELECT, SHOW, EXPLAIN, DELETE, UPDATE]deny: [DROP, DELETE] # deny takes precedence over allowOperation types: QUERY, INSERT, CREATE, ALTER, DROP, DELETE, UPDATE, SCROLL, SELECT, SHOW, EXPLAIN.
collections
Section titled “collections”Glob-pattern allowlist for collection names:
collections: ["tenant_*", "shared_*"] # allow tenant_ and shared_ prefixescollections: ["*"] # all collectionsinject — Single Filter
Section titled “inject — Single Filter”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 — Multi-Filter (AND Logic)
Section titled “inject — Multi-Filter (AND Logic)”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
Section titled “limits”limits: max_limit: 50 # cap LIMIT value — prevents large result dumpsComplete Example
Section titled “Complete Example”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]Hot Reload
Section titled “Hot Reload”With --policy-reload, the gateway watches the policy file with fsnotify and reloads it atomically on any change — no restart, no dropped connections.
qql-go serve --policy-file policies.yaml --policy-reloadEdit policies.yaml → gateway picks it up immediatelySection titled “Edit policies.yaml → gateway picks it up immediately”