The Dev Blog

  • What Does It Do?

    What Does It Do?

    I’ve tried to give the gist for this thing many times, and I am usually met with blank stares. I recently gave a long-winded explanation to a colleague and asked an AI to reduce it to Elevator Speech length. Here is that result

    ifServ is a low-code rules engine designed to handle complex business logic with simplicity and flexibility. It allows you to define rules as combinations of conditions and results—using either JavaScript or intuitive widgets—and apply them to any JSON Fact Model.

    Here’s the power: Rules are hierarchical, so you can start simple (e.g., ‘Require approval for for a purchase over $1,000’) and layer on complexity only when needed (‘…unless it’s a VIP or the alternative product or service has some limitation, or…’).

    We built it because traditional config systems buckle under real-world demands—for example, travel clients who might each need wildly different approval or security rules based on very nuanced conditions. With ifServ, you keep the simple cases simple while effortlessly scaling to handle even the most convoluted business logic—all without drowning users in complexity they don’t need.

    Simple as possible. Complex as needed. No more ‘WTF ARE ALL THESE OPTIONS?!’ moments


  • Back At It…

    Back At It…

    I stopped working on ifServ after about 10 months in October of 2024. Moved on to a couple other things (including getting a few AWS Certificates), and generally got very busy with the day job and other stuff. Speaking of the day job, I spend at least one hour per week in a meeting with five or six other people discussing… rules. Fiddly rules about desired customer behavior. I say “rules” in quotes because we’re really talking about custom code for individual customers. Lots of IFs... And at the end of these meetings, I think “I wish we had an IF Server”. So, I decided to jump back into it intensely for a couple weeks and see how far I can get.

    The goal for this little sprint is to:

    • De-suckify the UX in general. It will still suck (because I suck at UX), but I want to try and make it at least sensible.
    • A Tester / Request builder framework for calling rules from right within the app (no more postman!) and asserting various aspects about the results (think lightweight built-in Rest-Assured)
    • Some basic tree drag-drop gestures – so containers can be reorganized and rules can be moved around, etc.
    • Introduce “Community Containers” – containers which can be forked by anyone as a starting point. All rules and tests will come along for free. With this, I can start building some actual demo content for various use-cases.
    • Lots of other little things that I keep thinking up while working on it 🙂

    The goal is to make it possible for someone to actually figure out how to use it.

    Here’s a sneak peek at the Tester/Request Builder with a Woot! (Passing Test)

    The plan is to work intensely on these things until 1-April-2025 (about 2 weeks). It’s going to be a sprint. More to come…

    Infrastructure

    Along with these applicaiton changes, the app is now running in AWS. Front end is in a bucket. Database is RDS. Service layer is in ECS. It’s a very scaled down (read CHEAP) architecture, but it’s robust enough to use and demo the app.


  • it’s a Beta (sort of)!

    it’s a Beta (sort of)!

    ifServ Beta Milestone – October 2024

    Is it polished? Nope. This beta-ish is really about the engine itself. The MVP functionality is pretty stable. After working on this solo for about 10 months, I decided to put the pens down on it for a bit to concentrate on some other things.

    The UI/UX is a disaster and this is my weakest link skill-wise. I’d be amazed if anyone actually figured out how to use it given the current state of the UI. This post will describe the basic functionality of the Evaluation Engine only.

    Trying it out

    The application is running in the cloud here.

    You can create a user if you like, but it’s probably better for me to create a user for you in the testing area so you can see the demo rules. For a new user, there should be a whole onboarding / getting started flow which I have not done at all yet.

    When you “sign up”, your user is created as well as an Organization for that user. It also creates a sibling API user which provides the Auth token to make API calls with.

    Your user has Admin rights in your org (which lets you add other users). It also lets you view and copy the API auth token.

    Facts In, Results Out

    The rule config structure…

    • Rules contain Solutions.
    • Solutions consist of an optional Condition and a Result.
    • The Engine evaluates Your Fact Model and returns Your Result Model

    Conditions are built with a simple UI but are actually rendered and executed as plain old javascript. You can modify or extend the code as needed with a built-in VS Code editor.

    Results can be static JSON models or generative JavaScript. Like conditions, they have access to Your Facts and can even consume the result of other rules for advanced computations.

    Rules can be organized in an optional container hierarchy. The Evaluation API selects rules by Rule IDs or by Container ID and Rule Type to find the closest matching rule, supporting global defaults with specific overrides.

    An example would be great, Bob

    Let’s say you’re building a fancy service like oh, a rules engine and want to do subscription style billing. You have three subscription levels: Free, StartUp, BigShot. Each tier allows a certain number of included API calls with an overage cost per call and other parameters. At the end of a billing cycle, you want to figure out how much to bill an account. We’ll do this with two rules: One to hold the basic parameters of the subscription tiers and the other to use those parameters to actually compute a cost.

    subscription-features will be the rule with the parameters. subscription-billing will do the computation. The latter will consume the output of the former.

    subscription-features rule is selected and the free-tier solution within that rule is selected

    The subscription-features rule has three solutions (one for each tier). Each solution has a single condition that matches on the subscription level fact. For example, the free-tier solution has this one condition.

    Like I said… it’s very clunky

    The actual condition code is generated by the widgets and shown in the code editor like so:

    JavaScript
    // Declare output object
    var output = {
    	result:false,
    	meta:{} 
    };
    // Set boolean result value
    output.result=$ifsSubscription.level=='Free';

    If that condition evaluates true, the result (static json in this case) will be returned. The solutions for the other two tiers are similar with different values in the result.

    If I edit the Javascript, the condition widgets won’t agree anymore!

    Yep, that’s right but it senses that and indicates that the controls have been overridden. There is also a button to revert the script to bare widgets mode again.

    Here, I’ve added “&& true” to the boolean expression in the generated script. That disabled the widgets and lit up the button to restore the condition.

    Fact Tokens

    Note in the script that the fact token starts with a “$”. It represents a path into the fact object passed in to the evaluation API. Conditions can be written to access any arbitrary fact model you throw at it. Fact tokens are replaced at runtime with the actual fact input value.

    Where do these fact tokens come from?

    The condition editor has a pop-up picker with type-ahead support for browsing and choosing fact tokens. The choices presented are those of several built-in fact models.
    In the grand glorious future, you will be able to paste in any arbitrary fact model and the rule will learn all the attribute paths in that model and present them in the picker. We may even be able to make the VS Code editor aware of them for type-ahead.

    API Facts Input

    In this case, the API will be called with a Facts Model that looks like this:

    JSON
    "facts": {
        "ifsSubscription": {
            "level": "BigShot"
        },
        "ifsTransactionData": {
            "countCurrentBilling": 98750
        }
    }

    So the fact token $ifsSubscription.level will evaluate to “BigShot”. Notice that we’re passing in two facts: the subscription level and the number of evals done in the current billing period.

    The result object of the BigShot solution is

    JSON
    {
        "baseCost": {
            "amount": 79.95,
            "currency": {
                "code": "USD",
                "name": "US Dollar"
            }
        },
        "evalAllowance": {
            "hardLimit": -1,
            "included": 50000
        },
        "allowedUsers": 25,
        "overage": {
            "costPerEval": 0.0075
        }
    }

    This rule itself is not all that interesting. It’s basically a chunk of json that can be accessed by an ID (sounds like mongo db doesn’t it?). Now let’s look at the rule that does the heavy lifting.

    The subscription-billing rule has just one solution. That one solution has no conditions (that is, it is unconditional). The interesting part is the Result which in this case is javascript, not static JSON.

    ActiveState controls

    You’ll notice these widgets all over the place with the “Active” power button and a trash can. Most of the config objects in ifServ have a three state ActiveState attribute: Active/Inactive/Deleted. Inactive objects are removed from the rule execution. Deleted objects are also removed from the UI. Deleted objects are not really deleted but there is no UI to bring them back to life yet. Maybe an admin role can do that some day.

    JavaScript
    // call the sub-info rule
    Logger.log('Calling sub data rule');
    const subInfo = JSON.parse(Rules.evalRule('0HCF6CGRW5R5Z'));
    Logger.log(subInfo);
    
    var overageCount =
        $ifsTransactionData.countCurrentBilling -
        subInfo.evalAllowance.included;
    overageCount = Math.max(0, overageCount);
    
    // Build the output object
    var output = {
        evalCount: $ifsTransactionData.countCurrentBilling,
        currency: subInfo.baseCost.currency,
        baseCost: {
            amount: subInfo.baseCost.amount,
        },
        evalAllowance: {
            level: $ifsSubscription.level,
            hardLimit: subInfo.evalAllowance.hardLimit,
            included: subInfo.evalAllowance.included
        },
        evalOverage: {
            costPer: subInfo.overage.costPerEval,
            count: overageCount,
            cost: overageCount * subInfo.overage.costPerEval
        },
        total: {
            amount: subInfo.baseCost.amount +
                overageCount * subInfo.overage.costPerEval
        }
    }

    The actual computed result is whatever is put into the output object on line 12.

    Is it really the VS Code Editor?

    It’s actually the Monaco Editor which is what VS Code uses. It is aware of the content type (json or javascript) and so gives you all the same syntax help. It also opens up all the same type-ahead bits that you are used to. I think it is possible to even tell it about additional types – for example the helpers we inject or the results from a rule call – to give even better type-ahead support.

    The result script has access to all of the same facts as the conditions. The most interesting thing is what happens on line 3. It’s calling another rule and setting its result to a local const called subInfo. It is doing this through an ifServ helper class called Rules. Rules is one of the many ifServ engine helpers made available to the javascript runtime. The Rules helper lets you call anther rule with all of the same facts as the current rule has. The rule we’re calling here is the simple one above (subscription-features) which just returns the static parameters for the subscription level.

    Where did that Rule ID come from?

    In the rule editor there is a “fingerprint” button you can click to copy the Rule’s ID to the clipboard. This will probably expand to some more generic way of copying rule identifiers.

    We’re also using a second helper called Logger. Anything logged here is returned in the API output in the jsLogs[] array.

    The rest of this result script is pretty straightforward code that is computing the cost given the $ifsTransactionData.countCurrentBilling Fact and the result of the first rule (based on the $ifsSubscription.level fact).

    Result Types

    Solution Results can be either static JSON or generative Javascript. You can switch back and forth between these modes without losing your content.

    You can also convert a JSON type of result to a Javascript type with the click of a button. In either case, the VS Code editor knows which type of content you are editing and gives you all the syntax highlighting and error indication you are used to in that editor.

    Trying it out. The request payload might look like this:

    JSON
    {
        "ruleSelectors": [
            {
                "ruleId": "0HK714YJ4583A"
            }
        ],
        "facts": {
            "ifsSubscription": {
                "level": "BigShot"
            },
            "ifsTransactionData": {
                "countCurrentBilling": 98750
            }
        }
    }

    And a few milliseconds later, the result:

    JSON
    {
        "org": {
            "id": "0HBR7YDHR083A",
            "name": "ifServe Admin Org",
            "activeState": "Active"
        },
        "engineMilliseconds": 54,
        "ruleResults": [
            {
                "ruleId": "0HK714YJ4583A",
                "ruleName": "subscription-billing",
                "evaluatedSolutions": [
                    {
                        "solutionId": "0HK71APM4701P",
                        "conditionCombineLogic": "AllTrue",
                        "trueConditions": [],
                        "solutionResult": {
                            "id": "0HK71APN47006",
                            "resultMode": "Javascript",
                            "computedModel": {
                                "evalOverage": {
                                    "cost": 365.625,
                                    "count": 48750,
                                    "costPer": 0.0075
                                },
                                "baseCost": {
                                    "amount": 79.95
                                },
                                "total": {
                                    "amount": 445.575
                                },
                                "evalCount": 98750,
                                "evalAllowance": {
                                    "level": "BigShot",
                                    "hardLimit": -1,
                                    "included": 50000
                                },
                                "currency": {
                                    "code": "USD",
                                    "name": "US Dollar"
                                }
                            }
                        }
                    }
                ]
            }
        ],
        "contextUid": "9bede0ee-6391-4390-a04e-6596f8683968",
        "jsLogs": [
            "Calling sub data rule",
            "{baseCost: {amount: 79.95, currency: {code: \"USD\", name: \"US Dollar\"}}, evalAllowance: {hardLimit: -1, included: 50000}, allowedUsers: 25, overage: {costPerEval: 0.0075}}"
        ]
    }

    Here’s a CURL of the above request (auth header redacted)

    Bash
    curl --location 'https://api.ifserv.com/ruleapi/v1/eval' \
    --header 'Authorization: Bearer eyJhbGciOiJIUzI*********lKVDY' \
    --header 'Content-Type: application/json' \
    --data '{
        "ruleSelectors": [
            {
                "ruleId": "0HK714YJ4583A"
            }
        ],
        "facts": {
            "ifsSubscription": {
                "level": "BigShot"
            },
            "ifsTransactionData": {
                "countCurrentBilling": 98750
            }
        }
    }'
    Test Rules in the UI?

    Yes it’s planned and should be pretty easy to do. The rule UI will have a place to paste in the “facts” JSON input object. This will probably also double as a rule’s “learn” function for fact tokens so they show up in the picker. A button click will run the facts object through the rule and the computed result will be displayed. That saves you a trip to Postman

    AI (of course)

    I’ve done a few quick experiments with ChatGPT giving it a list of fact tokens and then decribing a simple rule I would like. I was impressed enough with what it came up with that I think it’s worth pursuing a smart “build what I say” function for rules.

    On the flip-side, I have also given ChatGPT the conditions and result object and asked it to explain the rule. Again, I was not unimpressed with the result.

    The Javascript Runtime

    I’m using the GRAAL VM javascript engine. It’s very powerful and I’ve barely scratched the surface. By definition, this app is letting users inject code to run on MY server. That sounds scary from a security standpoint but GRAAL has what seems to be a pretty robust security model. For the beta, I have not explored it much yet.

    Another capability that I have not tried yet is runtime debugging – yes you can actually attach an execution listener and do interactive debugging.

    I don’t even want to think about this yet, but GRAAL also supports Python

    Tech Stack

    Front end is Angular. Back end is SpringBoot (java). Persistence is JPA. Extensive use of Angular Material, Reactive forms and rxjs in the client. User auth is handled with a JWT token and an Auth filter in the Springboot java app.

    All code is in github (private)

    The current state of the dev-ops is as follows: The front end is a docker image with an nginx instance serving up the Angular App. The whole app is a docker compose app with front-end, back-end and db. Application production secrets are stored in github secrets and used by the various gh actions.

    Using github packages for the container repository and also for some shared maven dependencies.

    Deploy dev-ops is very rudimentary. No K8s. No Terraform. We just SCP the compose file up to a server, set some secrets environment variables and start it up.