Skip to content

Glossary

This page defines the terms used throughout the BetweenRows documentation. When in doubt about what a term means, check here.

Identity and access

User

An identity that connects through the BetweenRows proxy. Each user has a username, password, optional admin flag, and a set of user attributes. Users authenticate on the data plane (pgwire) and optionally on the management plane (admin UI/API).

Admin

A user with is_admin = true. Grants access to the admin UI and REST API. Does not grant data plane access — admin and data access are separate planes.

Role

A named group of users. Roles are used for policy assignment — assign a policy to a role, and all members receive it. Roles support inheritance (a DAG with cycle detection and a depth cap of 10).

Data source access

An explicit grant allowing a user or role to connect to a specific data source through the proxy. Every user starts with zero data access — grants must be added per data source.

User attributes (ABAC)

User attribute

Any property of a user that can be referenced in policy expressions or decision functions. User attributes are the foundation of attribute-based access control (ABAC).

User attributes come in two kinds:

KindExamplesDefined byAlways present?
Built-in attributesusername, idSystemYes — every user has these
Custom attributestenant, department, clearance, is_vipAdmin (via Attribute Definitions)No — only if the admin defines them and the user has a value set

Both kinds are accessed the same way in expressions: {user.username}, {user.tenant}, {user.clearance}.

Built-in attribute

A user attribute provided by the system: username (string) and id (UUID). Always available, cannot be overridden by custom attributes (reserved keys are rejected at the API).

Custom attribute

A user attribute defined by the admin via Attribute Definitions (as opposed to built-in attributes which are system-managed). Has a key, value type (string, integer, boolean, list), optional default value, and optional allowed-values enum. Custom attributes must be defined (via Attribute Definitions) before they can be assigned to users.

This is the standard term — prefer "custom attributes" over "user-defined attributes" in all documentation.

Attribute definition

The schema for a custom attribute — defines its key, value type, default value, and allowed values. Think of it as a "column definition" for user metadata. Created via the admin UI or API before assigning values to users.

Default value

The value used when a user lacks a custom attribute. If set, the default is substituted as a typed literal. If not set (NULL), SQL NULL is used — which evaluates to false in WHERE clauses, meaning the user sees zero rows. This is fail-closed by design.

Variables and expressions

Template variable

A placeholder in a row_filter or column_mask expression that is replaced with a value at query time. Written as {user.KEY} in expressions.

Today, template variables expose user attributes (both built-in and custom). The design allows for future expansion to other namespaces (e.g., {session.*}, {datasource.*}), but only {user.*} is implemented.

Template variables are substituted as typed SQL literals after the expression is parsed — never as raw SQL strings. This makes them injection-safe by construction.

→ Full reference: Template Expressions

Decision function context

The JSON object passed to a decision function's evaluate(ctx, config) call. It is a superset of what template variables expose — it includes user attributes plus additional context:

Context pathContentsAvailable when
ctx.session.user.*All user attributes (built-in + custom)Always
ctx.session.user.rolesArray of role namesAlways
ctx.session.time.nowISO 8601 timestampAlways
ctx.session.datasource.*Data source name and metadataAlways
ctx.query.tablesArray of {datasource, schema, table} objectsevaluate_context = "query" only
ctx.query.columnsOutput column namesevaluate_context = "query" only
ctx.query.join_countNumber of JOINsevaluate_context = "query" only
ctx.query.has_aggregationBooleanevaluate_context = "query" only
ctx.query.statement_type"SELECT"evaluate_context = "query" only

→ Full reference: Decision Functions

Filter expression

The SQL expression in a row_filter policy that determines which rows a user can see. Can reference table columns and template variables. Example: org = {user.tenant}.

Mask expression

The SQL expression in a column_mask policy that replaces a column's value. Can reference the original column and template variables. Example: '***-**-' || RIGHT(ssn, 4).

Where attributes and variables are available

Attributes and variables surface in two different contexts — template expressions and decision functions — with different capabilities:

Template expressions vs. decision functions

Template expressionsDecision functions
Used inrow_filter and column_mask definitionsAny policy (attached via decision_function_id)
SyntaxSQL with {user.KEY} placeholdersJavaScript: evaluate(ctx, config)
Built-in attributes{user.username}, {user.id}ctx.session.user.username, ctx.session.user.id
Custom attributes{user.tenant}, {user.clearance}, etc.ctx.session.user.tenant, ctx.session.user.clearance, etc.
User's rolesNot availablectx.session.user.roles (array of role names)
Session timeNot availablectx.session.time.now (ISO 8601)
Data source infoNot availablectx.session.datasource.name
Query metadataNot availablectx.query.tables, ctx.query.columns, ctx.query.join_count, ctx.query.has_aggregation, ctx.query.statement_type (requires evaluate_context = "query")
Type safetyValues substituted as typed SQL literals (Utf8, Int64, Boolean)Values as typed JSON (string, number, boolean, array)
Injection safetySafe by construction — literals in the parsed expression treeN/A — JS runs in a WASM sandbox, no SQL access
ComplexitySQL expressions only (comparisons, CASE, string functions)Full JavaScript logic (conditionals, loops, string manipulation)
When evaluatedAt query time, per queryAt connect time (evaluate_context = "session") or per query (evaluate_context = "query")
PerformanceNegligible — literal substitution in the plan~1ms WASM execution per invocation

When to use which

  • Template expressions are the default. Use them for straightforward attribute-based filtering and masking — org = {user.tenant}, CASE WHEN {user.department} = 'hr' THEN ssn ELSE masked END. No JavaScript needed.
  • Decision functions are the escape hatch. Use them when the gating logic is too complex for a SQL expression — time-based access, multi-attribute business rules, query-shape inspection (e.g., "deny if the query touches more than 3 tables"), or when you need access to roles or session metadata that template variables don't expose.

What's available today vs. planned

NamespaceTemplate expressionsDecision functionsStatus
user.* (built-in + custom attributes)YesYesShipped
user.rolesNoYesShipped
session.time.*NoYesShipped
session.datasource.*NoYesShipped
query.* (tables, columns, aggregation)NoYes (requires evaluate_context = "query")Shipped
datasource.* in template expressionsNoN/APlanned
table.* / column.* attributesNoNoPlanned

Template variables today are scoped to {user.*}. The architecture supports future expansion to other namespaces without breaking existing expressions.

Policies

Policy

A named, versioned rule that controls data access. Every policy has a policy_type, a set of targets (which schemas/tables/columns it applies to), and optionally a definition (the expression logic).

Policy type

One of five types, each with a different effect:

TypeIntentEffect
row_filterpermitAdds a WHERE clause to filter rows
column_maskpermitReplaces a column's value with a masked expression
column_allowpermitAllowlists specific columns (only in policy_required mode)
column_denydenyRemoves specific columns from results and schema
table_denydenyRemoves an entire table from the user's view

Deny-wins invariant

If any enabled deny policy matches, the deny is enforced — regardless of any permit policies. This holds across all scopes, roles, and priorities. It is a core security guarantee.

Policy assignment

The binding between a policy and a data source, with a scope (who it applies to) and a priority (which wins on conflict).

Assignment scope

Who a policy assignment applies to:

ScopeMeaning
allEvery user on the data source
roleAll members of a specific role (direct + inherited)
userOne specific user

Priority

A numeric value on each policy assignment (default: 100). Lower number = higher precedence. At equal priority, user-specific beats role-specific beats all.

Data sources and catalog

Data source

BetweenRows' representation of an upstream PostgreSQL database. Stores connection details, access mode, and the discovered catalog.

Catalog

The set of schemas, tables, and columns exposed through the proxy for a data source. Maintained as an allowlist — anything not in the catalog is invisible. Discovered from the upstream database and saved by the admin.

Access mode

Determines what happens when no policy matches a table:

ModeDefault behaviorUse case
policy_requiredTables are invisible without a column_allow policyProduction
openTables are visible to any user with data source accessDevelopment

Catalog drift

When the upstream schema changes but the saved catalog hasn't been re-synced. New upstream tables/columns are not automatically exposed — an admin must explicitly select them via Sync Catalog.

Architecture

Data plane

The pgwire proxy (default port 5434). Handles user connections, query rewriting, policy enforcement, and audit logging.

Management plane

The admin UI and REST API (default port 5435). Handles configuration: users, roles, policies, data sources, attribute definitions.

Logical plan rewriting

How BetweenRows enforces policies. Queries are parsed into a DataFusion logical plan, then the plan is rewritten (row filters injected as Filter nodes, column masks as Projection nodes, columns/tables removed) before execution against the upstream database. This approach makes policies bypass-immune — no query shape (aliases, CTEs, subqueries, JOINs) can escape enforcement.

Virtual schema

The per-user view of the database schema, built at connect time from the catalog + enabled policies. Each user sees a schema tailored to their access — denied columns/tables are absent, not just filtered.

Visibility follows access

The principle that schema metadata matches data access. If a column is denied, it disappears from information_schema.columns — the user cannot discover it exists. If a table is denied, \dt doesn't list it and queries return "table not found" (not "access denied").

Audit

Query audit log

An append-only log of every query processed by the proxy. Records the original SQL, rewritten SQL, policies applied, execution time, client info, and status.

Admin audit log

An append-only log of every mutation on the management plane — user/role/policy/datasource changes. Records the actor, action, resource, and a JSON diff of what changed.

404-not-403 principle

Denied resources return "not found" errors, not "access denied." This prevents users from discovering what exists but is restricted. Applied to table deny (table not found), column deny (column does not exist), and data source access (unknown database).