POLICY_USAGE_GUIDE.md raw

ORLY Policy System Usage Guide

The ORLY relay implements a comprehensive policy system that provides fine-grained control over event storage and retrieval. This guide explains how to configure and use the policy system to implement custom relay behavior.

Overview

The policy system allows relay operators to:

Quick Start

1. Enable the Policy System

Set the environment variable to enable policy checking:

export ORLY_POLICY_ENABLED=true

2. Create a Policy Configuration

Create the policy file at ~/.config/ORLY/policy.json:

{
  "default_policy": "allow",
  "global": {
    "max_age_of_event": 86400,
    "max_age_event_in_future": 300,
    "size_limit": 100000
  },
  "rules": {
    "1": {
      "description": "Text notes - basic validation",
      "max_age_of_event": 3600,
      "size_limit": 32000
    }
  }
}

3. Restart the Relay

# Restart your relay to load the policy
sudo systemctl restart orly

Configuration Structure

Top-Level Configuration

{
  "default_policy": "allow|deny",
  "kind": {
    "whitelist": ["1", "3", "4"],
    "blacklist": []
  },
  "global": { ... },
  "rules": { ... },
  "owners": ["hex_pubkey_1", "hex_pubkey_2"],
  "policy_admins": ["hex_pubkey_1", "hex_pubkey_2"],
  "policy_follow_whitelist_enabled": true
}

default_policy

Determines the fallback behavior when no specific rules apply:

kind Filtering

Controls which event kinds are processed:

"kind": {
  "whitelist": ["1", "3", "4", "9735"],
  "blacklist": []
}

owners

Specifies relay owners via the policy configuration file. This is particularly useful for cloud deployments where environment variables cannot be modified at runtime.

{
  "owners": [
    "4a93c5ac0c6f49d2c7e7a5b8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
    "5b84d6bd1d7e5a3d8e8b6c9e0f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0"
  ]
}

Key points:

Example use case - Cloud deployment:

When deploying to a cloud platform where you cannot set environment variables:

  1. Create ~/.config/ORLY/policy.json:
{
  "default_policy": "allow",
  "owners": ["your_hex_pubkey_here"]
}
  1. Enable the policy system:
export ORLY_POLICY_ENABLED=true

The relay will recognize your pubkey as an owner, granting full administrative access.

Global Rules

Rules that apply to all events regardless of kind:

"global": {
  "description": "Site-wide security rules",
  "write_allow": [],
  "write_deny": [],
  "read_allow": [],
  "read_deny": [],
  "size_limit": 100000,
  "content_limit": 50000,
  "max_age_of_event": 86400,
  "max_age_event_in_future": 300,
  "privileged": false
}

Kind-Specific Rules

Rules that apply to specific event kinds:

"rules": {
  "1": {
    "description": "Text notes",
    "write_allow": [],
    "write_deny": [],
    "read_allow": [],
    "read_deny": [],
    "size_limit": 32000,
    "content_limit": 10000,
    "max_age_of_event": 3600,
    "max_age_event_in_future": 60,
    "privileged": false
  }
}

Policy Fields

Access Control

writeallow / writedeny

Control who can publish events:

{
  "write_allow": ["npub1allowed...", "npub1another..."],
  "write_deny": ["npub1blocked..."]
}

readallow / readdeny

Control who can read events:

{
  "read_allow": ["npub1trusted..."],
  "read_deny": ["npub1suspicious..."]
}

Size Limits

size_limit

Maximum total event size in bytes:

{
  "size_limit": 32000
}

Includes ID, pubkey, sig, tags, content, and metadata.

content_limit

Maximum content field size in bytes:

{
  "content_limit": 10000
}

Only applies to the content field.

Age Validation

maxageof_event

Maximum age of events in seconds (prevents replay attacks):

{
  "max_age_of_event": 3600
}

Events older than current_time - max_age_of_event are rejected.

maxageeventinfuture

Maximum time events can be in the future in seconds:

{
  "max_age_event_in_future": 300
}

Events with created_at > current_time + max_age_event_in_future are rejected.

Advanced Options

privileged

Require events to be authored by authenticated users or contain authenticated users in p-tags:

{
  "privileged": true
}

Useful for private content that should only be accessible to specific users.

script

Path to a custom script for complex validation logic:

{
  "script": "/path/to/custom-policy.sh"
}

See the script section below for details.

New Policy Rule Fields (v0.32.0+)

maxexpiryduration

Specifies the maximum allowed expiry time using ISO-8601 duration format. Events must have an expiration tag within this duration from their created_at time.

{
  "max_expiry_duration": "P7D"
}

ISO-8601 Duration Format: P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S

Examples:

Example - Ephemeral notes with 24-hour expiry:

{
  "rules": {
    "20": {
      "description": "Ephemeral events must expire within 24 hours",
      "max_expiry_duration": "P1D"
    }
  }
}

Note: This field takes precedence over the deprecated max_expiry (which uses raw seconds).

protected_required

Requires events to have a - tag (NIP-70 protected events). Protected events signal that they should only be published to relays that enforce access control.

{
  "protected_required": true
}

Example - Require protected tag for DMs:

{
  "rules": {
    "4": {
      "description": "Encrypted DMs must be protected",
      "protected_required": true,
      "privileged": true
    }
  }
}

This ensures clients mark their sensitive events appropriately for access-controlled relays.

identifier_regex

A regex pattern that d tag identifiers must conform to. This is useful for enforcing consistent identifier formats for replaceable events.

{
  "identifier_regex": "^[a-z0-9-]{1,64}$"
}

Example patterns:

Example - Long-form content with slug identifiers:

{
  "rules": {
    "30023": {
      "description": "Long-form articles with URL-friendly slugs",
      "identifier_regex": "^[a-z0-9-]{1,64}$"
    }
  }
}

Note: If identifier_regex is set, events MUST have at least one d tag, and ALL d tags must match the pattern.

followswhitelistadmins

Specifies admin pubkeys (hex-encoded) whose follows are whitelisted for this specific rule. Unlike WriteAllowFollows which uses the global PolicyAdmins, this allows per-rule admin configuration.

{
  "follows_whitelist_admins": ["hex_pubkey_1", "hex_pubkey_2"]
}

Example - Community-curated content:

{
  "rules": {
    "30023": {
      "description": "Long-form articles from community curators' follows",
      "follows_whitelist_admins": [
        "4a93c5ac0c6f49d2c7e7a5b8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
        "5b84d6bd1d7e5a3d8e8b6c9e0f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0"
      ]
    }
  }
}

Integration with application: At startup, the application should:

  1. Call policy.GetAllFollowsWhitelistAdmins() to get all admin pubkeys
  2. Load kind 3 (follow list) events for each admin
  3. Call policy.UpdateRuleFollowsWhitelist(kind, follows) or policy.UpdateGlobalFollowsWhitelist(follows) to populate the cache

Note: The relay will NOT automatically fail to start if follow list events are missing. The application layer should implement this validation if desired.

Combining New Fields

The new fields can be combined with each other and with existing fields:

Example - Strict long-form content policy:

{
  "default_policy": "deny",
  "rules": {
    "30023": {
      "description": "Curated long-form articles with strict requirements",
      "max_expiry_duration": "P30D",
      "protected_required": true,
      "identifier_regex": "^[a-z0-9-]{1,64}$",
      "follows_whitelist_admins": ["curator_pubkey_hex"],
      "tag_validation": {
        "t": "^[a-z0-9-]{1,32}$"
      },
      "size_limit": 100000,
      "content_limit": 50000
    }
  }
}

This policy:

Example - Global protected requirement with per-kind overrides:

{
  "default_policy": "allow",
  "global": {
    "protected_required": true,
    "max_expiry_duration": "P7D"
  },
  "rules": {
    "1": {
      "description": "Text notes - shorter expiry",
      "max_expiry_duration": "P1D"
    },
    "0": {
      "description": "Metadata - no expiry requirement",
      "max_expiry_duration": ""
    }
  }
}

Policy Scripts

For complex validation logic, use custom scripts that receive events via stdin and return decisions via stdout.

Script Interface

Input: JSON event objects, one per line:

{
  "id": "event_id",
  "pubkey": "author_pubkey",
  "kind": 1,
  "content": "Hello, world!",
  "tags": [["p", "recipient"]],
  "created_at": 1640995200,
  "sig": "signature"
}

Additional fields provided:

Output: JSONL responses:

{"id": "event_id", "action": "accept", "msg": ""}
{"id": "event_id", "action": "reject", "msg": "Blocked content"}
{"id": "event_id", "action": "shadowReject", "msg": ""}

Actions

Example Scripts

Bash Script

#!/bin/bash
while read -r line; do
    if [[ -n "$line" ]]; then
        event_id=$(echo "$line" | jq -r '.id')

        # Check for spam content
        if echo "$line" | jq -r '.content' | grep -qi "spam"; then
            echo "{\"id\":\"$event_id\",\"action\":\"reject\",\"msg\":\"Spam detected\"}"
        else
            echo "{\"id\":\"$event_id\",\"action\":\"accept\",\"msg\":\"\"}"
        fi
    fi
done

Python Script

#!/usr/bin/env python3
import json
import sys

def process_event(event):
    event_id = event.get('id', '')
    content = event.get('content', '')
    pubkey = event.get('pubkey', '')
    logged_in = event.get('logged_in_pubkey', '')

    # Block spam
    if 'spam' in content.lower():
        return {
            'id': event_id,
            'action': 'reject',
            'msg': 'Content contains spam'
        }

    # Require authentication for certain content
    if 'private' in content.lower() and not logged_in:
        return {
            'id': event_id,
            'action': 'reject',
            'msg': 'Authentication required'
        }

    return {
        'id': event_id,
        'action': 'accept',
        'msg': ''
    }

for line in sys.stdin:
    if line.strip():
        try:
            event = json.loads(line)
            response = process_event(event)
            print(json.dumps(response))
            sys.stdout.flush()
        except json.JSONDecodeError:
            continue

Script Configuration

Place scripts in a secure location and reference them in policy:

{
  "rules": {
    "1": {
      "script": "/etc/orly/policy/text-note-policy.py",
      "description": "Custom validation for text notes"
    }
  }
}

Ensure scripts are executable and have appropriate permissions.

Script Requirements and Best Practices

Critical Requirements

1. Output Only JSON to stdout

Scripts MUST write ONLY JSON responses to stdout. Any other output (debug messages, logs, etc.) will break the JSONL protocol and cause errors.

Debug Output: Use stderr for debug messages - all stderr output from policy scripts is automatically logged to the relay log with the prefix [policy script /path/to/script].

// ❌ WRONG - This will cause "broken pipe" errors
console.log("Policy script starting...");  // This goes to stdout!
console.log(JSON.stringify(response));     // Correct

// ✅ CORRECT - Use stderr or file for debug output
console.error("Policy script starting...");  // This goes to stderr (appears in relay log)
fs.appendFileSync('/tmp/policy.log', 'Starting...\n');  // This goes to file (OK)
console.log(JSON.stringify(response));      // Stdout for JSON only

2. Flush stdout After Each Response

Always flush stdout after writing a response to ensure immediate delivery:

# Python
print(json.dumps(response))
sys.stdout.flush()  # Critical!
// Node.js (usually automatic, but can be forced)
process.stdout.write(JSON.stringify(response) + '\n');

3. Run as a Long-Lived Process

Scripts should run continuously, reading from stdin in a loop. They should NOT:

// ✅ CORRECT - Long-lived process
const readline = require('readline');
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

rl.on('line', (line) => {
  const event = JSON.parse(line);
  const response = processEvent(event);
  console.log(JSON.stringify(response));
});

4. Handle Errors Gracefully

Always catch errors and return a valid JSON response:

rl.on('line', (line) => {
  try {
    const event = JSON.parse(line);
    const response = processEvent(event);
    console.log(JSON.stringify(response));
  } catch (err) {
    // Log to stderr or file, not stdout!
    console.error(`Error: ${err.message}`);

    // Return reject response
    console.log(JSON.stringify({
      id: '',
      action: 'reject',
      msg: 'Policy script error'
    }));
  }
});

5. Response Format

Every response MUST include these fields:

{
  "id": "event_id",      // Must match input event ID
  "action": "accept",    // Must be: accept, reject, or shadowReject
  "msg": ""              // Required (can be empty string)
}

Common Issues and Solutions

Broken Pipe Error

ERROR: policy script /path/to/script.js stdin closed (broken pipe)

Causes:

Solutions:

  1. Remove ALL console.log() statements except JSON responses
  2. Use console.error() or log files for debugging
  3. Add error handling to catch and log exceptions
  4. Ensure script runs continuously (doesn't exit)

Response Timeout

WARN: policy script /path/to/script.js response timeout - script may not be responding correctly

Causes:

Solutions:

  1. Add sys.stdout.flush() (Python) after each response
  2. Optimize processing logic to be faster
  3. Check that script is reading from stdin correctly
  4. Remove debug output from stdout

Invalid JSON Response

ERROR: failed to parse policy response from /path/to/script.js
WARN: policy script produced non-JSON output on stdout: "Debug message"

Solutions:

  1. Validate JSON before outputting
  2. Use a JSON library, don't build strings manually
  3. Move debug output to stderr or files

Testing Your Script

Before deploying, test your script:

# 1. Test basic functionality
echo '{"id":"test123","pubkey":"abc","kind":1,"content":"test","tags":[],"created_at":1234567890,"sig":"def"}' | node policy-script.js

# 2. Check for non-JSON output
echo '{"id":"test123","pubkey":"abc","kind":1,"content":"test","tags":[],"created_at":1234567890,"sig":"def"}' | node policy-script.js 2>/dev/null | jq .

# 3. Test error handling
echo 'invalid json' | node policy-script.js

Expected output (valid JSON only):

{"id":"test123","action":"accept","msg":""}

Node.js Example (Complete)

#!/usr/bin/env node

const readline = require('readline');

// Use stderr for debug logging - appears in relay log automatically
function debug(msg) {
  console.error(`[policy] ${msg}`);
}

// Create readline interface
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

debug('Policy script started');

// Process each event
rl.on('line', (line) => {
  try {
    const event = JSON.parse(line);
    debug(`Processing event ${event.id}, kind: ${event.kind}, access: ${event.access_type}`);

    // Your policy logic here
    const action = shouldAccept(event) ? 'accept' : 'reject';

    if (action === 'reject') {
      debug(`Rejected event ${event.id}: policy violation`);
    }

    // ONLY JSON to stdout
    console.log(JSON.stringify({
      id: event.id,
      action: action,
      msg: action === 'reject' ? 'Policy rejected' : ''
    }));

  } catch (err) {
    debug(`Error: ${err.message}`);

    // Still return valid JSON
    console.log(JSON.stringify({
      id: '',
      action: 'reject',
      msg: 'Policy script error'
    }));
  }
});

rl.on('close', () => {
  debug('Policy script stopped');
});

function shouldAccept(event) {
  // Your policy logic
  if (event.content.toLowerCase().includes('spam')) {
    return false;
  }

  // Different logic for read vs write
  if (event.access_type === 'write') {
    // Write control logic
    return event.content.length < 10000;
  } else if (event.access_type === 'read') {
    // Read control logic
    return true; // Allow all reads
  }

  return true;
}

Relay Log Output Example:

INFO [policy script /home/orly/.config/ORLY/policy.js] [policy] Policy script started
INFO [policy script /home/orly/.config/ORLY/policy.js] [policy] Processing event abc123, kind: 1, access: write
INFO [policy script /home/orly/.config/ORLY/policy.js] [policy] Processing event def456, kind: 1, access: read

Event Fields

Scripts receive additional context fields:

{
  "id": "event_id",
  "pubkey": "author_pubkey",
  "kind": 1,
  "content": "Event content",
  "tags": [],
  "created_at": 1234567890,
  "sig": "signature",
  "logged_in_pubkey": "authenticated_user_pubkey",
  "ip_address": "127.0.0.1",
  "access_type": "read"
}

access_type values:

Use this to implement different policies for reads vs writes.

Policy Evaluation Order

Events are evaluated in this order:

  1. Global Rules - Applied first to all events
  2. Kind Filtering - Whitelist/blacklist check
  3. Kind-specific Rules - Rules for the event's kind
  4. Script Rules - Custom script logic (if configured)
  5. Default Policy - Fallback behavior

The first rule that makes a decision (allow/deny) stops evaluation.

Event Processing Integration

Write Operations (EVENT)

When ORLY_POLICY_ENABLED=true, each incoming EVENT is checked:

// Pseudo-code for policy integration
func handleEvent(event *Event, client *Client) {
    decision := policy.CheckPolicy("write", event, client.Pubkey, client.IP)
    if decision.Action == "reject" {
        client.SendOK(event.ID, false, decision.Message)
        return
    }
    if decision.Action == "shadowReject" {
        client.SendOK(event.ID, true, "")
        return
    }
    // Store event
    storeEvent(event)
    client.SendOK(event.ID, true, "")
}

Read Operations (REQ)

Events returned in REQ responses are filtered:

func handleReq(filter *Filter, client *Client) {
    events := queryEvents(filter)
    filteredEvents := []Event{}

    for _, event := range events {
        decision := policy.CheckPolicy("read", &event, client.Pubkey, client.IP)
        if decision.Action != "reject" {
            filteredEvents = append(filteredEvents, event)
        }
    }

    sendEvents(client, filteredEvents)
}

Common Use Cases

Basic Spam Filtering

{
  "global": {
    "max_age_of_event": 86400,
    "size_limit": 100000
  },
  "rules": {
    "1": {
      "script": "/etc/orly/scripts/spam-filter.sh",
      "max_age_of_event": 3600,
      "size_limit": 32000
    }
  }
}

Private Relay

{
  "default_policy": "deny",
  "global": {
    "write_allow": ["npub1trusted1...", "npub1trusted2..."],
    "read_allow": ["npub1trusted1...", "npub1trusted2..."]
  }
}

Content Moderation

{
  "rules": {
    "1": {
      "script": "/etc/orly/scripts/content-moderation.py",
      "description": "AI-powered content moderation"
    }
  }
}

Rate Limiting

{
  "global": {
    "script": "/etc/orly/scripts/rate-limiter.sh"
  }
}

Follows-Based Access

Combined with ACL system:

export ORLY_ACL_MODE=follows
export ORLY_ADMINS=npub1admin1...,npub1admin2...
export ORLY_POLICY_ENABLED=true

Monitoring and Debugging

Log Messages

Policy decisions are logged:

policy allowed event <id>
policy rejected event <id>: reason
policy filtered out event <id> for read access

Script Health

Script failures are logged:

policy rule for kind <N> is inactive (script not running), falling back to default policy (allow)
policy rule for kind <N> failed (script processing error: timeout), falling back to default policy (allow)

Testing Policies

Use the policy test tools:

# Test policy with sample events
./scripts/run-policy-test.sh

# Test policy filter integration
./scripts/run-policy-filter-test.sh

Debugging Scripts

Test scripts independently:

# Test script with sample event
echo '{"id":"test","kind":1,"content":"test message"}' | ./policy-script.sh

# Expected output:
# {"id":"test","action":"accept","msg":""}

Performance Considerations

Script Performance

Memory Usage

Scaling

Security Considerations

Script Security

Access Control

Data Validation

Troubleshooting

Policy Not Loading

Check file permissions and path:

ls -la ~/.config/ORLY/policy.json
cat ~/.config/ORLY/policy.json

Scripts Not Working

Verify script is executable and working:

ls -la /path/to/script.sh
./path/to/script.sh < /dev/null

Unexpected Behavior

Enable debug logging:

export ORLY_LOG_LEVEL=debug

Check logs for policy decisions and errors.

Common Issues

  1. Script timeouts: Increase script timeouts or optimize script performance
  2. Memory issues: Reduce script memory usage or use built-in rules
  3. Permission errors: Fix file permissions on policy files and scripts
  4. Configuration errors: Validate JSON syntax and field names

Dynamic Policy Configuration via Kind 12345

Both owners and policy admins can update the relay policy dynamically by publishing kind 12345 events. This enables runtime policy changes without relay restarts, with different permission levels for each role.

Role Hierarchy and Permissions

ORLY uses a layered permission model for policy updates:

RoleSourceCan ModifyRestrictions
OwnerORLY_OWNERS env or owners in policy.jsonAll fieldsOwners list must remain non-empty
Policy Adminpolicy_admins in policy.jsonExtend rules, add blacklistsCannot modify owners or policy_admins, cannot reduce permissions

Composition Rules

Policy updates from owners and policy admins compose as follows:

  1. Owner policy is the base - Defines minimum permissions and protected fields
  2. Policy admins can extend - Add to allow lists, add new kinds, add blacklists
  3. Blacklists override whitelists - Policy admins can ban users that owners allowed
  4. Protected fields are immutable - Only owners can modify owners and policy_admins

What Policy Admins CAN Do:

What Policy Admins CANNOT Do:

Enabling Dynamic Policy Updates

  1. Set yourself as both owner and policy admin in the initial policy.json:
{
  "default_policy": "allow",
  "owners": ["YOUR_HEX_PUBKEY_HERE"],
  "policy_admins": ["ADMIN_HEX_PUBKEY_HERE"],
  "policy_follow_whitelist_enabled": false
}

Important: The owners list must contain at least one pubkey to prevent lockout.

  1. Ensure policy is enabled:
export ORLY_POLICY_ENABLED=true

Publishing a Policy Update

Send a kind 12345 event with the new policy configuration as JSON content:

As Owner (full control):

{
  "kind": 12345,
  "content": "{\"default_policy\": \"deny\", \"owners\": [\"OWNER_HEX\"], \"policy_admins\": [\"ADMIN_HEX\"], \"kind\": {\"whitelist\": [1,3,7]}}",
  "tags": [],
  "created_at": 1234567890
}

As Policy Admin (extensions only):

{
  "kind": 12345,
  "content": "{\"default_policy\": \"deny\", \"owners\": [\"OWNER_HEX\"], \"policy_admins\": [\"ADMIN_HEX\"], \"kind\": {\"whitelist\": [1,3,7,30023], \"blacklist\": [4]}, \"rules\": {\"1\": {\"write_deny\": [\"BAD_ACTOR_HEX\"]}}}",
  "tags": [],
  "created_at": 1234567890
}

Note: Policy admins must include the original owners and policy_admins values unchanged.

Policy Admin Follow List Whitelisting

When policy_follow_whitelist_enabled is true, the relay automatically grants access to all pubkeys followed by policy admins.

{
  "policy_admins": ["ADMIN_PUBKEY_HEX"],
  "policy_follow_whitelist_enabled": true
}

Security Considerations

Error Messages

Common validation errors:

ErrorCause
owners list cannot be emptyOwner tried to remove all owners
cannot modify the 'owners' fieldPolicy admin tried to change owners
cannot modify the 'policy_admins' fieldPolicy admin tried to change admins
cannot remove kind X from whitelistPolicy admin tried to reduce permissions
cannot reduce size_limit for kind XPolicy admin tried to make limits stricter
cannot blacklist owner XPolicy admin tried to blacklist an owner
cannot blacklist policy admin XPolicy admin tried to blacklist another admin

Testing the Policy System

Edge Cases Discovered During Testing

When writing tests for the policy system, the following edge cases were discovered:

  1. Config File Requirement: NewWithManager() with enabled=true requires the XDG config file (~/.config/APP_NAME/policy.json) to exist before initialization. Tests must create this file first.
  1. Error Message Format: Validation errors use underscores in field names (e.g., invalid policy_admin pubkey) - tests should match this exact format.
  1. Binary Tag Storage: When comparing pubkeys from e/p tags, always use tag.ValueHex() instead of tag.Value() due to binary optimization.
  1. Concurrent Access: The policy system uses sync.RWMutex for thread-safe access to the follows list during updates.
  1. Message Processing Pause: Policy updates pause message processing with an exclusive lock to ensure atomic updates.

Running Policy Tests

# Run all policy package tests
CGO_ENABLED=0 go test -v ./pkg/policy/...

# Run handler tests for kind 12345
CGO_ENABLED=0 go test -v ./app/... -run "PolicyConfig|PolicyAdmin"

# Run specific test categories
CGO_ENABLED=0 go test -v ./pkg/policy/... -run "ValidateJSON|Reload|Follow|TagValidation"

Advanced Configuration

Multiple Policies

Use different policies for different relay instances:

# Production relay
export ORLY_APP_NAME=production
# Policy at ~/.config/production/policy.json

# Staging relay
export ORLY_APP_NAME=staging
# Policy at ~/.config/staging/policy.json

Dynamic Policies

Policies can be updated without restart by modifying the JSON file. Changes take effect immediately for new events.

Integration with External Systems

Scripts can integrate with external services:

import requests

def check_external_service(content):
    response = requests.post('http://moderation-service:8080/check',
                           json={'content': content}, timeout=5)
    return response.json().get('approved', False)

Examples Repository

See the docs/ directory for complete examples:

Support

For issues with policy configuration:

  1. Check the logs for error messages
  2. Validate your JSON configuration
  3. Test scripts independently
  4. Review the examples in docs/
  5. Check file permissions and paths

Migration from Other Systems

From Simple Filtering

Replace simple filters with policy rules:

// Before: Simple size limit
// After: Policy-based size limit
{
  "global": {
    "size_limit": 50000
  }
}

From Custom Code

Migrate custom validation logic to policy scripts:

{
  "rules": {
    "1": {
      "script": "/etc/orly/scripts/custom-validation.py"
    }
  }
}

The policy system provides a flexible, maintainable way to implement complex relay behavior while maintaining performance and security.