Embed in your app (feelc)

Run the real feelc engine directly in your TypeScript app — no HTTP API. feelc is the Go engine compiled to WebAssembly, so results are byte-for-byte identical to the feelc CLI (same parser, same exact-decimal VM, same determinism). It runs in the browser, Node 18+, bundlers (Vite / webpack 5 / Next / esbuild), and edge runtimes (Cloudflare Workers, Deno). ESM-only.

This is the embeddable counterpart to the in-browser playground: same WASM engine, packaged for your own apps. The .wasm is 6 MB (1.5 MB gzipped), loaded lazily on createEngine().

npm install feelc

Quick start

import { createEngine } from "feelc";

const feelc = await createEngine(); // loads the WASM once

const source = `
model "promo" {}
input cart_total : number >= 0
input is_member  : boolean
decision discount_pct : number {
  needs: cart_total, is_member
  hit: collect max
  >= 50  | -    => 5
  >= 100 | -    => 10
  -      | true => 8
}`;

const { output } = feelc.run(source, "discount_pct", { cart_total: 120, is_member: true });
console.log(output); // 10

Three ways to run

Mode API When
Compile at runtime feelc.run(source, decision, input) One-shot; you have the .rules text.
Compile once, evaluate many feelc.compile(source)model.evaluate(...) Reactive UIs; avoid recompiling per keystroke.
Ship a precompiled artifact feelc.load(bytes)model.evaluate(...) Smallest payload; hides rule source.
// compile once, evaluate many
const model = feelc.compile(source);
model.evaluate("discount_pct", { cart_total: 120, is_member: true }); // no recompile
// evaluate MANY rows in one JS↔WASM crossing — ~2× faster for bulk/reactive use (ADR 0024)
model.evaluateBatch("discount_pct", [
  { cart_total: 120, is_member: true },
  { cart_total: 60, is_member: false },
]); // -> { decision, results: [{ output }, ...] }  (a failed row is { error })
model.info();              // { name, inputs, decisions } — build forms from this
model.required("discount_pct");
model.dispose();           // free the WASM-side handle when done

The precompiled .ir.bin artifact is interchangeable with the CLI: feelc compile rules.rules -o model.ir.bin produces bytes that feelc.load() accepts, and model.export() produces bytes the CLI runs (see the IR format). You can also produce one from JS with the bundled feelc-compile CLI:

npx feelc-compile rules.rules -o model.ir.bin
const bytes = await (await fetch("/model.ir.bin")).arrayBuffer();
const model = feelc.load(bytes);
model.evaluate("discount_pct", { cart_total: 120, is_member: true });

Full surface

Source-based (mirror the HTTP service): run, verify, model, graph, trace, required, check. Compiled-model: compile, load, then on the model evaluate, evaluateBatch, info, required, export, dispose.

feelc.verify(source);            // { hash, report, blockers }
feelc.model(source);             // { name, inputs, decisions }
feelc.graph(source);             // { mermaid, dot, graph, findings, blockers }
feelc.required(source, "discount_pct");
feelc.check(source, [{ decision: "discount_pct", input: { cart_total: 120, is_member: true }, expect: 10 }]);

Per-environment notes

Errors

Engine failures throw FeelcError; compile errors carry a structured diagnostic (file/line/col/ code), the same error schema the CLI and HTTP API use.

import { FeelcError } from "feelc";
try {
  feelc.run("model x {", "d", {});
} catch (e) {
  if (e instanceof FeelcError) console.error(e.message, e.diag);
}

Caveat — decimal precision

Outputs cross into JS as number (float64), so very large/precise decimals can lose precision in JS. The engine itself stays exact (apd decimals); this only affects the value once it is handed back to JavaScript.