Fystack's Policy Engine: Static Permissions Meet Programmable Controls
Thanh Vo
Author

Every authorization system starts with a simple question:
Can this user perform this action?
Static permissions work well for defining responsibilities inside a workspace. A signer can sign. A proposer can propose. An administrator can manage configuration.
That model starts to break down once a request carries risk.
Consider a signer creating a withdrawal. The permission check succeeds, but that alone does not tell us much. Is the destination address new? Is the amount unusually large? Has the workspace already moved significant value today? Is the contract call attempting to grant an unlimited token allowance?
None of those questions are answered by the user's role.
The signer may be authorized to act, yet the request itself may still deserve a different outcome.

That is the problem Fystack's policy engine is designed to solve.
The Limits of RBAC

Role-based access control is good at expressing who can perform an action. It is much less effective at expressing the circumstances under which an action should be allowed.
A treasury team could create separate roles for every combination of amount, destination type, contract method, network, and time window, but that quickly becomes difficult to manage.
Transaction risk is contextual. It depends on the request being made, not just the user making it.
Instead of encoding operational judgment into roles, Fystack evaluates each request against the facts available at runtime.
Further reading:
The Control Model
Fystack does not replace RBAC or approvals. It separates responsibilities between three layers:
- RBAC determines who can initiate an action.
- Policy evaluates the action itself.
- Approval remains the human checkpoint when automation should not have the final word.
The policy engine sits between permission and execution for value-bearing actions.

A request must first pass RBAC. Fystack then builds an evaluation payload and asks the policy engine for a decision.
An example of a programmable policy in Fystack, inspired by AWS policies.
doc := policy.Document{
DefaultEffect: &defaultEffect,
Policies: []policy.Policy{
{
Name: "withdrawal-approval",
Rules: []policy.Rule{
{
ID: "block_large",
Effect: policy.EffectDeny,
Condition: "ValueUSD > 50000",
},
{
ID: "auto_approve_small_whitelisted",
Effect: EffectAutoApprove,
Condition: "IsWhitelisted && ValueUSD < 5000",
},
{
ID: "flag_medium",
Effect: EffectFlagReview,
Condition: "ValueUSD >= 5000 && ValueUSD <= 50000",
},
{
ID: "allow_whitelisted",
Effect: policy.EffectAllow,
Condition: "IsWhitelisted",
},
},
},
},
}
engine, err := policy.CompileDocument(doc)
if err != nil {
panic(err)
}
input := map[string]any{
"ValueUSD": 3500.0,
"IsWhitelisted": true,
}
decision := engine.Evaluate(context.Background(), input)
switch decision.Effect {
case policy.EffectDeny:
fmt.Println("blocked:", decision.Message)
case EffectAutoApprove:
fmt.Println("auto-approved — no manual review needed")
case EffectFlagReview:
fmt.Println("flagged for manual review")
case policy.EffectAllow:
fmt.Println("allowed — requires standard approval")
}The outcome is intentionally simple
DENY
ALLOW
NO MATCH
DENY blocks the action immediately. The request never becomes a withdrawal, signing job, or any other operation that can move assets.
ALLOW means the workspace has defined an explicit rule that permits the action to bypass the approval queue. The request still had to pass RBAC, and the rule still had to match the runtime facts, but no human review is required.
If no rule matches, the policy engine stays silent and Fystack follows the normal approval flow. This is an important distinction: no match is not an implicit allow. It simply falls back to the workspace's standard governance process.
Conflict Resolution
When multiple rules apply, custody-safe behavior takes priority.
DENY > ALLOW
Within the same effect, declaration order provides deterministic matching so operators can identify exactly which rule produced the decision.
Writing Policies
A rule follows the shape most operators already understand:
IF condition matches
THEN effect is ALLOW or DENY

Withdrawal Controls
For withdrawals, conditions typically reference amount, destination, whitelist state, velocity, and time:

Contract-Call Controls
For contract calls, the policy can evaluate method identity, contract metadata, gas parameters, and decoded calldata:
DENY resource.method_name == "approve"
DENY resource.contract_address == "0x..."
ALLOW resource.method_name == "transfer"
Before a rule is stored, Fystack validates that:
- the trigger action exists
- the effect is valid
- the condition is present
- every referenced field belongs to the action schema
Invalid policy never reaches the approval process.
Evaluating a Request
The evaluator receives a normalized payload regardless of action type.

Every request includes workspace and action information, with additional sections such as withdrawal, wallet, resource, and context populated as needed.
Evaluation follows a fixed path:
Load policy bundle
→ Match trigger action
→ Apply wallet targeting
→ Evaluate conditions
→ Resolve conflicts
→ Return decision
The result is a small decision object that answers one question:
Should this action be blocked, approved automatically, or continue through approval?

The response also contains enough context for explanation. Instead of returning a generic rejection, Fystack can identify the policy and rule responsible for the outcome.
Governing Policy Changes
Programmable controls need governance.

A policy can block transactions, but it can also remove approval requirements. That makes policy changes security-sensitive operations in their own right.
For that reason, policy changes follow the same review process as other high-impact actions.
Revisions
New policies and updates are proposed as revisions rather than becoming active immediately.
Draft
→ Revision
→ Approval
→ Active Policy
Diffs
Each revision records exactly what changed, including policy fields and individual rules.
Reviewers can inspect both the resulting policy and the diff against the previous version before approving it.
Auditability
Once approved, the new version becomes active, the policy version advances, and the change is recorded in the audit trail.
The result is a system where both policy decisions and policy changes remain visible, reviewable, and accountable.
Operating the System
The operating model is straightforward:
- Use roles to determine who can act.
- Use policy to evaluate the request.
- Use approval when the policy layer does not have an explicit answer.

A treasury team might begin with only a handful of rules:
DENY withdrawal.value_usd > 10000 && withdrawal.is_whitelisted == false
ALLOW withdrawal.value_usd < 1000 && withdrawal.is_whitelisted == true
DENY resource.method_name == "approve"
The goal is not to encode every possible decision in policy. Start with a few controls that represent obvious risk or obvious safety, then let everything else continue through the normal approval process.
Over time, teams can expand policy coverage where automation adds value while keeping human review for the cases that remain ambiguous.
Why This Matters for Custody
Permissions alone cannot distinguish between a routine transaction and an unusual one.
In custody systems, that distinction matters. The user may be authorized, but the request still needs to be evaluated against the context in which it occurs.
That is the role of policy.
Conclusion
A signer may be allowed to create a withdrawal.
Whether that withdrawal should proceed is a different question.
Permissions can establish who is authorized to act, but they cannot evaluate the details of the request itself. By the time money is about to move, factors such as destination, amount, transaction history, contract method, and broader activity patterns may matter more than the role attached to the user.
Fystack's policy engine exists to evaluate those runtime facts before execution. It gives workspaces a way to express operational judgment explicitly, while preserving approval workflows for cases where automation should not make the final decision.
The result is a system that can reason about transactions, not just users, before money moves.
We've open-sourced Fystack's core Programmable Policy Engine. Explore it here: https://github.com/fystack/programmable-policy-engine.
Have questions about your custody setup? Share what you are building via the form and explore how Fystack’s MPC wallets, KYT integrations, and consolidation engine fit your architecture.
Not ready yet? Join our Telegram for product updates and architecture discussions: https://t.me/+9AtC0z8sS79iZjFl

