Deprecated
Advanced quick start
Understand the transparent alpha API and the engine lifecycle beneath the stable facade.Maelys DL is an embedded Datalog policy engine for C applications. You write authorization logic in a small declarative language, supply runtime facts about the current request, and the engine tells you whether the request is allowed.
This page gets you from zero to a working authorization check in under five minutes.
What you need
- A C99 compiler (clang or gcc)
- libmaelys_datalog.a (built from source with make)
- include/maelys_datalog.h (historical 0.9.1 header; absent from the 0.10.0 SDK)Link against the library and include the single public header. No other dependencies.
#include "include/maelys_datalog.h"The idea in one picture
- Domaindeclare vocabulary
- Policywrite .dl rules
- EDBadd request facts
- Solvederive decisions
- Querycheck allow()
You declare which predicates exist (domain), write rules that derive decisions (policy), supply the runtime context (EDB), run the solver, and query the result. Any failure at any step means DENY.
Step 1 — Define your vocabulary
Policies can only use predicates you declare. Create a static domain table:
#include "include/maelys_datalog.h"
static const maelys_datalog_predicate_def_t preds[] = {
/* runtime facts — supplied per request */
{ "user", 1, MAELYS_DATALOG_PRED_KIND_EDB },
{ "owns", 2, MAELYS_DATALOG_PRED_KIND_EDB },
{ "blocked", 1, MAELYS_DATALOG_PRED_KIND_EDB },
/* derived decision — queryable */
{ "allow", 2, MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERY },
};Step 2 — Write a policy
Create a file policy.dl:
allow(User, Doc) :-
user(User),
owns(User, Doc),
not(blocked(User)).
The allow rule derives a decision: a user may access a document if they own
it and are not blocked. The concrete users, ownership relations, and blocked
users are runtime observations, so they belong in the EDB rather than in the
policy source.
Language features you will see elsewhere
MAELYS-DATALOG-v2 uses not(atom) for stratified negation. It also supports
the anonymous wildcard _ and a contextual, bounded or between positive
atoms:
can_read(User, Doc) :-
owns(User, Doc) or delegated(User, Doc),
not(blocked(User)).
has_any_document(User) :- owns(User, _).
Each _ occurrence is fresh and may only appear in a positive body atom.
or is expanded into ordinary rules before solving, so it does not appear in
proofs or Why-true output. These examples require delegated/2, can_read/2,
and has_any_document/1 to be declared in the predicate registry before the
policy is loaded. Source strings have no escape syntax: a quote or backslash
cannot be encoded inside a string literal.
Step 3 — Load the policy
maelys_datalog_policy_set_t policy_set;
maelys_datalog_diagnostic_t diag = {0};
maelys_result_t rc = maelys_datalog_load_policy_inline_with_static_domain(
preds,
sizeof(preds) / sizeof(preds[0]),
"access", /* domain name */
"access.main", /* policy id */
policy_src, /* .dl source */
policy_src_len,
0,
&policy_set,
&diag);
if (rc != MAELYS_OK) {
/* diag.message explains the failure */
return DENY;
}Step 4 — Supply runtime facts
For each request, create an EDB with the relevant facts:
static maelys_datalog_edb_t edb;
static maelys_datalog_fact_t pool[64];
rc = maelys_datalog_edb_init(&edb, pool, 64,
&policy_set.policies[0].symbols,
&policy_set.policies[0].registry);
if (rc != MAELYS_OK) return DENY;
/* Intern each request value once, then reuse its id. */
maelys_datalog_symbol_id_t user_id, doc_id, blocked_user_id;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "alice", &user_id);
if (rc != MAELYS_OK) return DENY;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "roadmap.pdf", &doc_id);
if (rc != MAELYS_OK) return DENY;
rc = maelys_datalog_edb_intern_runtime_symbol(
&edb, "mallory", &blocked_user_id);
if (rc != MAELYS_OK) return DENY;
rc = maelys_datalog_edb_add_symbol_id_fact(&edb, "user", user_id);
if (rc != MAELYS_OK) return DENY;
rc = maelys_datalog_edb_add_symbol_ids_fact(
&edb, "owns", user_id, doc_id);
if (rc != MAELYS_OK) return DENY;
rc = maelys_datalog_edb_add_symbol_id_fact(
&edb, "blocked", blocked_user_id);
if (rc != MAELYS_OK) return DENY;
rc = maelys_datalog_edb_finalize(&edb);
if (rc != MAELYS_OK) return DENY;Step 5 — Solve and query
maelys_datalog_solve_result_t *result = NULL;
rc = maelys_datalog_solve_once(&policy_set.policies[0], &edb, &result);
if (rc != MAELYS_OK) return DENY;
maelys_datalog_term_t terms[2] = {
{ .kind = MAELYS_DATALOG_TERM_SYMBOL, .as.symbol = user_id },
{ .kind = MAELYS_DATALOG_TERM_SYMBOL, .as.symbol = doc_id },
};
bool allowed = false;
rc = maelys_datalog_query_solved_ground_fact(
result, "allow", terms, 2, &allowed);
maelys_datalog_solve_result_free(result);
if (rc != MAELYS_OK || !allowed) return DENY;
return ALLOW;That's it. allowed is true when the solver derived allow("alice", "roadmap.pdf"). If anything fails along the way, the result is DENY.
Complete example
#include "include/maelys_datalog.h"
#include <stdbool.h>
#include <stdio.h>
static const maelys_datalog_predicate_def_t preds[] = {
{ "user", 1, MAELYS_DATALOG_PRED_KIND_EDB },
{ "owns", 2, MAELYS_DATALOG_PRED_KIND_EDB },
{ "blocked", 1, MAELYS_DATALOG_PRED_KIND_EDB },
{ "allow", 2, MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERY },
};
static const char policy_src[] =
"allow(User, Doc) :-\n"
" user(User), owns(User, Doc), not(blocked(User)).\n";
static bool failed(const char *step, maelys_result_t rc) {
if (rc == MAELYS_OK) return false;
fprintf(stderr, "%s failed with code %d\n", step, (int)rc);
return true;
}
int main(void) {
maelys_datalog_policy_set_t ps = {0};
maelys_datalog_diagnostic_t diag = {0};
maelys_datalog_solve_result_t *result = NULL;
bool edb_ready = false;
int exit_code = 1;
/* Load */
maelys_result_t rc = maelys_datalog_load_policy_inline_with_static_domain(
preds, 4, "access", "access.main",
policy_src, sizeof(policy_src) - 1, 0, &ps, &diag);
if (rc != MAELYS_OK) { fprintf(stderr, "load: %s\n", diag.message); return 1; }
/* EDB */
static maelys_datalog_edb_t edb;
static maelys_datalog_fact_t pool[64];
rc = maelys_datalog_edb_init(&edb, pool, 64,
&ps.policies[0].symbols, &ps.policies[0].registry);
if (failed("edb_init", rc)) goto cleanup;
edb_ready = true;
maelys_datalog_symbol_id_t u, d, blocked_user;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "alice", &u);
if (failed("intern alice", rc)) goto cleanup;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "roadmap.pdf", &d);
if (failed("intern roadmap.pdf", rc)) goto cleanup;
rc = maelys_datalog_edb_intern_runtime_symbol(
&edb, "mallory", &blocked_user);
if (failed("intern mallory", rc)) goto cleanup;
rc = maelys_datalog_edb_add_symbol_id_fact(&edb, "user", u);
if (failed("add user fact", rc)) goto cleanup;
rc = maelys_datalog_edb_add_symbol_ids_fact(&edb, "owns", u, d);
if (failed("add owns fact", rc)) goto cleanup;
rc = maelys_datalog_edb_add_symbol_id_fact(
&edb, "blocked", blocked_user);
if (failed("add blocked fact", rc)) goto cleanup;
maelys_datalog_term_t t[2] = {
{ .kind = MAELYS_DATALOG_TERM_SYMBOL, .as.symbol = u },
{ .kind = MAELYS_DATALOG_TERM_SYMBOL, .as.symbol = d },
};
rc = maelys_datalog_edb_finalize(&edb);
if (failed("edb_finalize", rc)) goto cleanup;
/* Solve */
rc = maelys_datalog_solve_once(&ps.policies[0], &edb, &result);
if (failed("solve", rc)) goto cleanup;
bool ok = false;
rc = maelys_datalog_query_solved_ground_fact(
result, "allow", t, 2, &ok);
if (failed("query allow", rc)) goto cleanup;
printf("allow(alice, roadmap.pdf) = %s\n", ok ? "true" : "false");
exit_code = ok ? 0 : 2;
cleanup:
maelys_datalog_solve_result_free(result);
if (edb_ready) maelys_datalog_edb_clear(&edb);
maelys_datalog_policy_set_clear(&ps);
return exit_code;
}Expected output:
allow(alice, roadmap.pdf) = trueTry changing "alice" to "mallory" — the policy blocks them, so the result
becomes false.
Membership and Why-true explanations
Membership and explanation are deliberately separate operations. The first asks whether a ground fact is true; the second asks for the canonical witness of a derived IDB fact:
| Binding | Membership | Why-true text |
|---|---|---|
| C | maelys_datalog_query_solved_ground_fact | maelys_datalog_explain_solved_fact then maelys_datalog_format_explanation_text |
| Python | SolveResult.contains_fact | SolveResult.explain_fact_text |
| WASM / JavaScript | query | explainTrue |
Why-true in C: explicit ownership
The native API separates the structured witness from its textual rendering.
The caller constructs the already-ground query fact, owns the explanation, and
owns the output text. maelys_datalog_explanation_t is large, so allocate it
off-stack:
#include "include/maelys_datalog.h"
#include <stdio.h>
#include <stdlib.h>
static int print_why_true(
const maelys_datalog_ruleset_t *ruleset,
const maelys_datalog_solve_result_t *result,
const maelys_datalog_term_t terms[2]) {
maelys_datalog_fact_t target = {0};
if (!maelys_datalog_predicate_registry_find(
&ruleset->registry, "allow", 2, &target.predicate_id)) {
return 1;
}
target.arity = 2;
target.terms[0] = terms[0];
target.terms[1] = terms[1];
maelys_datalog_explanation_t *explanation =
calloc(1, sizeof(*explanation));
if (explanation == NULL) return 1;
maelys_result_t rc =
maelys_datalog_explain_solved_fact(result, &target, explanation);
if (rc != MAELYS_OK) {
free(explanation);
return 1;
}
/* No matching derived IDB fact: this is absence, not truncation. */
if (!explanation->found) {
free(explanation);
return 0;
}
size_t required = 0;
rc = maelys_datalog_format_explanation_text(
ruleset, explanation, NULL, 0, &required);
if (rc != MAELYS_OK) {
free(explanation);
return 1;
}
char *text = malloc(required + 1);
if (text == NULL) {
free(explanation);
return 1;
}
size_t written = 0;
rc = maelys_datalog_format_explanation_text(
ruleset, explanation, text, required + 1, &written);
if (rc == MAELYS_OK && written == required) fputs(text, stdout);
free(text);
free(explanation);
return (rc == MAELYS_OK && written == required) ? 0 : 1;
}With the objects from the complete example, call
print_why_true(&ps.policies[0], result, t) after the successful membership
query and before freeing result. The count-only call obtains the exact
text length; the second call writes atomically into a correctly-sized buffer.
If the bounded witness is truncated, found remains true and the formatter
returns a normal document containing status=truncated.
For allow("alice", "roadmap.pdf") in this Quickstart, the helper above emits
the following canonical document (captured from the current C engine):
MAELYS-DATALOG-v2
document=why-true
status=complete
steps=1 premises=3
step=0 rule=1 fact="allow"("alice","roadmap.pdf")
premise=0 body=0 kind=positive origin=edb fact="user"("alice") parent=-
premise=1 body=1 kind=positive origin=edb fact="owns"("alice","roadmap.pdf") parent=-
premise=2 body=2 kind=negative-absence origin=edb fact="blocked"("alice") parent=-
result-step=0From C to Python and WebAssembly
The three bindings expose the same engine sequence:
load policy → add EDB facts → solve → test membership → optionally explainC exposes the native terms, facts, and caller-owned buffers. Python resolves input values for you and returns Python objects. The JavaScript wrapper does the same for symbolic queries of arity one or two against the WebAssembly module. Neither wrapper reparses or reconstructs the explanation: both return the canonical bytes formatted by the C engine.
After solving, the corresponding high-level calls look like this:
allowed = result.contains_fact("allow", ["alice", "roadmap.pdf"])
why = result.explain_fact_text("allow", ["alice", "roadmap.pdf"])
if why is not None:
print(why, end="")const allowed = pg.query('allow', ['alice', 'roadmap.pdf'])
const why = pg.explainTrue('allow', ['alice', 'roadmap.pdf'])
console.log(why)The Python Ruleset.solve(edb) result and JavaScript playground instance
pg must already hold a finalized solve result. The C and Python snippets in this
historical guide use deprecated APIs; the JavaScript snippet uses the current
typed WASM binding. For new integrations, start with the C Stable API,
current Python binding or current WASM API.
Python V1 returns None when no derived IDB witness exists. The current
JavaScript binding returns a canonical status=not-derived document instead. That is not the same as false, an error, or a truncated
explanation: a true EDB or policy fact may have no Why-true witness. A returned
document starts with:
MAELYS-DATALOG-v2
document=why-trueTreat the remainder as opaque, versioned engine output unless your application explicitly implements the normative format. Explanations may contain symbols from application facts, so decide explicitly before logging, persisting, or transmitting them. See the deprecated Python V1 reference for the behavior shown here; use the current Python or WASM reference for 0.10.0 applications.
Next steps
- Tutorial — step-by-step walkthrough with multiple users, sensitivity levels, and production-style manifest loading.
- Registries — how to declare more complex vocabularies.
- Rulesets — comparison operators, negation, and variable safety.
MAELYS-DATALOG-v2specification — normative source grammar, semantics, and Why-true document format.- C Low-level API — complete function reference.