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.
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.
| Surface | How it integrates | Typical use |
|---|---|---|
| Native C API | Include include/maelys_datalog.h and call the C functions directly. | Servers, agents, gateways, embedded policy checks. |
| WASM bindings | Compile 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 bindings | Native 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 loading | Load a manifest JSON and policy files from disk. | Production deployments with versioned policy bundles. |
| Buffer manifest loading | Load manifest JSON and policy source bytes from memory. | WASM, tests, streaming pipelines, in-memory policy stores. |
| Example domains | Optional 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.
domain_registry_*Install the domain vocabulary: which predicates exist, their arities, and their kinds (EDB, POLICY_FACT, IDB, QUERY). Policies can only use declared predicates.
manifest_load*Load and verify the policy set: identifiers, domains, SHA-256 fingerprints, loading modes, and the public query interface.
ruleset_*Each policy source is parsed against its domain registry. Unknown predicates, wrong arities, and unsafe rules are rejected.
edb_*Supply the runtime facts describing the current request: who, what, and in which context.
solve_onceCompute the least fixpoint, once. The result is immutable: no facts can be added after solving.
query_*Ask whether a derived fact is present in the solved result. Only predicates declared as queries in the manifest are visible.
receipt_*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.
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
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).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 → queryUsing 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") = falseWhy 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 pathIn Maelys DL the three concerns are deliberately separated:
| Concern | Soufflé | Maelys DL |
|---|---|---|
| Vocabulary declarations | .decl in source file | Domain registry → manifest |
| Facts / data | .input or inline in file | EDB (runtime, per request) |
| Rules | Inline in file | Ruleset (loaded once) |
| Outputs | .output | Public 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 bytesFor 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.
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_idThis means the manifest still controls policy identity, domain binding, query
surface, and integrity checks. The source array only provides the actual .dl
bytes.
Documentation
| Section | Lifecycle step | Description |
|---|---|---|
| Registries | 1 — Registry | Define the domain vocabulary: predicates, arities, and kinds. |
| Manifest loading | 2 — Manifest | Load and validate policy manifests from files or memory. |
| Rulesets | 3 — Ruleset | Parse and manage Datalog policies. |
| Runtime EDB | 4 — EDB | Supply request-specific input facts. |
| Solving | 5 — Solve | Compute the least fixpoint. |
| Querying | 6 — Query | Inspect explicitly declared derived facts. |
| Receipts | 7 — Receipt | Produce decision attribution and audit evidence. |
| Bindings overview | — | Compare the WASM and Python bindings and see where each fits. |
| WASM bindings | — | Use the engine from JavaScript and browser runtimes. |
| Python bindings | — | Use the engine natively from Python — no browser, no WASM. |
| Errors | — | Error codes, diagnostics, and fail-closed behavior. |
Choosing an entry point
| Use case | Recommended entry point |
|---|---|
| Production policies on disk | File manifest loading. |
| Policies already in memory | Buffer manifest loading. |
| Browser or playground integration | Browser helper (createPlayground) + WASM bindings + buffer manifest loading. |
| Native research/automation tooling | Python bindings + native domain builder. |
| Standalone tests and examples | Example domains + in-memory manifest loading. |
| Application-specific authorization | Custom 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.