Blog
Explainers

Building an Authorization Rules Engine

Blog
Explainers

Building an Authorization Rules Engine

Mirek Klimos
Rik Heijdens
February 12, 2025
 • 
#
 min read

Card authorization - the process of approving or declining card transactions - is at the heart of any card program. While the basic concept is straightforward - checking if a customer has sufficient funds - the reality involves complex decision-making: detecting fraud patterns, enforcing spending limits, ensuring regulatory compliance, and respecting cardholder preferences.

Over the past decade of managing authorization rules for Privacy.com, we've learned firsthand just how intricate card program authorization can be. This experience has taught us many lessons about the nature of authorization data, what makes rules truly effective, and what doesn't work. It also revealed a new opportunity: build better tooling for managing authorization rules for card program managers.

When we began rebuilding our authorization infrastructure, we decided to take that opportunity and build something better - not just for our internal needs, but for all our customers. In this post, we’ll discuss the outcome of this process, Lithic’s Rules Engine, which sits at the center of our authorization infrastructure and powers our recently launched Auth Rules - a system we hope can grow into a valuable tool for all customers, either instead of, or in addition to, our Auth Stream Access (ASA) product. For more context on Lithic’s role in card authorization processing, see Architecting a Modern Card Authorization Platform.

Design objectives

Our Rules Engine and Auth Rules product were designed with five core objectives in mind:

  • Lithic-managed versus customer-managed rules: Today, many of our customers use Auth Stream Access (ASA), which requires them to build and maintain their own webhook responder for authorization logic. While ASA offers maximum flexibility, it puts the burden on our customers to build and maintain their own, highly-available responder - a substantial engineering investment. Our objective was to create a built-in rules system where customers could configure their business logic directly within Lithic. This reduces the engineering investment required to launch new card programs and removes the burden of maintaining high-availability systems - Lithic handles all the execution, scalability, and reliability. This approach aligns well with our commitment to "Insanely Fast Card Issuing" - enabling customers to implement authorization rules just as quickly as they can issue cards. 
  • Flexibility: Rules engines often force you into overly simple patterns (AND/OR conditions), or require learning a bespoke domain-specific language (DSL). Our goal was to support complex, real-world authorization scenarios - like combining multiple risk signals or implementing custom risk scoring models - without the need for a special DSL.
  • Reliability: Authorization systems must be rock-solid - our core processing already delivers better than 99.99% availability, and Auth Rules need to match this bar. This means building a system that's not just reliable and performant, but also operates redundantly across multiple geographic regions. We know this is not an easy task - we regularly observe availability issues with many of our customer’s ASA endpoints.
  • Transparency: We wanted to solve one of the most common customer frustrations: understanding why a card transaction was declined. Our goal was providing clear visibility into every decline decision - the list of rules that declined, with both machine-readable decline codes for automated processing and human-readable explanations. This would enable faster troubleshooting and more effective customer support.
  • Testability: Rule logic needs to be adjusted from time to time. Over the years, we've seen rule changes backfire in two ways: becoming too restrictive and causing unnecessary friction for cardholders, or becoming too permissive and creating security vulnerabilities. A critical objective was building testing capabilities directly into the system - specifically, the ability to run rules in shadow mode and perform backtesting against historical transactions. These capabilities needed to be core features, not afterthoughts.

Rules Engine Architecture

We built Rules Engine following a classic control plane/data plane architecture, split into two core services:

The Rules-manager service acts as our control plane, handling all rule administration. It maintains critical metadata about rules: their definitions, ownership, evaluation triggers, and current status. This service ensures rules are properly configured and available where needed.

The Rules-runner service serves as our data plane, executing rules in real-time during authorization processing. It receives requests from our authorization service, evaluates requested rules, and returns decisions.

For rule implementation, we decided to use WebAssembly (WASM) for several key reasons:

  • Security and control: WASM modules run in a sandbox, critical for both security and accurate backtesting
  • Development flexibility: We let developers write rules in any language that compiles to WebAssembly
  • Performance: WASM enables fast rule execution while supporting arbitrary logic

We also explored utilizing modern JavaScript runtimes (such as V8), but decided that WebAssembly is more flexible and better suited for our use case. Shopify’s precedent with Functions gave us confidence that this is the right direction for the long term.

Example: Conditional Block Auth Rule

Let's explore how our Rules Engine works through a practical example: protecting against high-risk transactions on frequently-used cards. This example shows a common fraud prevention pattern where we combine multiple risk signals to make smarter authorization decisions. 

We'll create a rule that blocks authorizations if two conditions are met: 

  1. The network-provided risk score exceeds 500 (indicating potentially suspicious activity)
  2. The card has been used more than 10 times in the last 24 hours (suggesting an unusual usage pattern or card details misuse)

While our Rules Engine internally uses WebAssembly modules for rule execution, our external Auth Rules product takes a simpler approach. Instead of asking customers to write code, we provide pre-built templates for common use cases. Each template (in this example, we'll use CONDITIONAL_BLOCK) is backed by a single WASM module that we've optimized for that specific use case. Customers configure these templates with their parameters, and at runtime, we pass these parameters to the underlying WASM module.

Here's what happens when a customer creates a rule:

  1. API Request. The process begins when a customer sets up a rule - either in Lithic Dashboard, or by making a direct request to Lithic’s public API. Our API gateway forwards this request to the Rules-manager service.
  2. Rule Registration. Rules-manager validates the request and proceeds to register a new authorization rule if successful. It looks up the WASM module for CONDITIONAL_BLOCK template and stores the necessary input arguments. Reusing a WASM module in this way is much more efficient than creating a new module for every rule.
  3. Evaluation preparation. When a rule is registered, we need to make sure that the associated WebAssembly module is ready to be evaluated by Rules-runner - this means it has to be available in all geographical regions and ideally populated in Rules-runner caches for fast retrieval. For Auth Rules, this typically only happens the first time the rule template is instantiated.
  4. API Response. When everything is set up, Rules-manager returns the created Auth Rules object, with a unique token that customers can use to manage the rule’s configuration going forward.

Once created, the rule becomes part of the authorization decision process for all future transactions matching its criteria. When an authorization request arrives, our system performs the following steps:

  1. Rule Discovery. Lithic’s authorization service consults Rules-manager to determine which rules need to be evaluated. Rules-manager checks the rules' status and applicability, then returns a list of rules that should be evaluated for this specific request. This includes the newly created rule.
  2. Data Collection. Before evaluation can begin, we need to gather all the necessary data. Rules can specify required features in their configuration - these are pieces of data needed for evaluation beyond the basic authorization request. In our example, the rule needs to know the number of transactions on the card in the last 24 hours. The authorization service collects this information from our spend-velocity service before proceeding.
  3. Rule Execution. With the inputs ready, the authorization service calls Rules-runner to evaluate all the applicable rules. Rules-runner uses the Wasmtime runtime to execute each rule's WebAssembly module with the authorization data and any additional features. Rule execution must complete within a strict timeout to maintain fast authorization processing.
  4. Logging and Reporting. Every rule evaluation is recorded in an append-only log, which feeds into our data warehouse. This is crucial for troubleshooting, generating rule performance reports, backtesting, and optimizing rule performance in general.

Backtesting

The easiest way to test rule changes is through shadow mode. When a rule runs in shadow mode, it is evaluated for authorization requests in the same way as active rules, but the outcome of the evaluation is only logged and not used for making the final authorization decision. This lets us assess the impact of the rule change without any risk to live transactions.

While shadow mode is valuable, it requires patience - we need to wait for enough authorizations to flow through the system to evaluate the rule's performance. That’s where backtesting comes in: instead of running rules side-by-side on incoming new authorization events, we replay a rule against historical events. This leads to a much shorter feedback cycle than running the rule in shadow mode.

For backtesting to work correctly, three prerequisites must be satisfied:

  1. Rules evaluation must be deterministic - rules must produce consistent outputs given the same inputs. This includes any logic dependent on randomness or system time. The Wasmtime runtime provides us with fine-grained control over the execution environment and allows us to seed random number generators and fix the system time.
  2. We need access to all data features required by the rule. This is fairly straightforward for the basic authorization request, but not trivial for temporal features such as spend velocity. For some of those advanced features, backtesting is only partially supported.
  3. We need to identify applicable historical events. Lithic’s Rules Engine solves this through "rule bindings" - a system that lets us associate rules with specific cards, accounts, customer segments, or transaction types. When running a backtest, we use these bindings to select the appropriate historical events to evaluate.

Backtesting runs as an asynchronous batch job through our job-scheduling system. When compute resources become available, the scheduler spins up a Backtest-processor container which:

  1. Loads the configuration of the backtest. The configuration specifies which rules should be backtested and over what timeframe. Using the rules' bindings, it determines which historical events should be included in the test.
  2. Loads and transforms the historical data from Lithic's data warehouse. For some rules, the Backtest-processor may need to re-derive certain inputs that weren't historically stored, reconstructing what they would have been at the time of the original transaction.
  3. Evaluates all specified rules using the inputs from the previous step, recording each result for analysis.
  4. Aggregates and stores the results in a database. When complete, it sends a signal that triggers a webhook to customers in the case of Auth Rules.

Future work

We’re only getting started with our Rules Engine platform. We plan to improve Auth Rules along three axes: more flexibility, more features, and more flows.

First, the current CONDITIONAL_BLOCK and VELOCITY_LIMIT templates serve many common use cases, but we want to give our customers even more control. Our vision is to enable customers to bring their own code, allowing for truly customized rule logic that fits their specific needs. 

Second, we want to offer more features, particularly in the fraud prevention space. To better prevent fraud, we can make richer signals available as inputs to rules: typical spend patterns of cardholders, history of merchant relationships, geographic patterns of physical card usage, etc.

Third, we plan to expand the number of use-cases for our Rules Engine. Internally, we already use our Rules Engine for authorization requests and 3DS authentication. In the future, we hope to integrate this into other processes that need per-customer customization, such as tokenization decisioning.

Conclusion

In this post, we’ve explored Lithic’s Rules Engine and how it both powers our Auth Rules product and supports internal use-cases. Thanks to WebAssembly, we can express arbitrary rule logic, provide powerful backtesting functionality, and evaluate customer-provided rules in a sensitive environment in a safe way.

Auth Rules represents our first step in exposing the power of the Rules Engine to our customers. You can use it today via API or via Lithic Dashboard. We're excited to see how you'll use it and we're eager for your feedback. Whether you have feature requests, innovative use cases, or general thoughts on the system, please reach out. And if you're interested in helping us build the future of card issuing infrastructure, we're hiring!

Want a payments platform that helps you as you grow?