API Reference

Integration API

Maelys DL exposes a small, fail-closed integration surface for embedding the Datalog engine in native applications and browser runtimes.

The canonical API is the C99 API exported through include/maelys_datalog.h. WASM integrations use the same engine through exported bindings that can be called from JavaScript. The lifecycle is identical in both environments: install a domain vocabulary, load policies, supply runtime facts, solve once, query explicit public predicates, and optionally emit receipts.

i

Authorization is explicit. A request is allowed only if the expected authorization fact is present in the solved result. Anything else — missing fact, load error, solve error, invalid query, or unsupported domain — means DENY.

Integration surfaces

Maelys DL is designed to be embedded rather than run as a separate service. The same policy engine can be used from native C code, server-side runtimes, tests, and browser-based WASM environments.

SurfaceHow it integratesTypical use
Native C APIInclude include/maelys_datalog.h and call the C functions directly.Servers, agents, gateways, embedded policy checks.
WASM bindingsCompile the standalone engine to WASM and call exported functions from JavaScript or the browser playground helper.Browser playgrounds, web apps, demos, sandboxed policy evaluation.
Python bindingsNative cffi binding over the same C API, in-process, no browser or WASM runtime.Research pipelines, notebooks, offline analysis, long-running native services.
File manifest loadingLoad a manifest JSON and policy files from disk.Production deployments with versioned policy bundles.
Buffer manifest loadingLoad manifest JSON and policy source bytes from memory.WASM, tests, streaming pipelines, in-memory policy stores.
Example domainsOptional graph and decision domains for examples and standalone tests.Documentation, samples, test fixtures, playgrounds.
!

The C API is the source of truth. JavaScript and WASM integrations should be understood as bindings over the same fail-closed engine, not as a separate policy model.

Lifecycle

The engine is built around a strict one-way pipeline. Each stage consumes the output of the previous one, and nothing can be skipped or reordered.

1. Registrydomain_registry_*

Install the domain vocabulary: which predicates exist, their arities, and their kinds (EDB, POLICY_FACT, IDB, QUERY). Policies can only use declared predicates.

2. Manifestmanifest_load*

Load and verify the policy set: identifiers, domains, SHA-256 fingerprints, loading modes, and the public query interface.

3. Rulesetruleset_*

Each policy source is parsed against its domain registry. Unknown predicates, wrong arities, and unsafe rules are rejected.

4. EDBedb_*

Supply the runtime facts describing the current request: who, what, and in which context.

5. Solvesolve_once

Compute the least fixpoint, once. The result is immutable: no facts can be added after solving.

6. Queryquery_*

Ask whether a derived fact is present in the solved result. Only predicates declared as queries in the manifest are visible.

7. Receiptreceipt_*

Emit an audit-grade attribution of the decision: which policy, which rule, which facts.

The pipeline is fail-closed at every stage: a failure anywhere yields DENY, never an implicit ALLOW.

Three layers, one pipeline

Most authorization engines mix vocabulary, logic, and runtime data in the same place. Maelys DL separates them into three distinct layers with different lifetimes and different owners.

Manifest
contract · vocabulary · authority

Declares which predicates can exist, with what arity and kind. A rule or EDB fact that uses an undeclared predicate is rejected. The manifest is the security perimeter — it prevents the policy from reasoning about predicates that were never authorized.

edge/2         EDB
path/2         IDB
allow/1        QUERY
blocker/1      POLICY_FACT
Ruleset
logic · rules · deduction

Contains the Datalog rules. Defines how IDB facts are derived from EDB inputs. Loaded once, reused across every request. Does not contain runtime data — only the reasoning logic that the policy expresses.

path(X,Y) :- edge(X,Y).
path(X,Z) :- path(X,Y),
           edge(Y,Z).
allow(X)  :- path(R,X),
           not blocker(X).
EDB
runtime facts · input · request context

Holds the facts that are true for this specific request. Changes on every evaluation. Supplied by the caller, not by the policy author. If a fact is missing, the solver cannot invent it — the absence keeps evaluation fail-closed.

edge("root", "a").
edge("a", "b").
blocker("b").

The three layers combine in a single pipeline that runs exactly once per request:

Manifest  →  validates vocabulary
Ruleset   ↓  deduction logic      ┐
EDB       ↓  runtime input facts  ┤→ solve_once → IDB facts → query

Using the example above:

Manifest declares: edge/2 EDB, path/2 IDB, allow/1 QUERY, blocker/1 POLICY_FACT
Ruleset defines:   path(X,Z) :- path(X,Y), edge(Y,Z).  allow(X) :- path(root,X), not blocker(X).
EDB contains:      edge(root,a)   edge(a,b)   blocker(b)
 
Solver derives:
  path(root, a)    ← edge(root,a)
  path(a, b)       ← edge(a,b)
  path(root, b)    ← path(root,a) + edge(a,b)
  allow(a)         ← path(root,a), not blocker(a)  ✓
  allow(b)         blocked — blocker(b) is true     ✗
 
Query result: allow("a") = true  /  allow("b") = false

Why the separation matters

In a single-file system like Soufflé, declarations, rules, and data live together:

/* Soufflé — everything in one file */
.decl edge(x:symbol, y:symbol)
.input edge
path(x,y) :- edge(x,y).
path(x,z) :- path(x,y), edge(y,z).
.output path

In Maelys DL the three concerns are deliberately separated:

ConcernSouffléMaelys DL
Vocabulary declarations.decl in source fileDomain registry → manifest
Facts / data.input or inline in fileEDB (runtime, per request)
RulesInline in fileRuleset (loaded once)
Outputs.outputPublic Query Whitelist in manifest

This separation means:

The manifest fixes the perimeter.         (what can be said)
The ruleset reasons within that perimeter. (how to conclude)
The EDB injects only conforming facts.    (what is true now)
The solver derives only what is authorized. (what follows)

A policy cannot reason about admin_override(...) or secret_backend(...) unless those predicates were declared in the manifest. An EDB cannot inject a POLICY_FACT predicate — that slot belongs to the policy source alone.

Native C integration

Native integrations call the C API directly.

A minimal production integration usually does the following:

1. Register the application domain vocabulary.
2. Load a manifest from disk or memory.
3. Create runtime EDB facts for the request.
4. Solve the policy set once.
5. Query only the predicates declared as public queries.
6. Deny if anything fails.

The application remains responsible for deciding which query fact represents an authorization decision. For example, a system may require allow(request_id) to be present and treat every other outcome as DENY.

WASM and JavaScript integration

WASM integrations use the same engine in a browser or JavaScript runtime. The policy lifecycle does not change.

The main difference is how data enters the engine:

Native file mode:
    manifest path + .dl files on disk
 
WASM / browser mode:
    manifest JSON string + in-memory .dl source bytes

For that reason, browser integrations typically use buffer-based manifest loading. JavaScript provides the manifest JSON and policy source bytes, while the WASM engine validates the manifest, verifies SHA-256 fingerprints, parses the policies, and solves the rules. The website playground uses a browser helper in src/lib/datalog-wasm.ts that loads the real compiled WASM engine and throws WasmUnavailableError if the artifacts cannot be loaded — it does not fall back to a stub or fake engine.

i

The browser playground helper preserves the same fail-closed semantics as the C API. When the real WASM binary is unavailable, it raises WasmUnavailableError rather than silently substituting a fake engine — the UI surfaces the failure honestly instead of returning fabricated results.

Manifest versus in-memory sources

The manifest is the contract: it describes which policies exist, which domain they use, which predicates are queryable, and which SHA-256 each source must match.

For buffer-based loading, the .dl sources are supplied separately as in-memory policy sources. They are matched to manifest entries by policy_id.

manifest policy_id  →  in-memory source policy_id

This means the manifest still controls policy identity, domain binding, query surface, and integrity checks. The source array only provides the actual .dl bytes.

Documentation

SectionLifecycle stepDescription
Registries1 — RegistryDefine the domain vocabulary: predicates, arities, and kinds.
Manifest loading2 — ManifestLoad and validate policy manifests from files or memory.
Rulesets3 — RulesetParse and manage Datalog policies.
Runtime EDB4 — EDBSupply request-specific input facts.
Solving5 — SolveCompute the least fixpoint.
Querying6 — QueryInspect explicitly declared derived facts.
Receipts7 — ReceiptProduce decision attribution and audit evidence.
Bindings overviewCompare the WASM and Python bindings and see where each fits.
WASM bindingsUse the engine from JavaScript and browser runtimes.
Python bindingsUse the engine natively from Python — no browser, no WASM.
ErrorsError codes, diagnostics, and fail-closed behavior.

Choosing an entry point

Use caseRecommended entry point
Production policies on diskFile manifest loading.
Policies already in memoryBuffer manifest loading.
Browser or playground integrationBrowser helper (createPlayground) + WASM bindings + buffer manifest loading.
Native research/automation toolingPython bindings + native domain builder.
Standalone tests and examplesExample domains + in-memory manifest loading.
Application-specific authorizationCustom domain registry + explicit query predicates.

Integration contract

Every integration should preserve these rules:

Install domains before loading policies.
Load policies before creating request facts.
Add EDB facts before solving.
Solve exactly once for a request.
Query only predicates declared as public queries.
Deny on every error path.
!

Do not treat the absence of deny(...) as authorization. Authorization should be based on the presence of the expected allow(...) fact, or another explicit query predicate chosen by the integration.