Concepts
Registries
Define the closed predicate vocabulary that policies are allowed to use.Registries define the vocabulary a Datalog policy is allowed to use.
A Datalog rule can mention predicates such as owns, shared_with, blocked,
or allow, but the Datalog engine does not know what these names mean by
itself. The application must first declare which predicates exist, how many
arguments they accept, and how they may be used.
In Maelys DL, this is a core security boundary:
| Element | Role |
|---|---|
| Domain registry | Lists the domains declared by the application. Each domain defines the predicates a policy may use. |
| Policy source | Contains the rules and policy facts written using that vocabulary. |
| Manifest | Selects the domain and policy files, checks their integrity, and restricts which predicates may be queried. |
For programming details, see C Stable registries, Python registries and TypeScript registries.
Why registries exist
A Datalog engine can evaluate rules generically, but applications need their own vocabulary. For example, a document-access application might need:
owns(User, Document)
shared_with(User, Document)
blocked(User)
allow(User, Document)
A graph application might need:
edge(From, To)
reachable(From, To)
A decision engine might need:
safe(Request)
allow(Request)
deny(Request)
The Datalog engine should not hard-code any of these names. Instead, each
application registers a domain that declares the predicates policies may use.
For the application, the important object is the domain definition: it
lists the predicates that a policy may use. Registering that definition makes
the domain available to policy loaders. A manifest may then restrict which of
its QUERY predicates callers can inspect, but cannot add predicates.
Where registries fit
The application registers a domain such as documents, declaring owns/2,
blocked/1, and allow/2 once. When a policy selects documents, the loader
installs those same declarations in a predicate registry belonging to the
loaded policy, freezes it, and checks the policy source against it. This is an
implementation step, not a second vocabulary for the application to maintain.
Domain Registryapp registers once
Load policyselect documents
Predicate Registrycopied + frozen
Parse policyunknown names rejected
- Domain Registry → Load policy
- Load policy → Predicate Registry
- Predicate Registry → Parse policy
For policy A or B, the loader repeats the last three stages. The application
registers documents only once; it never defines a second vocabulary for
each policy.
Architectural model
Three layers govern how a policy is loaded and evaluated.
| Layer | Role |
|---|---|
| Domain Registry | Vocabulary authority. Defines predicate names, arities, and kind flags. |
| Policy source | Datalog logic. Uses predicates declared by the selected domain. |
| Manifest | Deployment contract. Binds policies to domains, verifies integrity, and controls public observation through the Public Query Whitelist. |
Predicate kinds at a glance
Three roles describe where facts come from; QUERY is a separate observation permission. A predicate has one origin and may additionally be query-enabled.
| Role | Meaning |
|---|---|
| EDB | Runtime facts supplied by the application. |
| IDB | Facts derived by rules. |
| POLICY_FACT | Base facts written directly in policy source. |
| QUERY | Observation capability: when combined with EDB, POLICY_FACT, or IDB, permits inspection through the query API. |
For example, IDB|QUERY declares a derived relation that may be queried.
QUERY never stands alone: it does not say where the facts come from.
The C predicate flags
describe the corresponding declarations.
Domain-closed vocabulary
Maelys DL uses a domain-closed vocabulary model. That means every predicate used by a policy must be accepted by the selected domain. A policy cannot invent predicates during loading, and a manifest cannot add predicates to the domain. For example, this policy:
helper(X) :- safe(X).
allow(X) :- helper(X).
requires the selected domain to declare:
safe/1 EDB
helper/1 IDB
allow/1 IDB|QUERY
If helper/1 is not declared by the domain, loading fails.
Example domain: document access
Suppose an application wants to evaluate document access rules. The application wants this vocabulary:
| Predicate | Arity | Kind | Meaning |
|---|---|---|---|
owns(User, Document) | 2 | EDB | Runtime fact saying a user owns a document. |
shared_with(User, Document) | 2 | EDB | Runtime fact saying a document was shared with a user. |
sensitivity_level(User, Level) | 2 | EDB | Runtime fact saying a user has a sensitivity level. |
blocked(User) | 1 | POLICY_FACT | Fact declared by the policy source. |
allow(User, Document) | 2 | IDB + QUERY | Derived access decision. |
A policy in this domain might contain:
blocked("mallory").
allow(User, Document) :-
owns(User, Document),
not(blocked(User)),
sensitivity_level(User, Level),
Level >= 3.
allow(User, Document) :-
shared_with(User, Document),
not(blocked(User)),
sensitivity_level(User, Level),
Level >= 3.
The extra sensitivity_level/2 guard adds a numeric threshold: ownership or
sharing alone is not enough, the caller must also meet the minimum level.
This example also declares "mallory" in the domain's atoms, because that
string is written in policy source. This permits the constant; it does not
insert blocked("mallory"). The policy fact itself does that. By contrast,
request values such as "alice" and "roadmap.pdf" need not be declared as
atoms when they are supplied only in the EDB.
In this example:
| Predicate | Who provides it? |
|---|---|
owns/2 | Runtime caller |
shared_with/2 | Runtime caller |
sensitivity_level/2 | Runtime caller |
blocked/1 | Policy source |
allow/2 | Datalog solver |
The application supplies runtime facts such as:
owns("alice", "roadmap.pdf").
shared_with("bob", "roadmap.pdf").
sensitivity_level("alice", 4).
sensitivity_level("bob", 3).
The policy declares policy facts such as:
blocked("mallory").
The solver derives facts such as:
allow("alice", "roadmap.pdf").
allow("bob", "roadmap.pdf").
Both users satisfy the Level >= 3 guard, so the solver still derives allow/2
for each of them. A user with a lower sensitivity level would not derive
allow/2 even if the ownership or sharing fact were present.
Whether callers may publicly inspect allow/2 depends on the loading path:
| Loading path | Public query behavior |
|---|---|
| Manifest loading | allow/2 must be listed in the manifest Public Query Whitelist. |
| Inline loading | Any predicate declared QUERY by the domain is accessible. |
Two registries, one declared vocabulary
Domain registryregistered once by the application
Domain: documentsowns/2, blocked/1, allow/2
Loaded policy A
Predicate registry Acopy of documents, frozen
Loaded policy B
Predicate registry Bcopy of documents, frozen
- Domain: documents → Predicate registry A
- Domain: documents → Predicate registry B
The loader copies the same declarations into each policy. A and B do not register new domains or share a mutable predicate registry.
| Term | What it means for an application |
|---|---|
| Domain definition | The declaration you provide: a domain name and its allowed predicates, arities, flags, and optional policy-source atoms. |
| Domain registry | The engine's catalog of registered domain definitions. A policy selects one domain by name. |
| Predicate registry | The loaded policy's working copy of the selected domain's predicate declarations. It is frozen before the source is parsed. The loader creates and freezes it for you; it is not a second declaration to maintain. |
For example, two policies can both select documents. Each loaded policy gets
its own predicate registry for validation, but both use the vocabulary of the
same registered domain. Loading a second policy does not require registering
the domain again and cannot silently add a new predicate.
Predicate identity and freeze
A predicate name identifies one relation with one arity: if owns/2 is
declared, the same domain cannot also use owns/1 for a different shape.
Give different relations different names. A loaded policy receives the selected
domain's declarations in an open predicate registry; before parsing, that
registry is frozen. The parser then rejects unknown names and wrong arities,
and a manifest cannot extend the frozen vocabulary.
Freezing is a loader step, not an extra call required by the public domain API. A domain defines the available vocabulary; a manifest selects a domain and restricts public observation.
What you have learned
KEEP THIS MODEL
Declare the vocabulary once- The domain is your vocabulary declaration
Give it a name, list the predicates with their arity and origin, and declare any policy-source atoms. Stable C takes a declaration table; Advanced C also supports an installer callback.
- The domain registry makes that declaration reusable
It is the catalog in which the application registers domains. Registering the same declaration again is accepted; a different declaration under the same name is rejected, not silently substituted.
- The predicate registry is created for you
Each loaded policy receives a frozen copy of the predicate declarations of its selected domain. You do not maintain another vocabulary: unknown predicate names and wrong arities fail loading.
- Origin and query permission answer different questions
EDB means application-supplied facts, POLICY_FACT means facts written in the policy, and IDB means derived facts. QUERY permits observation and must accompany one of these origins.
- The loading path determines the observation checks
A manifest loads a policy set with metadata and SHA-256 verification; its queries list restricts which query-enabled predicates may be inspected. Inline loading takes one in-memory policy with no manifest whitelist, so all domain QUERY predicates are accessible.
- A declaration table works with either loading path
Register the domain first, then load its policy through a manifest or an inline source. The current SDK has no combined register-and-load call.
- A manifest never creates predicates
It selects an existing domain and may narrow public queries, but cannot extend the vocabulary of that domain. Its deprecated idb_predicates field is accepted for compatibility and has no effect.
Decision ruleThe domain defines what a policy may say. The policy defines how to reason. The manifest narrows what callers may inspect.