Back to Blog

Fystack's Policy Engine: Static Permissions Meet Programmable Controls

T

Thanh Vo

Author

June 15, 2026
6 min read
Fystack's Policy Engine: Static Permissions Meet Programmable Controls

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.

RBAC and Policies

That is the problem Fystack's policy engine is designed to solve.

The Limits of RBAC

RBAC's limit

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.

Programmable Policy Control Model

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.

Go
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

Plain Text
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.

Plain Text
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:

Plain Text
IF condition matches
THEN effect is ALLOW or DENY
Policy as Code model

Withdrawal Controls

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

Policy Builder

Contract-Call Controls

For contract calls, the policy can evaluate method identity, contract metadata, gas parameters, and decoded calldata:

Plain Text
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.

Evaluation Contract

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:

Plain Text
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?
Policy rejects withdrawal

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.

Plain Text
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.
Policy Operating model

A treasury team might begin with only a handful of rules:

Plain Text
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

Share this post