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

CODE
- 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.

CODE
#include "include/maelys_datalog.h"

The idea in one picture

  1. Domain
    declare vocabulary
  2. Policy
    write .dl rules
  3. EDB
    add request facts
  4. Solve
    derive decisions
  5. Query
    check 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:

CODE
#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:

DATALOG
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:

DATALOG
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

CODE
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:

CODE
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

CODE
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

CODE
#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:

CODE
allow(alice, roadmap.pdf) = true

Try 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:

BindingMembershipWhy-true text
Cmaelys_datalog_query_solved_ground_factmaelys_datalog_explain_solved_fact then maelys_datalog_format_explanation_text
PythonSolveResult.contains_factSolveResult.explain_fact_text
WASM / JavaScriptqueryexplainTrue

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:

CODE
#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):

CODE
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=0

From C to Python and WebAssembly

The three bindings expose the same engine sequence:

CODE
load policy → add EDB facts → solve → test membership → optionally explain

C 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:

CODE
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="")
CODE
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:

CODE
MAELYS-DATALOG-v2
document=why-true

Treat 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-v2 specification — normative source grammar, semantics, and Why-true document format.
  • C Low-level API — complete function reference.