BaaS - Business logic As A Service
Explainable, low-code decision engine with API and built-in UI
Power business rules, workflows, and AI guardrails with a fully open-source, developer-friendly engine designed for clarity, control, and auditability.
The Rule Engine design pattern did not reach its full potential. Not in terms of the vision but on adoption and implementation due to many practical limitations.
There are many awesome rule engines, but
Core flow:
derivedFactsbreakdowndocker run -p 3000:3000 zeguru/baas:latest
version: '3.8'
services:
app:
image: zeguru/baas:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=dev
docker run -d \
-p 3000:3000 \
-e DB_TYPE=mysql \
-e DB_HOST=server.xyz.com \
-e DB_PORT=3306 \
-e DB_DATABASE=baas \
-e DB_USERNAME=user \
-e DB_PASSWORD=password \
zeguru/baas:0.57
NB: adjust accordingly:
eg image -> zeguru/baas:latest
eg port -> 80
eg dbtype -> mariadb|postgres|oracledb|mssql
version: "3.9"
services:
app:
image: zeguru/baas:latest
container_name: baas-app
restart: unless-stopped
ports:
- "3000:3000"
environment:
DB_TYPE: mysql
DB_HOST: mysql
DB_PORT: 3306
DB_DATABASE: baas
DB_USERNAME: root
DB_PASSWORD: secret
NB: using a .env file
version: "3.9"
services:
app:
image: zeguru/baas:latest
container_name: baas-app
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- .env
docker-compose up -d
Define preconditions for rule execution.
Supported Operators:
alwaysisDefinedlessThanlessThanInclusivegreaterThangreaterThanInclusiveequalequalIgnoreCasenotEqualnotEqualIgnoreCaseininIgnoreCasenotInnotInIgnoreCase
Rule Action: main options:
advicevalidationapply-adjustmentSimply logs a message.
Message with an option to break, ie stop the execution and return immediately.

Perform a calculation, transformation or a process.
Modes available:
fixedexpressionvalue-lookup, range-lookup, value-range-lookupExpression
Powered by MathJS (https://mathjs.org/)

Value Lookup

Range Lookup

Value Range Lookup

Sandbox

Request
POST baas/calculator/sample-netpay-calc/compute
{
"grossPay": "150000"
}
Response
baseFacts are the original facts passed.
derivedFacts includes all computed values.
breakdown is the object that explains each fired rule and the result at that instance.
This can be displayed on a user interface or added to the LLM context when used in an AI agent.
stopped=false means that the evaluation was not interrupted/stopped by any rule.
{
"ruleSet": "sample-netpay-calc",
"stopped": false,
"baseFacts": {
"grossPay": "150000"
},
"derivedFacts": {
"shif": 4125,
"housingLevy": 2250,
"nssfTier1": 8000,
"nssfTier2": 64000,
"nssfTotal": 4320,
"personalRelief": 2400,
"taxableIncome": 143625,
"band1": 24000,
"band2": 8333,
"band3": 111292,
"grossPAYE": 37870.85,
"finalPAYE": 35470.85,
"netPay": 103834.15,
"timestamp": "2026-04-08T11:17:45+03:00"
},
"breakdown": [
{
"do": "apply-adjustment",
"message": "SHIF. 2.75% of gross pay",
"result": 4125
},
{
"do": "apply-adjustment",
"message": "Housing Levy. 1.5% of gross pay",
"result": 2250
},
{
"do": "apply-adjustment",
"message": "NSSF Tier I. First 8000 or less if pay is less than 8,000",
"result": 8000
},
{
"do": "apply-adjustment",
"message": "NSSF Tier II. Next 64,000 or less if gross pay is less than 72,000",
"result": 64000
},
{
"do": "apply-adjustment",
"message": "Compute Total NSSF deduction",
"result": 4320
},
{
"do": "apply-adjustment",
"message": "Define Personal Relief",
"result": 2400
},
{
"do": "apply-adjustment",
"message": "Compute taxable income",
"result": 143625
},
{
"do": "apply-adjustment",
"message": "Identify amount falling within band 1. (5K-24K)",
"result": 24000
},
{
"do": "apply-adjustment",
"message": "Identify amount falling within band 2. (next 8,333)",
"result": 8333
},
{
"do": "apply-adjustment",
"message": "Identify amount falling within band 3. (next 467,667)",
"result": 111292
},
{
"do": "apply-adjustment",
"message": "Compute PAYE before Relief",
"result": 37870.85
},
{
"do": "apply-adjustment",
"message": "Compute PAYE after Relief",
"result": 35470.85
},
{
"do": "apply-adjustment",
"message": "Compute NET pay",
"result": 103834.15
}
]
}
NB: A single rule is not very useful by itself. Multiple rules need to be grouped into what we call a ruleset in order to implement some business logic (eg price calculator, quote generator, etc).
A single rule
{
"when": {
"all": [
{ "fact": "numberOfCofeeCups","operator": "greaterThan","value": 3}
]
},
"then": {
"do": "advice",
"with": {
"message": "Too much coffee detected! Switch to water before you start coding in circles."
}
},
"priority": 10
}
A named group of rules, aka ruleset
[
{
"when": {
"all": [
{ "fact": "numberOfCofeeCups","operator": "greaterThan", "value": 3}
]
},
"then": {
"do": "advice",
"with": {
"message": "Too much coffee detected! Switch to water before you start coding in circles."
}
},
"priority": 10
},
{
"when": {
"all": [
{ "fact": "numberOfCommitsToday","operator": "lessThan","value": 1}
]
},
"then": {
"do": "advice",
"with": {
"message": "No commits yet? Time to make some magic happen."
}
},
"priority": 10
},
{
"when": {
"all": [
{
"fact": "numberOfProductionIncidents","operator": "greaterThanInclusive", "value": 1
}
]
},
"then": {
"do": "advice",
"with": {
"message": "Tackling production incidents ? May the force be with you."
}
},
"priority": 10
},
{
"when": {
"all": [
{"fact": "releaseDay","operator": "equalIgnoreCase","value": "Friday"}
]
},
"then": {
"do": "advice",
"with": {
"message": "Deploying on Friday? May the rollback odds be ever in your favor."
}
},
"priority": 10
},
{
"when": {
"all": [
{"fact": "numberOfCofeeCups","operator": "lessThanInclusive","value": 3},
{"fact": "numberOfCommitsToday","operator": "greaterThanInclusive","value": 1},
{"fact": "numberOfProductionIncidents","operator": "lessThan","value": 1},
{"fact": "releaseDay","operator": "notEqualIgnoreCase","value": "Friday"}
]
},
"then": {
"do": "advice",
"with": {
"message": "Balanced caffeine, steady commits, no incidents and no Friday releases. You are living the dream."
}
},
"priority": 5
}
]
All the facts defined in the ruleset must be set and sent as a request. Found under the when object.
Every evaluation includes a breakdown:
npm install
npm run start:dev
/public # Editor
/samples # Sample Rulesets
/readme # Documentation for sample rulesets
/src/calculator # Main rule evaluator/calculator
/src/common # Utils and DTOs
/src/meta # Dynamic metadata for ui
/src/ruleset # Ruleset management
/src/session # Session logic
Contributions are welcome and encouraged!
We need help in
BaaS aims for simplicity.
Fork the repository
Clone your fork
git clone https://github.com/YOUR_USERNAME/baas.git
cd repo-name
Add upstream (important!)
This links your local repo to the original repo:
git remote add upstream https://github.com/zeguru/baas.git
Now you have
- Original repo β `upstream`
- Your fork β `origin`
Create a feature branch
git checkout -b feature/amazing-feature
Make changes and Commit
git add .
git commit -m "Add amazing feature"
Push to your fork (not upstream!)
git push origin feature/amazing-feature
Open a Pull Request
From your fork
Target:
- base repo β original repo (upstream)
- head repo β your fork (origin)
Fill the PR documentation
Keep your fork updated
a. Sync with upstream
git checkout main
git fetch upstream
git merge upstream/main
b. Push updated main
git push origin main
npm run test:cov
npm run test
Built with:
Inspired by the need for:
GNU Affero General Public License v3 (AGPL)
If you find this project useful:
This project aims to be the decision layer for modern applicationsβfrom traditional systems to AI-powered workflows.
Resist all temptations to make this tool complex !
TypeScript
54.0%
JavaScript
28.4%
HTML
9.4%
CSS
7.8%
BaaS - Business logic As A Service
Explainable, low-code decision engine with API and built-in UI
Power business rules, workflows, and AI guardrails with a fully open-source, developer-friendly engine designed for clarity, control, and auditability.
The Rule Engine design pattern did not reach its full potential. Not in terms of the vision but on adoption and implementation due to many practical limitations.
There are many awesome rule engines, but
Core flow:
derivedFactsbreakdowndocker run -p 3000:3000 zeguru/baas:latest
version: '3.8'
services:
app:
image: zeguru/baas:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=dev
docker run -d \
-p 3000:3000 \
-e DB_TYPE=mysql \
-e DB_HOST=server.xyz.com \
-e DB_PORT=3306 \
-e DB_DATABASE=baas \
-e DB_USERNAME=user \
-e DB_PASSWORD=password \
zeguru/baas:0.57
NB: adjust accordingly:
eg image -> zeguru/baas:latest
eg port -> 80
eg dbtype -> mariadb|postgres|oracledb|mssql
version: "3.9"
services:
app:
image: zeguru/baas:latest
container_name: baas-app
restart: unless-stopped
ports:
- "3000:3000"
environment:
DB_TYPE: mysql
DB_HOST: mysql
DB_PORT: 3306
DB_DATABASE: baas
DB_USERNAME: root
DB_PASSWORD: secret
NB: using a .env file
version: "3.9"
services:
app:
image: zeguru/baas:latest
container_name: baas-app
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- .env
docker-compose up -d
Define preconditions for rule execution.
Supported Operators:
alwaysisDefinedlessThanlessThanInclusivegreaterThangreaterThanInclusiveequalequalIgnoreCasenotEqualnotEqualIgnoreCaseininIgnoreCasenotInnotInIgnoreCase
Rule Action: main options:
advicevalidationapply-adjustmentSimply logs a message.
Message with an option to break, ie stop the execution and return immediately.

Perform a calculation, transformation or a process.
Modes available:
fixedexpressionvalue-lookup, range-lookup, value-range-lookupExpression
Powered by MathJS (https://mathjs.org/)

Value Lookup

Range Lookup

Value Range Lookup

Sandbox

Request
POST baas/calculator/sample-netpay-calc/compute
{
"grossPay": "150000"
}
Response
baseFacts are the original facts passed.
derivedFacts includes all computed values.
breakdown is the object that explains each fired rule and the result at that instance.
This can be displayed on a user interface or added to the LLM context when used in an AI agent.
stopped=false means that the evaluation was not interrupted/stopped by any rule.
{
"ruleSet": "sample-netpay-calc",
"stopped": false,
"baseFacts": {
"grossPay": "150000"
},
"derivedFacts": {
"shif": 4125,
"housingLevy": 2250,
"nssfTier1": 8000,
"nssfTier2": 64000,
"nssfTotal": 4320,
"personalRelief": 2400,
"taxableIncome": 143625,
"band1": 24000,
"band2": 8333,
"band3": 111292,
"grossPAYE": 37870.85,
"finalPAYE": 35470.85,
"netPay": 103834.15,
"timestamp": "2026-04-08T11:17:45+03:00"
},
"breakdown": [
{
"do": "apply-adjustment",
"message": "SHIF. 2.75% of gross pay",
"result": 4125
},
{
"do": "apply-adjustment",
"message": "Housing Levy. 1.5% of gross pay",
"result": 2250
},
{
"do": "apply-adjustment",
"message": "NSSF Tier I. First 8000 or less if pay is less than 8,000",
"result": 8000
},
{
"do": "apply-adjustment",
"message": "NSSF Tier II. Next 64,000 or less if gross pay is less than 72,000",
"result": 64000
},
{
"do": "apply-adjustment",
"message": "Compute Total NSSF deduction",
"result": 4320
},
{
"do": "apply-adjustment",
"message": "Define Personal Relief",
"result": 2400
},
{
"do": "apply-adjustment",
"message": "Compute taxable income",
"result": 143625
},
{
"do": "apply-adjustment",
"message": "Identify amount falling within band 1. (5K-24K)",
"result": 24000
},
{
"do": "apply-adjustment",
"message": "Identify amount falling within band 2. (next 8,333)",
"result": 8333
},
{
"do": "apply-adjustment",
"message": "Identify amount falling within band 3. (next 467,667)",
"result": 111292
},
{
"do": "apply-adjustment",
"message": "Compute PAYE before Relief",
"result": 37870.85
},
{
"do": "apply-adjustment",
"message": "Compute PAYE after Relief",
"result": 35470.85
},
{
"do": "apply-adjustment",
"message": "Compute NET pay",
"result": 103834.15
}
]
}
NB: A single rule is not very useful by itself. Multiple rules need to be grouped into what we call a ruleset in order to implement some business logic (eg price calculator, quote generator, etc).
A single rule
{
"when": {
"all": [
{ "fact": "numberOfCofeeCups","operator": "greaterThan","value": 3}
]
},
"then": {
"do": "advice",
"with": {
"message": "Too much coffee detected! Switch to water before you start coding in circles."
}
},
"priority": 10
}
A named group of rules, aka ruleset
[
{
"when": {
"all": [
{ "fact": "numberOfCofeeCups","operator": "greaterThan", "value": 3}
]
},
"then": {
"do": "advice",
"with": {
"message": "Too much coffee detected! Switch to water before you start coding in circles."
}
},
"priority": 10
},
{
"when": {
"all": [
{ "fact": "numberOfCommitsToday","operator": "lessThan","value": 1}
]
},
"then": {
"do": "advice",
"with": {
"message": "No commits yet? Time to make some magic happen."
}
},
"priority": 10
},
{
"when": {
"all": [
{
"fact": "numberOfProductionIncidents","operator": "greaterThanInclusive", "value": 1
}
]
},
"then": {
"do": "advice",
"with": {
"message": "Tackling production incidents ? May the force be with you."
}
},
"priority": 10
},
{
"when": {
"all": [
{"fact": "releaseDay","operator": "equalIgnoreCase","value": "Friday"}
]
},
"then": {
"do": "advice",
"with": {
"message": "Deploying on Friday? May the rollback odds be ever in your favor."
}
},
"priority": 10
},
{
"when": {
"all": [
{"fact": "numberOfCofeeCups","operator": "lessThanInclusive","value": 3},
{"fact": "numberOfCommitsToday","operator": "greaterThanInclusive","value": 1},
{"fact": "numberOfProductionIncidents","operator": "lessThan","value": 1},
{"fact": "releaseDay","operator": "notEqualIgnoreCase","value": "Friday"}
]
},
"then": {
"do": "advice",
"with": {
"message": "Balanced caffeine, steady commits, no incidents and no Friday releases. You are living the dream."
}
},
"priority": 5
}
]
All the facts defined in the ruleset must be set and sent as a request. Found under the when object.
Every evaluation includes a breakdown:
npm install
npm run start:dev
/public # Editor
/samples # Sample Rulesets
/readme # Documentation for sample rulesets
/src/calculator # Main rule evaluator/calculator
/src/common # Utils and DTOs
/src/meta # Dynamic metadata for ui
/src/ruleset # Ruleset management
/src/session # Session logic
Contributions are welcome and encouraged!
We need help in
BaaS aims for simplicity.
Fork the repository
Clone your fork
git clone https://github.com/YOUR_USERNAME/baas.git
cd repo-name
Add upstream (important!)
This links your local repo to the original repo:
git remote add upstream https://github.com/zeguru/baas.git
Now you have
- Original repo β `upstream`
- Your fork β `origin`
Create a feature branch
git checkout -b feature/amazing-feature
Make changes and Commit
git add .
git commit -m "Add amazing feature"
Push to your fork (not upstream!)
git push origin feature/amazing-feature
Open a Pull Request
From your fork
Target:
- base repo β original repo (upstream)
- head repo β your fork (origin)
Fill the PR documentation
Keep your fork updated
a. Sync with upstream
git checkout main
git fetch upstream
git merge upstream/main
b. Push updated main
git push origin main
npm run test:cov
npm run test
Built with:
Inspired by the need for:
GNU Affero General Public License v3 (AGPL)
If you find this project useful:
This project aims to be the decision layer for modern applicationsβfrom traditional systems to AI-powered workflows.
Resist all temptations to make this tool complex !
TypeScript
54.0%
JavaScript
28.4%
HTML
9.4%
CSS
7.8%