A fully customizable data access governance layer.
BetweenRows is a SQL-aware proxy that enforces fine-grained access policies β masking, filtering, and blocking β in real-time. Works with PostgreSQL today; warehouses and lakehouses on the roadmap. Free to self-host. Source-available.
π Full documentation: docs.betweenrows.dev
Enforcement & audit
- SQL-aware β parses every query, then masks columns, filters rows, or blocks operations at the level where data actually moves.
- Every query and every policy decision is logged end-to-end β who ran what, when, and what BetweenRows did about it.
Fully customizable
- Composable roles, custom attributes, and JavaScript decision functions β all first-class variables in policy expressions that evaluate at query time.
- RBAC, ABAC, and programmable logic in one unified engine.
Free and source-available
- Self-host with no usage limits and no seat fees.
- Inspect the code. Run it on your infrastructure. No black-box security.
Built in Rust on DataFusion β low overhead, memory-safe, production-grade query rewriting.
BetweenRows ships as a single binary with two planes:
Data plane (port 5434) β PostgreSQL wire protocol proxy. Connect with any PostgreSQL client (psql, TablePlus, DBeaver, your app). Policies are enforced transparently on every query.
Management plane (port 5435) β Admin UI and REST API for managing users, data sources, roles, policies, and audit logs. Only admin users have access.
The two planes are independent β being an admin does not grant data access. All data access must be explicitly granted via data source assignments and policies.
psql / app
β PostgreSQL wire protocol (port 5434)
BetweenRows
ββ Authenticates user
ββ Checks data source access
ββ Applies policies:
β row_filter β inject WHERE clauses
β column_mask β replace column values
β column_deny β hide columns
β table_deny β hide tables
β column_allow β allowlist columns
ββ Executes via DataFusion
β
Upstream PostgreSQL
docker run -d \
-e BR_ADMIN_USER=admin \
-e BR_ADMIN_PASSWORD=changeme \
-p 5434:5434 -p 5435:5435 \
-v betweenrows_data:/data \
ghcr.io/getbetweenrows/betweenrows:latest # demo only β pin a specific tag for anything real| Variable | Required | Default | Description |
|---|---|---|---|
BR_ADMIN_USER |
No | admin |
Username for the initial admin account. Change it now if you prefer a different name β the username cannot be changed after creation. You can always create additional admin users through the UI later. |
BR_ADMIN_PASSWORD |
Yes | β | Password for the initial admin account. Only used on first boot. You can change the password later through the UI. |
-p 5434:5434 |
Yes | β | SQL proxy port. Connect your SQL clients here. |
-p 5435:5435 |
Yes | β | Admin UI and REST API port. |
-v betweenrows_data:/data |
Yes | β | Persistent volume. Stores the SQLite database (users, data sources, policies, audit logs) and auto-generated encryption/JWT keys when BR_ENCRYPTION_KEY and BR_ADMIN_JWT_SECRET are not set. Do not omit β without it, all data and keys are lost when the container restarts. |
Change these values to your preference before the first run. See Configuration for all available options.
Open http://localhost:5435 and log in with your admin credentials.
- π Add a data source β Go to Data Sources β Create. Enter your data source connection details and test the connection.
- π Discover the schema β Click "Discover Catalog" on your new data source. Select which schemas, tables, and columns to expose through the proxy.
- π€ Create a user β Go to Users β Create. Set a username and password.
- π Grant access β On the data source page, assign the user (or a role) access to the data source.
- π Create a policy β Go to Policies β Create. For example, a
row_filterpolicy with expressiontenant = {user.tenant}to isolate rows by tenant. (Thetenantattribute must be defined as a custom attribute definition first.) - π― Assign the policy β On the data source page, assign the policy to a user, role, or all users.
- π Connect through the proxy β The user can now query through BetweenRows:
Policies are applied automatically. Check the Query Audit page to see what happened.
psql "postgresql://alice:secret@localhost:5434/my-datasource"
| Env var | Required | Default | Description |
|---|---|---|---|
BR_ADMIN_PASSWORD |
Yes (first boot) | β | Password for the initial admin account. Must be set when no users exist in DB. |
BR_ADMIN_USER |
No | admin |
Username for the initial admin account. Only used on first boot. |
BR_ENCRYPTION_KEY |
No | (auto-persisted) | 64-char hex β AES-256-GCM key for secrets at rest. If unset, auto-generated and saved to /data/.betweenrows/encryption_key. Set explicitly in prod. If switching from auto-generated to explicit, copy the value from /data/.betweenrows/encryption_key β using a different key makes existing secrets unreadable. |
BR_ADMIN_JWT_SECRET |
No | (auto-persisted) | Any non-empty string β HMAC-SHA256 signing key for admin JWTs. If unset, auto-generated and saved to /data/.betweenrows/jwt_secret. Set explicitly in prod. |
BR_ADMIN_JWT_EXPIRY_HOURS |
No | 24 |
JWT lifetime in hours. |
BR_ADMIN_DATABASE_URL |
No | sqlite://proxy_admin.db?mode=rwc |
SeaORM connection URL (use postgres://β¦ for shared backend). |
BR_PROXY_BIND_ADDR |
No | 127.0.0.1:5434 |
Proxy listen address. Docker image defaults to 0.0.0.0:5434. |
BR_ADMIN_BIND_ADDR |
No | 127.0.0.1:5435 |
Admin REST API listen address. Docker image defaults to 0.0.0.0:5435. |
BR_IDLE_TIMEOUT_SECS |
No | 900 (15 min) |
Close idle proxy connections after this many seconds. Set to 0 to disable. |
BR_CORS_ALLOWED_ORIGINS |
No | (empty, same-origin only) | Comma-separated list of allowed CORS origins for the Admin API. |
RUST_LOG |
No | info |
Log filter (standard Rust/tracing convention). |
BetweenRows speaks the PostgreSQL wire protocol β connect with any PostgreSQL client using the datasource name as the database:
psql "postgresql://<user>:<password>@127.0.0.1:5434/<datasource-name>"Tested with psql and TablePlus. Any tool that supports PostgreSQL or ODBC with a PostgreSQL driver should work β including DBeaver, DataGrip, BI tools, and application ORMs.
Note: Some SQL clients send additional metadata queries (e.g., for autocompletion or schema browsing) that BetweenRows may not support yet. If your client fails to connect, please open an issue.
BetweenRows supports five policy types:
| Type | What it does |
|---|---|
row_filter |
Injects a WHERE clause to filter rows (e.g., tenant = {user.tenant}) |
column_mask |
Replaces column values with an expression (e.g., '***@' || split_part(email, '@', 2)) |
column_allow |
Permits access to specific columns (required in policy_required mode) |
column_deny |
Hides columns from the user's schema entirely |
table_deny |
Hides entire tables from the user's schema |
Key concepts:
- RBAC β assign policies to roles. Users inherit policies through role membership, including via role hierarchies
- ABAC β define custom user attributes (e.g., department, region, clearance level) and use them in policy expressions via
{user.<key>}template variables - Decision functions β optional JavaScript functions compiled to WASM that gate policy evaluation based on arbitrary logic (time windows, multi-attribute conditions, external state)
- Assignment scopes β policies can be assigned to individual users, roles, or all users on a data source
- Template variables β
{user.username},{user.id}(built-in), and custom attributes like{user.tenant},{user.region}are substituted at query time, making policies dynamic per user - Access modes β data sources can be set to
open(all tables accessible by default) orpolicy_required(tables are hidden unless acolumn_allowpolicy grants access) - Deny wins β deny policies are evaluated before permit policies and cannot be overridden
- Visibility follows access β denied columns and tables are removed from the user's schema at connection time, so they don't appear in client tools like TablePlus or DBeaver
See docs-site/docs/concepts/policy-model.md for the full guide.
Before a data source is queryable through the proxy, its catalog must be saved. The UI wizard guides through four steps:
- Discover schemas β select which schemas to include
- Discover tables β select tables within those schemas
- Discover columns β choose which columns to expose
- Save β persists selections and makes them available for queries
The catalog is an allowlist β the proxy can never expose tables or columns not explicitly saved. To detect schema drift after upstream changes, use "Sync Catalog" from the data source page.
BetweenRows includes a full REST API at http://localhost:5435/api/v1 for managing users, data sources, roles, policies, catalog discovery, and audit logs. All endpoints require JWT authentication (POST /auth/login to obtain a token).
Everything you can do in the admin UI can also be done via the API β useful for scripting, CI/CD integration, and automation.
Create users without the UI β useful for scripting and automation. If you're locked out of the admin UI, use --admin to create a new admin user to regain access. You can then change passwords through the UI. A forgot/reset password feature is on the roadmap:
# Docker
docker exec -it <container> proxy user create --username alice --password secret
docker exec -it <container> proxy user create --username rescue --password secret --admin
# From source
cargo run -p proxy -- user create --username alice --password secretSee docs-site/docs/about/roadmap.md for planned features including shadow mode, governance workflows, and more.
See CONTRIBUTING.md for architecture details, build instructions, and development setup.


