API Reference

Registries

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:

Domain Registry  =  vocabulary authority
Policy source    =  Datalog logic using that vocabulary
Manifest         =  deployment contract and public observation gate
i

Registries keep the Datalog engine domain-neutral. The engine evaluates rules, but the application defines the vocabulary those rules are allowed to use.

!

The Domain Registry is the sole source of predicate vocabulary. Manifests never create predicates. A predicate is valid only if it is declared by the selected domain.

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. Registries answer three questions:

QuestionAnswered by
Which domains are available?Domain registry
Which predicates may this policy use?Predicate registry
Which predicates may callers observe publicly?Manifest Public Query Whitelist, constrained by the domain

Where registries fit

Registries define and freeze the vocabulary before policy loading happens.

Domain Registryvocabulary authorityPredicate Registryper-policy vocabularyPolicy sourceparsed after freeze

Architectural model

Three layers govern how a policy is loaded and evaluated.

LayerRole
Domain RegistryVocabulary authority. Defines predicate names, arities, and kind flags.
Policy sourceDatalog logic. Uses predicates declared by the selected domain.
ManifestDeployment contract. Binds policies to domains, verifies integrity, and controls public observation through the Public Query Whitelist.
Domain Registry
    -> declares what predicates exist and what they can do
Policy source
    -> uses that vocabulary to derive facts
Manifest
    -> selects a domain, verifies source integrity, and exposes only
       whitelisted QUERY predicates
i

The manifest controls public observation. It does not control vocabulary. The selected domain remains the vocabulary authority.

Predicate kinds at a glance

Four kinds are used throughout this page. They are introduced here so the examples that follow are self-explanatory.

Kind constantShortMeaning
MAELYS_DATALOG_PRED_KIND_EDBEDBRuntime fact supplied by the caller before solving.
MAELYS_DATALOG_PRED_KIND_IDBIDBFact derived by Datalog rules during solving.
MAELYS_DATALOG_PRED_KIND_QUERYQUERYDerived fact that may be inspected through the query API.
MAELYS_DATALOG_PRED_KIND_POLICY_FACTPOLICY_FACTFact declared directly in the policy source.

Flags may be combined: IDB|QUERY means a predicate is both derived and queryable. The full C definitions and combination rules are in Predicate kinds below.

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.

!

A predicate is usable only if it is declared by the selected domain and used in a way that matches its registered kind and arity.

Example domain: document access

Suppose an application wants to evaluate document access rules. The application wants this vocabulary:

PredicateArityKindMeaning
owns(User, Document)2EDBRuntime fact saying a user owns a document.
shared_with(User, Document)2EDBRuntime fact saying a document was shared with a user.
sensitivity_level(User, Level)2EDBRuntime fact saying a user has a sensitivity level.
blocked(User)1POLICY_FACTFact declared by the policy source.
allow(User, Document)2IDB + QUERYDerived 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.

In this example:

PredicateWho provides it?
owns/2Runtime caller
shared_with/2Runtime caller
sensitivity_level/2Runtime caller
blocked/1Policy source
allow/2Datalog 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 pathPublic query behavior
Manifest loadingallow/2 must be listed in the manifest Public Query Whitelist.
Inline loadingAny predicate declared QUERY by the domain is accessible.

Registry model

Maelys DL uses two related registries.

RegistryPurpose
Domain registryGlobal catalog of application domains that policies may reference.
Predicate registryPer-policy vocabulary used to parse and validate one loaded policy.

The domain registry answers:

Which domains are available?

The predicate registry answers:

Which predicates may this policy use?

During loading:

selected domain
    -> installs predicate vocabulary
    -> policy source is validated against that vocabulary

Domain registry

The domain registry stores domain definitions. A domain definition has:

FieldMeaning
domain_nameStable name used by manifests or inline loading.
descriptionOptional human-readable description.
predicatesStatic predicate table. NULL for callback-based domains.
predicate_countNumber of entries in the static table. 0 for callback-based domains.
install_predicatesInstaller callback. NULL for static-table domains.

A domain may provide its vocabulary in exactly one of two ways:

MethodUse when
Static predicate tableVocabulary is fixed at compile time.
Installer callbackInstallation needs custom logic, shared setup, or multiple modules.
!

Exactly one method must be provided. Providing both or neither returns MAELYS_ERR_INVALID_ARGUMENT.

The constraint is an XOR:

valid:
  install_predicates != NULL
  predicates == NULL
  predicate_count == 0
valid:
  install_predicates == NULL
  predicates != NULL
  predicate_count > 0
invalid:
  install_predicates != NULL
  predicates != NULL
invalid:
  install_predicates == NULL
  predicates == NULL

Method 1: static predicate table

A fixed domain can declare its predicates as a static table.

static const maelys_datalog_predicate_def_t document_access_predicates[] = {
    {
        .name       = "owns",
        .arity      = 2,
        .kind_flags = MAELYS_DATALOG_PRED_KIND_EDB,
    },
    {
        .name       = "shared_with",
        .arity      = 2,
        .kind_flags = MAELYS_DATALOG_PRED_KIND_EDB,
    },
    {
        .name       = "sensitivity_level",
        .arity      = 2,
        .kind_flags = MAELYS_DATALOG_PRED_KIND_EDB,
    },
    {
        .name       = "blocked",
        .arity      = 1,
        .kind_flags = MAELYS_DATALOG_PRED_KIND_POLICY_FACT,
    },
    {
        .name       = "allow",
        .arity      = 2,
        .kind_flags = MAELYS_DATALOG_PRED_KIND_IDB |
                      MAELYS_DATALOG_PRED_KIND_QUERY,
    },
};
static const maelys_datalog_domain_def_t document_access_domain_def = {
    .domain_name     = "document_access",
    .predicates      = document_access_predicates,
    .predicate_count = sizeof(document_access_predicates) /
                       sizeof(document_access_predicates[0]),
    .description     = "Document access-control policy domain",
};

This form is convenient when the whole vocabulary is known at compile time.

i

The domain registry stores a shallow copy of the domain definition. The domain_name string and predicates array must remain valid for as long as the global domain registry may reference them. Static arrays and string literals fulfill this requirement automatically.

Method 2: installer callback

A domain can also install its predicates through a callback.

static maelys_result_t
install_document_access_predicates(
    maelys_datalog_predicate_registry_t *registry)
{
    maelys_result_t rc;
    rc = maelys_datalog_predicate_registry_add_domain(
        registry,
        "owns",
        2,
        MAELYS_DATALOG_PRED_KIND_EDB);
    if (rc != MAELYS_OK) {
        return rc;
    }
    rc = maelys_datalog_predicate_registry_add_domain(
        registry,
        "shared_with",
        2,
        MAELYS_DATALOG_PRED_KIND_EDB);
    if (rc != MAELYS_OK) {
        return rc;
    }
    rc = maelys_datalog_predicate_registry_add_domain(
        registry,
        "sensitivity_level",
        2,
        MAELYS_DATALOG_PRED_KIND_EDB);
    if (rc != MAELYS_OK) {
        return rc;
    }
    rc = maelys_datalog_predicate_registry_add_domain(
        registry,
        "blocked",
        1,
        MAELYS_DATALOG_PRED_KIND_POLICY_FACT);
    if (rc != MAELYS_OK) {
        return rc;
    }
    return maelys_datalog_predicate_registry_add_domain(
        registry,
        "allow",
        2,
        MAELYS_DATALOG_PRED_KIND_IDB |
        MAELYS_DATALOG_PRED_KIND_QUERY);
}
static const maelys_datalog_domain_def_t document_access_domain_def = {
    .domain_name        = "document_access",
    .predicates         = NULL,
    .predicate_count    = 0,
    .description        = "Document access-control policy domain",
    .install_predicates = install_document_access_predicates,
};

predicates = NULL and predicate_count = 0 indicate the callback path. This form is useful when predicates come from multiple C modules, require conditional installation, or need shared setup logic.

Registering a domain

A domain is registered globally with:

maelys_datalog_domain_registry_register(&document_access_domain_def);

The global domain registry uses first-registration-wins semantics. If a domain name is already registered, subsequent calls with the same name are silently ignored and return MAELYS_OK. The originally registered domain remains authoritative for the lifetime of the process.

Once registered, the domain is available to all loading paths by name. A manifest references it through the domain field:

{
  "domain": "document_access"
}

An inline load passes the domain name as the first argument to maelys_datalog_load_policy_inline. See Inline loading for the full call signature and semantics.

!

Use a distinct name for each distinct vocabulary. Because registration is first-wins and permanent for the lifetime of the process, re-registering the same name with a different predicate table has no effect — the original domain remains in place.

Predicate registry

The predicate registry contains the predicates available to one loaded policy. A predicate declaration defines:

FieldMeaning
namePredicate name, for example owns, shared_with, or allow.
arityNumber of terms accepted by the predicate.
kind_flagsPredicate role: runtime fact, derived fact, query-capable fact, or policy fact.

Example:

maelys_datalog_predicate_registry_add_domain(
    registry,
    "allow",
    2,
    MAELYS_DATALOG_PRED_KIND_IDB |
    MAELYS_DATALOG_PRED_KIND_QUERY);

This declaration means:

allow/2 exists
allow/2 is derived by Datalog rules
allow/2 is capable of being queried

It does not mean allow/2 is publicly observable for every manifest-loaded deployment. Manifest-loaded policies still require allow/2 to be listed in the Public Query Whitelist.

Predicate identity: one name, one arity

A predicate name expresses a unique relation with a fixed arity. The registry enforces this as an identity contract: once owns is declared with arity 2, no other declaration of owns with a different arity is accepted.

owns/2  EDB   ✓  — the relation "user owns document"
edge/1  EDB   ✓  — the relation "node exists"
edge/2  EDB   ✗  — rejected: "edge" already has arity 1

If the semantic relationship changes arity, the predicate must change name:

edge/1          node-exists relation
link/2          two-node relation  — different name, different concept

The registry returns MAELYS_ERR_INVALID_FIELD if the same name is registered with two different arities. This is by design: the name of a predicate is the contract of the relation it represents. Allowing the same name to mean two different structural relations — one with one argument, one with two — would make policy rules ambiguous and solver results unpredictable.

!

If you need two structurally different relations, give them distinct names. edge/1 and edge/2 must become, for example, node/1 and edge/2.

Registry lifecycle: open, then frozen

The predicate registry has exactly two states. When a ruleset is initialized the registry is open: predicates may be added through maelys_datalog_predicate_registry_add_domain (or by installing a domain). Once the vocabulary is complete it is frozen, and the policy source is parsed and validated against that frozen vocabulary. Freezing is the point of no return: no predicate may be added after it.

1. Open registryruleset_init

Right after ruleset_init the registry is open and empty. It is ready to accept predicate declarations.

2. Add predicateadd_domain

Declare a predicate — its name, arity, and kind flags — into the open registry. Called once per predicate (or via a domain installer that batches these calls).

one call per predicate
3. Freezeregistry_freeze

Lock the vocabulary. After this point the set of predicates, their arities, and their kind flags are fixed. This is what makes “manifests never create predicates” true.

add after freeze → rejectedINVALID_STATE
4. Parse against frozen vocabularyparse_ruleset_ex

The parser validates every predicate name and arity in the policy source against the frozen registry. Any predicate not declared before freeze causes a parse failure.

The red branch is the security property in visual form: once the registry is frozen, any further attempt to add a predicate is refused. The vocabulary a policy may use is sealed before a single line of policy source is parsed.

void
maelys_datalog_predicate_registry_freeze(
    maelys_datalog_predicate_registry_t *registry);

In the manual ruleset pipeline, freeze sits between domain installation and parsing:

/* Open registry: install the domain vocabulary */
maelys_datalog_predicate_registry_add_domain(
    &ruleset.registry, "owns",    2, MAELYS_DATALOG_PRED_KIND_EDB);
maelys_datalog_predicate_registry_add_domain(
    &ruleset.registry, "blocked", 1, MAELYS_DATALOG_PRED_KIND_POLICY_FACT);
maelys_datalog_predicate_registry_add_domain(
    &ruleset.registry, "allow",   2,
    MAELYS_DATALOG_PRED_KIND_IDB | MAELYS_DATALOG_PRED_KIND_QUERY);
 
/* Freeze: no predicates may be added after this point */
maelys_datalog_predicate_registry_freeze(&ruleset.registry);
 
/* Parse: validated against the frozen vocabulary */
maelys_datalog_parse_ruleset_ex(&ruleset, src, src_len, file_path, &diag);
i

Most callers never invoke freeze directly. The high-level loaders (maelys_datalog_manifest_load_ex, maelys_datalog_manifest_load_from_text, maelys_datalog_load_policy_inline, and the WASM domainCommit) install the domain, freeze the registry, and parse in one call. freeze is exposed for callers that drive the ruleset pipeline manually — see Rulesets — Lifecycle for the full sequence.

!

Freezing is what guarantees the manifest cannot widen a policy’s vocabulary. The manifest selects a domain and a Public Query Whitelist, but the predicate set is already sealed by the time the manifest’s policy source is parsed.

Predicate kinds

KindMeaning
MAELYS_DATALOG_PRED_KIND_EDBRuntime fact supplied by the caller.
MAELYS_DATALOG_PRED_KIND_IDBDerived fact produced by Datalog rules.
MAELYS_DATALOG_PRED_KIND_QUERYDerived fact capable of being inspected through the query API.
MAELYS_DATALOG_PRED_KIND_POLICY_FACTFact declared by the policy source, not supplied by runtime callers.

A predicate may combine flags:

MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERY

This means the predicate is derived by the solver and has query capability. For manifest-loaded policies, public visibility is still controlled by the manifest Public Query Whitelist.

Loading paths

Once a domain is registered, it is available to both loading paths. The path chosen determines whether the QUERY predicates declared by the domain are subject to a Public Query Whitelist.

Domain Registryvocabulary authorityManifest pathManifestqueries whitelistPolicy setrestricted QUERY surfaceInline pathload_policy_inlineno whitelistPolicy setall QUERY accessible

Manifest path — the manifest selects the registered domain, verifies policy source integrity, and enforces a Public Query Whitelist through its queries field. Only predicates explicitly listed there are publicly observable after solving. See Manifest loading for the full model.

APIPublic Query WhitelistUse when
maelys_datalog_manifest_load_exManifest queries fieldProduction, multi-policy, SHA, versioned deployments
maelys_datalog_manifest_load_from_textManifest queries fieldWASM, embedded, manifest model without filesystem

Inline path — the domain name is passed directly to maelys_datalog_load_policy_inline or maelys_datalog_load_policy_inline_with_static_domain. There is no manifest and no Public Query Whitelist: all predicates declared as QUERY by the selected domain are accessible. See Manifest loading — Inline loading for the full call signature.

APIRegister stepUse when
maelys_datalog_load_policy_inlineExplicit, before loadTests, examples, REPLs, embedded simple callers
maelys_datalog_load_policy_
inline_with_static_domain
Combined, idempotentStatic C tables, WASM, one-call convenience

Domain registration in WASM

The domain registration model is the same in WASM and C. How you load the domain depends on which loading strategy you use.

Dynamic target — domain defined in JavaScript

With maelys_datalog_dynamic.js, the domain is defined at runtime from JavaScript using the dynamic builder. No C code is needed.

pg.domainBegin('access')
pg.domainAddPredicate('safe',  1, PredKind.EDB)
pg.domainAddPredicate('allow', 1, PredKind.IDB | PredKind.QUERY)
pg.domainCommit()

This corresponds to the static-table model but called from JavaScript. The predicate vocabulary is registered inside the WASM module. One domain per module instance — domainCommit() is permanent.

Under the hood, the dynamic builder accumulates the domain definition in a WASM-side static buffer, then makes the same core registration call as the static-table model above — it is not a separate registration mechanism. Only domainCommit enters the core engine:

JavaScript callWASM wrappermaelys_datalog_ prefix omittedEffectmaelys_datalog_ prefix omitted
domainBegin(name)wasm_domain_beginWASM-side only — copies the domain name into a static buffer (s_domain_name) and resets the builder.
domainAddPredicate(name, arity, kind)wasm_domain_add_predicateWASM-side only — appends a predicate_def_t to a static array (s_predicates[]), the same struct the static table uses.
domainCommit()wasm_domain_commitCore engine call — builds a domain_def_t from the buffered predicates and calls domain_registry_register.

In other words, domainBegin and domainAddPredicate build, in WASM-module memory, the exact predicate table shown in Method 1: static predicate table — the runtime equivalent of writing that static table, which in C is a compile-time artifact, not a function call. The single core-engine entry point is domainCommit, which performs the same domain registration call (maelys_datalog_domain_registry_register) that C code would make directly. The vocabulary, kind flags, arity contract, and first-registration-wins semantics are therefore identical; only the call site differs (JavaScript instead of C).

See WASM bindings — Dynamic builder.

Production target — domain table compiled in C

This target is a two-faced model. In it, JavaScript does not build the domain: the domain table is C code compiled into the WASM module. JavaScript only calls the exported loader for that fixed domain. The C face declares the vocabulary; the JS face just hands a policy source to a single export.

The C face — the same static table as Method 1, plus one exported loader:

static const maelys_datalog_predicate_def_t preds[] = {
    { "owns",  2, MAELYS_DATALOG_PRED_KIND_EDB },
    { "allow", 2, MAELYS_DATALOG_PRED_KIND_IDB |
                  MAELYS_DATALOG_PRED_KIND_QUERY },
};
 
EMSCRIPTEN_KEEPALIVE
maelys_result_t load_document_access(const char *src, size_t src_len) {
    return maelys_datalog_load_policy_inline_with_static_domain(
        preds, 2, "document_access", "doc.main",
        src, src_len, 0, &g_policy, &g_diag);
}

This is the static-table approach compiled into the WASM binary. The domain is fixed; the policy source can be fetched at runtime. The JavaScript side should normally call this through a small application wrapper rather than raw ccall; see the WASM static-domain strategy for the full loader.

Loading API comparison

Manifest APIs — require explicit domain registration before load, enforce a Public Query Whitelist.

APIPublic Query WhitelistUse when
maelys_datalog_manifest_load_exManifest queries fieldProduction, multi-policy, SHA, versioned deployments
maelys_datalog_manifest_load_from_textManifest queries fieldWASM, embedded, manifest model without filesystem

Inline APIs — no manifest, no whitelist. All domain QUERY predicates accessible.

APIRegister stepUse when
maelys_datalog_load_policy_inlineExplicit, before loadTests, examples, REPLs, embedded simple callers
maelys_datalog_load_policy_
inline_with_static_domain
Combined, idempotentStatic C tables, WASM, one-call convenience

Validation behavior

Error caseBehavior
Unknown domainPolicy loading fails.
Policy uses an undeclared predicatePolicy loading or parsing fails.
Policy uses a predicate with the wrong arityPolicy loading or parsing fails.
Both methods provided in domain definitionMAELYS_ERR_INVALID_ARGUMENT.
Neither method provided in domain definitionMAELYS_ERR_INVALID_ARGUMENT.
Manifest queries entry unknown to domainLoading fails closed.
Manifest queries entry arity mismatchLoading fails closed.
Manifest queries entry lacks QUERY flagLoading fails closed.
Manifest queries: [] or field absentNothing publicly queryable.
Runtime caller supplies an undeclared EDB predicateRuntime fact insertion fails.
Caller queries a non-whitelisted predicate from a manifest-loaded policyQuery rejected.
Caller queries a QUERY predicate from an inline-loaded policyAccepted — no whitelist.

Summary

ConceptDefines
Domain RegistrySole vocabulary authority. Predicate names, arities, and kind flags.
Domain definitionHow to install predicates for one domain: static table XOR callback.
Predicate registryWhich predicates one loaded policy may use.
Manifest loadingLoads policy sets with manifest JSON, metadata, SHA verification, and Public Query Whitelist.
Public Query WhitelistManifest queries field. Controls which QUERY predicates are publicly observable. Does not create predicates.
Inline loadingLoads one in-memory policy. No manifest whitelist. All domain QUERY predicates accessible.
Inline with static domainRegisters a static-table domain and loads a policy in one call. Inline semantics.
Predicate kind flagsWhether a predicate is runtime-supplied, policy-declared, derived, or query-capable.
First-registration-winsIf a domain name is already registered, re-registration is ignored.
idb_predicatesDeprecated manifest field. Accepted for compatibility, no semantic effect.
i

The registry layer makes policy evaluation explicit: a policy can only use the vocabulary installed by the selected domain. The manifest controls public observation — it never extends the vocabulary.