radiatordb
RadiatorDB is a document-oriented backend that stores and retrieves JSON documents in PostgreSQL. See the RadiatorDB article for an overview of features and concepts. See RadiatorDB REST API for management API document operations.
Example
radiatordb "RADIATORDB" {
server "primary" {
host "db.example.com";
port 5432;
database "radiator";
username "radiator";
password "secret";
}
collection "users" {
roles {
read "support";
write "ops";
}
name "Users";
description "User credentials for authentication";
key {
field {
name "Username";
type text;
}
}
field "password" {
name "Password";
type password;
required true;
}
field "sessionTimeout" {
name "Session Timeout";
type number;
}
}
query "FIND_USER" {
collection "users";
key aaa.identity;
mapping {
user.username = doc | jsonpath("$.username");
user.password = doc | jsonpath("$.password");
vars.session_timeout = doc | jsonpath("$.sessionTimeout");
}
}
put "STORE_ACCOUNTING" {
collection "accounting";
key "%{aaa.identity}:%{radius.request.attr.acct-session-id}";
bindings {
username = aaa.identity;
attrs = radius.request.attrs;
}
}
}
Configuration Options
server
Defines a PostgreSQL server for document storage. Multiple server blocks can be configured for high availability. Each server is identified by a unique name.
RadiatorDB uses the same PostgreSQL server configuration as the postgres backend. See that reference for the full list of server options including url, host, port, database, username, password, connections { ... }, priority, tls, and service-level-objective.
server "primary" {
host "db.example.com";
port 5432;
database "radiator";
username "radiator";
password "secret";
connections {
max 10;
}
}
collection
Defines a document collection and its field schema. Collections organize documents into logical groups and describe their structure for the management UI.
See collection for all available options.
query
Defines a read operation that retrieves documents from a collection. The query can look up a single document by exact key, or scan the collection using key filters and where clauses.
When no documents match, the query result is reject and aaa.reason is set to a descriptive message (either "Key not found: collection/key" for key lookups, or "No documents found from collection: name" for filtered scans).
When one or more documents match, the result is accept and the configured mapping is applied to each matched document.
Query with exact key
The simplest query form looks up a single document by its full key:
query "FIND_USER" {
collection "users";
key aaa.identity;
mapping {
user.username = doc | jsonpath("$.username");
user.password = doc | jsonpath("$.password");
}
}
The key value is an expression that resolves to the document key at runtime.
Query with multipart key
Multiple key directives compose a multipart key. All parts are joined to form the full document key for an exact lookup:
query "FIND_DEVICE" {
collection "devices";
key vars.site;
key radius.request.attr.NAS-Identifier;
mapping {
user.username = doc | jsonpath("$.username");
user.password = doc | jsonpath("$.password");
}
}
When the key is not found, the reject reason includes the composed key: "Key not found: devices/["site:helsinki","edge-1"]".
Query with key filters
Key filters match documents by inspecting individual parts of multipart keys without requiring a full exact match. Two filter types are available:
at filter - matches a specific key part by index (zero-based, supports negative indexes):
query "FIND_DEVICE" {
collection "devices";
key {
part vars.site;
at 0;
}
key {
part radius.request.attr.NAS-Identifier;
at 1;
}
where {
username == aaa.identity;
}
mapping {
user.username = doc | jsonpath("$.username");
user.password = doc | jsonpath("$.password");
}
}
Negative indexes count from the end of the key (-1 is the last part, -2 is second to last). If the negative index is out of range for a document's key length, that document does not match.
contains filter - matches documents where any key part equals the specified value:
query "FIND_DEVICE" {
collection "devices";
key {
contains vars.site;
}
where {
username == aaa.identity;
}
mapping {
user.username = doc | jsonpath("$.username");
user.password = doc | jsonpath("$.password");
}
}
Key filters cannot be mixed with plain key directives in the same query.
Where clause
The where block filters documents by comparing JSON field values. It is valid only in queries that use key filters or that have no key at all (collection scan). A query with an exact key cannot have a where block.
query "FIND_USER" {
collection "users";
where {
username == aaa.identity;
role != "guest";
}
mapping {
user.username = doc | jsonpath("$.username");
user.password = doc | jsonpath("$.password");
}
}
Each filter line specifies a top-level JSON field name, an operator, and a value expression.
Supported operators:
| Operator | Description |
|---|---|
== | Equals. Automatically promotes to IP containment check when the field value is a CIDR network, or to regex match when the value is a regex literal. |
!= | Not equals. |
contains | Case-insensitive substring match. |
Regex matching - use a regex literal (delimited by /) as the value:
where {
username == /^ali.*$/;
}
IP containment - when the document field contains a CIDR network value (e.g. "192.0.2.0/24"), the == operator checks whether the compared IP address is contained within that network:
where {
network == radius.request.attr.NAS-Ip-Address;
}
Multiple filters - all filters in a where block must match (AND logic). Documents that fail any filter are excluded.
Mapping
The mapping block extracts values from matched documents into the request context. Documents are available through the doc variable with the jsonpath() filter for field extraction:
mapping {
user.username = doc | jsonpath("$.username");
user.password = doc | jsonpath("$.password");
vars.ip = doc | jsonpath("$.framedIpAddress");
}
The matched document key is available through the key local variable. key is a multivalue variable with one value for each document key part. Use zero-based indexing to read individual parts:
mapping {
vars.site = key[0];
vars.username = key[1];
}
For single-part keys, use key[0]. For multipart keys, each key part appears in the same order as the stored document key. The key variable is available for exact key lookups, key-filter queries, and collection scans.
For queries that return multiple documents, the mapping is applied to each document. Use the += operator to accumulate values into a list:
mapping {
vars.users += doc | jsonpath("$.username");
}
put
Defines a write operation that stores a document in a collection. The document content is built from the bindings block.
put "STORE_ACCOUNTING" {
collection "accounting";
key aaa.identity;
key radius.request.attr.acct-session-id;
expire 1h;
bindings {
username = aaa.identity;
attrs = radius.request.attrs;
metadata.source = "radius";
}
}
put key
At least one key directive is required. The key identifies the document within the collection.
A single key expression:
put "STORE_SESSION" {
collection "sessions";
key aaa.identity;
key radius.request.attr.acct-session-id;
bindings { ... }
}
Multiple key directives create a multipart key:
put "STORE_ACCOUNTING" {
collection "accounting";
key aaa.identity;
key radius.request.attr.acct-session-id;
bindings { ... }
}
put collection
Specifies the target collection. Required.
put expire
Sets a time-to-live for the stored document. After this duration, the document is no longer returned by queries. Accepts duration units.
put "STORE_SHORT" {
collection "sessions";
key aaa.identity;
key radius.request.attr.acct-session-id;
expire 30m;
bindings { ... }
}
If expire is not set, the document persists indefinitely.
put bindings
The bindings block defines the document content as field-value assignments. Only the = operator is allowed.
bindings {
username = aaa.identity;
attrs = radius.request.attrs;
metadata.source = "radius";
metadata.timestamp = datetime.timestamp;
}
Dotted field names (e.g. metadata.source) create nested JSON objects in the stored document.
Multipart Keys
Multipart keys allow hierarchical document organization. See RadiatorDB REST API multipart keys for key formatting in CLI arguments and management API URLs.
Using RadiatorDB in AAA Pipelines
Reference a RadiatorDB query from an AAA pipeline handler. Define conditions in the handler block, then use @execute and the backend action to run the query for access requests:
aaa {
policy "DEFAULT" {
handler "AUTHENTICATION" {
conditions all {
radius.request.code == radius.ACCESS_REQUEST;
}
@execute {
backend {
name "RADIATORDB";
query "FIND_USER";
}
pap;
}
}
}
}
For put operations, define the accounting request condition in the handler block and use @execute to store the document:
aaa {
policy "DEFAULT" {
handler "ACCOUNTING" {
conditions all {
radius.request.code == radius.ACCOUNTING_REQUEST;
}
@execute {
backend {
name "RADIATORDB";
query "STORE_ACCOUNTING";
}
accept;
}
}
}
}
Revision Retention
RadiatorDB stores revision history for each document insert, update, and delete. Configure how long RadiatorDB keeps old revisions in the collection revisions block:
collection "users" {
roles {
read "support";
write "ops";
}
revisions {
age 30d;
max 100;
}
key {
field {
name "Username";
type text;
}
}
}
The revisions block supports these parameters:
ageremoves archived revisions older than the configured duration. The default is30d.maxkeeps at most this many archived revisions for each document key. The default is100.
Both limits are enforced by RadiatorDB garbage collection. RadiatorDB can remove a revision when it is older than age or when the document key has more than max retained revisions.
Garbage Collection
RadiatorDB garbage collection removes old tombstones and old archived revisions. A tombstone is the delete marker that RadiatorDB keeps after a document is deleted so that deletion can replicate to other RadiatorDB nodes.
Configure scheduled garbage collection in the backend gc block:
radiatordb "mydb" {
gc {
enable true;
max_tombstone_age 30d;
interval {
duration 1h;
random 5m;
}
}
server "primary" {
host "db.example.com";
database "radiator";
username "radiator";
}
collection "users" {
roles {
read "support";
write "ops";
}
key {
field {
name "Username";
type text;
}
}
}
}
The gc block supports these parameters:
enablecontrols whether scheduled garbage collection runs. The default istrue.max_tombstone_ageremoves tombstones older than the configured duration. The default is30d.intervaldefines one or more schedules. The default isduration 1hwithrandom 5mjitter. See interval for the shared interval syntax.
If the gc block is omitted, RadiatorDB uses these defaults and scheduled garbage collection is enabled.
Use only one scheduled GC runner for a shared RadiatorDB deployment. If multiple Radiator Server processes use the same RadiatorDB backend, enable scheduled GC on one process and disable it on the others. This avoids duplicate maintenance work against the same PostgreSQL databases.
You can keep the same configuration file on all servers by reading enable from an environment variable:
radiatordb "mydb" {
gc {
enable env.RADIATORDB_GC_ENABLE | default("false");
max_tombstone_age 30d;
}
# Other backend settings are identical on every Radiator Server.
}
Set RADIATORDB_GC_ENABLE=true for exactly one Radiator Server process. Leave it unset, or set it to false, on the other processes. See Environment Variables for more information about env.VAR_NAME configuration values.