Skip to main content

Serialization

Policies in @hex-di/guard are data, not callbacks. This is a deliberate design choice that enables serialization, storage, transmission, and introspection.

Why Policies Are Data

Most authorization libraries define policies as functions:

// Callback-based (NOT how @hex-di/guard works)
const policy = subject => subject.permissions.has("read");

Functions can't be serialized, stored in databases, sent over networks, or introspected. @hex-di/guard instead represents policies as discriminated union trees:

// Data-based (how @hex-di/guard works)
const policy = hasPermission(ReadUsers);
// -> { _tag: "hasPermission", permission: "ReadUsers" }

This enables:

  • Database storage -- persist policies alongside resources
  • Network transmission -- send policies to microservices
  • Audit logging -- record the exact policy that was evaluated
  • Admin UIs -- display and edit policies in management dashboards
  • Explanation -- generate human-readable descriptions

serializePolicy()

Converts a policy to a JSON string.

import { serializePolicy } from "@hex-di/guard";

const json = serializePolicy(policy);
// Store in database, send over network, etc.

deserializePolicy()

Converts a JSON string back to a policy. Returns a Result -- deserialization can fail if the JSON is malformed or references unknown policy kinds.

import { deserializePolicy } from "@hex-di/guard";

const restored = deserializePolicy(json);
// Structurally identical to original

Round-Trip

Serialization is lossless -- deserializePolicy(serializePolicy(policy)) produces a structurally identical policy. This is guaranteed by the discriminated union design: every policy node has a _tag and known fields.

explainPolicy()

Generates a human-readable description of a policy tree.

import { explainPolicy } from "@hex-di/guard";

const explanation = explainPolicy(policy);
// "all of: has permission 'ReadUsers', has role 'Admin'"

Useful for:

  • Audit logs that humans need to read
  • Admin UIs showing what a policy does
  • Debugging authorization decisions
  • Error messages explaining why access was denied

Use Cases

Database Storage

// Store policy with a resource
await db.resources.update(resourceId, {
accessPolicy: serializePolicy(policy),
});

// Retrieve and evaluate
const row = await db.resources.get(resourceId);
const policy = deserializePolicy(row.accessPolicy);
const decision = evaluate(policy, subject);

Network Transmission

// API endpoint returns a policy
app.get("/api/resource/:id/policy", (req, res) => {
const policy = getPolicyForResource(req.params.id);
res.json({ policy: serializePolicy(policy) });
});

// Client deserializes and uses it
const response = await fetch(`/api/resource/${id}/policy`);
const { policy: json } = await response.json();
const policy = deserializePolicy(json);

Audit Logging

// Record the exact policy that was evaluated
auditTrail.record({
subjectId: subject.id,
policy: serializePolicy(policy),
decision: decision.granted ? "allow" : "deny",
timestamp: new Date(),
});