Getting Started

Getting Started

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  (single public header)

Link against the library and include the single public header. No other dependencies.

#include "include/maelys_datalog.h"

The idea in one picture

Domaindeclare vocabularyPolicywrite .dl rulesEDBadd request factsSolvederive decisionsQuerycheck 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 },
 
  /* policy fact — declared in the .dl source */
  { "blocked", 1, MAELYS_DATALOG_PRED_KIND_POLICY_FACT },
 
  /* 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:

blocked("mallory").
 
allow(User, Doc) :-
    user(User),
    owns(User, Doc),
    not blocked(User).

Two things happen here. blocked("mallory") is a policy fact — it lives in the source file, not in the runtime data. The allow rule derives a decision: a user may access a document if they own it and are not blocked.

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;
}
i

load_policy_inline_with_static_domain registers the domain and loads the policy in a single call. Use it for small embedded policies with a fixed vocabulary. For production deployments with SHA verification and multiple policies, see Manifest loading.

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];
 
maelys_datalog_edb_init(&edb, pool, 64,
    &policy_set.policies[0].symbols,
    &policy_set.policies[0].registry);
 
/* alice exists */
maelys_datalog_edb_add_runtime_symbol_fact(&edb, "user", "alice");
 
/* alice owns roadmap.pdf */
maelys_datalog_symbol_id_t user_id, doc_id;
maelys_datalog_symbol_intern(&policy_set.policies[0].symbols,
    "alice", 5, &user_id);
maelys_datalog_symbol_intern(&policy_set.policies[0].symbols,
    "roadmap.pdf", 11, &doc_id);
 
maelys_datalog_term_t terms[2];
terms[0].kind = MAELYS_DATALOG_TERM_SYMBOL; terms[0].as.symbol = user_id;
terms[1].kind = MAELYS_DATALOG_TERM_SYMBOL; terms[1].as.symbol = doc_id;
maelys_datalog_edb_add_fact(&edb, "owns", terms, 2);
 
maelys_datalog_edb_finalize(&edb);

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;
 
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 <string.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_POLICY_FACT },
  { "allow",   2, MAELYS_DATALOG_PRED_KIND_IDB |
                  MAELYS_DATALOG_PRED_KIND_QUERY },
};
 
static const char policy_src[] =
  "blocked(\"mallory\").\n"
  "allow(User, Doc) :-\n"
  "    user(User), owns(User, Doc), not blocked(User).\n";
 
int main(void) {
  maelys_datalog_policy_set_t ps;
  maelys_datalog_diagnostic_t diag = {0};
 
  /* 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];
  maelys_datalog_edb_init(&edb, pool, 64,
      &ps.policies[0].symbols, &ps.policies[0].registry);
 
  maelys_datalog_edb_add_runtime_symbol_fact(&edb, "user", "alice");
 
  maelys_datalog_symbol_id_t u, d;
  maelys_datalog_symbol_intern(&ps.policies[0].symbols, "alice",       5, &u);
  maelys_datalog_symbol_intern(&ps.policies[0].symbols, "roadmap.pdf", 11, &d);
  maelys_datalog_term_t t[2] = {
    { .kind = MAELYS_DATALOG_TERM_SYMBOL, .as.symbol = u },
    { .kind = MAELYS_DATALOG_TERM_SYMBOL, .as.symbol = d },
  };
  maelys_datalog_edb_add_fact(&edb, "owns", t, 2);
  maelys_datalog_edb_finalize(&edb);
 
  /* Solve */
  maelys_datalog_solve_result_t *result = NULL;
  rc = maelys_datalog_solve_once(&ps.policies[0], &edb, &result);
  if (rc != MAELYS_OK) return 1;
 
  bool ok = false;
  maelys_datalog_query_solved_ground_fact(result, "allow", t, 2, &ok);
  maelys_datalog_solve_result_free(result);
 
  printf("allow(alice, roadmap.pdf) = %s\n", ok ? "true" : "false");
  return 0;
}

Expected output:

allow(alice, roadmap.pdf) = true

Try changing "alice" to "mallory" — the policy blocks them, so the result becomes false.

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.
  • API Reference — complete function reference.