{"id":48837511,"url":"https://github.com/getbetweenrows/betweenrows","last_synced_at":"2026-04-24T04:00:55.821Z","repository":{"id":341289334,"uuid":"1166146949","full_name":"getbetweenrows/betweenrows","owner":"getbetweenrows","description":"Fine-grained access control for Postgres (row filters, column masking, blocking) via a     SQL-aware proxy. Free and source-available. Warehouses on the roadmap.","archived":false,"fork":false,"pushed_at":"2026-04-14T22:54:01.000Z","size":4879,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-15T00:33:28.004Z","etag":null,"topics":["access-control","admin-console","data-access-layer","data-governance","data-security","postgres-protocol","sql-connector","sql-proxy"],"latest_commit_sha":null,"homepage":"https://betweenrows.dev","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/getbetweenrows.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":"Governance Dev Mode with Branch-Based .md","roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-24T23:26:43.000Z","updated_at":"2026-04-14T22:54:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/getbetweenrows/betweenrows","commit_stats":null,"previous_names":["getbetweenrows/betweenrows"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/getbetweenrows/betweenrows","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getbetweenrows%2Fbetweenrows","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getbetweenrows%2Fbetweenrows/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getbetweenrows%2Fbetweenrows/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getbetweenrows%2Fbetweenrows/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/getbetweenrows","download_url":"https://codeload.github.com/getbetweenrows/betweenrows/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getbetweenrows%2Fbetweenrows/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32208477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T03:15:14.334Z","status":"ssl_error","status_checked_at":"2026-04-24T03:15:11.608Z","response_time":64,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-control","admin-console","data-access-layer","data-governance","data-security","postgres-protocol","sql-connector","sql-proxy"],"created_at":"2026-04-15T00:04:52.966Z","updated_at":"2026-04-24T04:00:55.812Z","avatar_url":"https://github.com/getbetweenrows.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BetweenRows\n\n[![CI](https://github.com/getbetweenrows/betweenrows/actions/workflows/cicd.yml/badge.svg)](https://github.com/getbetweenrows/betweenrows/actions)\n[![Version](https://img.shields.io/github/v/tag/getbetweenrows/betweenrows?label=Version)](https://github.com/getbetweenrows/betweenrows/tags)\n![Status: Beta](https://img.shields.io/badge/Status-Beta-orange.svg)\n[![License: ELv2](https://img.shields.io/badge/License-ELv2-blue.svg)](LICENSE)\n[![Docker](https://img.shields.io/badge/Docker-GHCR-blue?logo=docker)](https://ghcr.io/getbetweenrows/betweenrows)\n[![Built with Rust](https://img.shields.io/badge/Built%20with-Rust-orange?logo=rust)](https://www.rust-lang.org)\n\nA fully customizable data access governance layer.\n\nBetweenRows 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.\n\n📖 **Full documentation:** [docs.betweenrows.dev](https://docs.betweenrows.dev)\n\n## ✨ Why BetweenRows\n\n**Enforcement \u0026 audit**\n- SQL-aware — parses every query, then masks columns, filters rows, or blocks operations at the level where data actually moves.\n- Every query and every policy decision is logged end-to-end — who ran what, when, and what BetweenRows did about it.\n\n**Fully customizable**\n- Composable roles, custom attributes, and JavaScript decision functions — all first-class variables in policy expressions that evaluate at query time.\n- RBAC, ABAC, and programmable logic in one unified engine.\n\n**Free and source-available**\n- Self-host with no usage limits and no seat fees.\n- Inspect the code. Run it on your infrastructure. No black-box security.\n\nBuilt in **Rust** on **DataFusion** — low overhead, memory-safe, production-grade query rewriting.\n\n## 📸 Screenshots\n\n| | |\n|---|---|\n| ![Admin dashboard](docs-site/docs/public/screenshots/introduction-dashboard-v0.14.png) | The admin dashboard — data sources, users, roles, and policies at a glance. |\n| ![Row filter policy editor](docs-site/docs/public/screenshots/row-filters-policy-editor-v0.15.png) | Row filter policy editor — write a `WHERE` expression with template variables like `{user.tenant}`. |\n| ![Query audit detail](docs-site/docs/public/screenshots/row-filters-audit-v0.15.png) | Query audit — see the original SQL, the rewritten SQL, and which policies fired. |\n\n## 🏗️ Components\n\nBetweenRows ships as a single binary with two planes:\n\n**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.\n\n**Management plane** (port 5435) — Admin UI and REST API for managing users, data sources, roles, policies, and audit logs. Only admin users have access.\n\nThe 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.\n\n```\npsql / app\n    ↓  PostgreSQL wire protocol (port 5434)\nBetweenRows\n    ├─ Authenticates user\n    ├─ Checks data source access\n    ├─ Applies policies:\n    │      row_filter   — inject WHERE clauses\n    │      column_mask  — replace column values\n    │      column_deny  — hide columns\n    │      table_deny   — hide tables\n    │      column_allow — allowlist columns\n    └─ Executes via DataFusion\n    ↓\nUpstream PostgreSQL\n```\n\n## 🚀 Quick Start (Docker)\n\n```bash\ndocker run -d \\\n  -e BR_ADMIN_USER=admin \\\n  -e BR_ADMIN_PASSWORD=changeme \\\n  -p 5434:5434 -p 5435:5435 \\\n  -v betweenrows_data:/data \\\n  ghcr.io/getbetweenrows/betweenrows:latest  # demo only — pin a specific tag for anything real\n```\n\n| Variable                    | Required | Default | Description                                                                                                                                                                                                                                                                            |\n| --------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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.                                                                               |\n| `BR_ADMIN_PASSWORD`         | **Yes**  | —       | Password for the initial admin account. Only used on first boot. You can change the password later through the UI.                                                                                                                                                                     |\n| `-p 5434:5434`              | **Yes**  | —       | SQL proxy port. Connect your SQL clients here.                                                                                                                                                                                                                                         |\n| `-p 5435:5435`              | **Yes**  | —       | Admin UI and REST API port.                                                                                                                                                                                                                                                            |\n| `-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. |\n\nChange these values to your preference **before the first run**. See [Configuration](#configuration) for all available options.\n\nOpen **http://localhost:5435** and log in with your admin credentials.\n\n### 5-Minute Walkthrough\n\n1. 🔗 **Add a data source** — Go to Data Sources → Create. Enter your data source connection details and test the connection.\n2. 🔍 **Discover the schema** — Click \"Discover Catalog\" on your new data source. Select which schemas, tables, and columns to expose through the proxy.\n3. 👤 **Create a user** — Go to Users → Create. Set a username and password.\n4. 🔑 **Grant access** — On the data source page, assign the user (or a role) access to the data source.\n5. 📜 **Create a policy** — Go to Policies → Create. For example, a `row_filter` policy with expression `tenant = {user.tenant}` to isolate rows by tenant. (The `tenant` attribute must be defined as a custom attribute definition first.)\n6. 🎯 **Assign the policy** — On the data source page, assign the policy to a user, role, or all users.\n7. 🚀 **Connect through the proxy** — The user can now query through BetweenRows:\n   ```bash\n   psql \"postgresql://alice:secret@localhost:5434/my-datasource\"\n   ```\n   Policies are applied automatically. Check the Query Audit page to see what happened.\n\n## ⚙️ Configuration\n\n| Env var                     | Required             | Default                            | Description                                                                                                                                                                                                                                                                                                                |\n| --------------------------- | -------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `BR_ADMIN_PASSWORD`         | **Yes** (first boot) | —                                  | Password for the initial admin account. Must be set when no users exist in DB.                                                                                                                                                                                                                                             |\n| `BR_ADMIN_USER`             | No                   | `admin`                            | Username for the initial admin account. Only used on first boot.                                                                                                                                                                                                                                                           |\n| `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. |\n| `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.**                                                                                                                                                          |\n| `BR_ADMIN_JWT_EXPIRY_HOURS` | No                   | `24`                               | JWT lifetime in hours.                                                                                                                                                                                                                                                                                                     |\n| `BR_ADMIN_DATABASE_URL`     | No                   | `sqlite://proxy_admin.db?mode=rwc` | SeaORM connection URL (use `postgres://…` for shared backend).                                                                                                                                                                                                                                                             |\n| `BR_PROXY_BIND_ADDR`        | No                   | `127.0.0.1:5434`                   | Proxy listen address. Docker image defaults to `0.0.0.0:5434`.                                                                                                                                                                                                                                                             |\n| `BR_ADMIN_BIND_ADDR`        | No                   | `127.0.0.1:5435`                   | Admin REST API listen address. Docker image defaults to `0.0.0.0:5435`.                                                                                                                                                                                                                                                    |\n| `BR_IDLE_TIMEOUT_SECS`      | No                   | `900` (15 min)                     | Close idle proxy connections after this many seconds. Set to `0` to disable.                                                                                                                                                                                                                                               |\n| `BR_CORS_ALLOWED_ORIGINS`   | No                   | _(empty, same-origin only)_        | Comma-separated list of allowed CORS origins for the Admin API.                                                                                                                                                                                                                                                            |\n| `RUST_LOG`                  | No                   | `info`                             | Log filter (standard Rust/tracing convention).                                                                                                                                                                                                                                                                             |\n\n## 🔌 Connecting to the Proxy\n\nBetweenRows speaks the PostgreSQL wire protocol — connect with any PostgreSQL client using the datasource name as the database:\n\n```bash\npsql \"postgresql://\u003cuser\u003e:\u003cpassword\u003e@127.0.0.1:5434/\u003cdatasource-name\u003e\"\n```\n\nTested 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.\n\n\u003e **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](https://github.com/getbetweenrows/betweenrows/issues).\n\n## 🛡️ Policy System\n\nBetweenRows supports five policy types:\n\n| Type           | What it does                                                                              |\n| -------------- | ----------------------------------------------------------------------------------------- |\n| `row_filter`   | Injects a WHERE clause to filter rows (e.g., `tenant = {user.tenant}`)                    |\n| `column_mask`  | Replaces column values with an expression (e.g., `'***@' \\|\\| split_part(email, '@', 2)`) |\n| `column_allow` | Permits access to specific columns (required in `policy_required` mode)                   |\n| `column_deny`  | Hides columns from the user's schema entirely                                             |\n| `table_deny`   | Hides entire tables from the user's schema                                                |\n\nKey concepts:\n\n- **RBAC** — assign policies to roles. Users inherit policies through role membership, including via role hierarchies\n- **ABAC** — define custom user attributes (e.g., department, region, clearance level) and use them in policy expressions via `{user.\u003ckey\u003e}` template variables\n- **Decision functions** — optional JavaScript functions compiled to WASM that gate policy evaluation based on arbitrary logic (time windows, multi-attribute conditions, external state)\n- **Assignment scopes** — policies can be assigned to individual users, roles, or all users on a data source\n- **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\n- **Access modes** — data sources can be set to `open` (all tables accessible by default) or `policy_required` (tables are hidden unless a `column_allow` policy grants access)\n- **Deny wins** — deny policies are evaluated before permit policies and cannot be overridden\n- **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\n\nSee [docs-site/docs/concepts/policy-model.md](docs-site/docs/concepts/policy-model.md) for the full guide.\n\n## 📋 Catalog Workflow\n\nBefore a data source is queryable through the proxy, its catalog must be saved. The UI wizard guides through four steps:\n\n1. **Discover schemas** — select which schemas to include\n2. **Discover tables** — select tables within those schemas\n3. **Discover columns** — choose which columns to expose\n4. **Save** — persists selections and makes them available for queries\n\nThe 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.\n\n## 🔗 Admin REST API\n\nBetweenRows 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).\n\nEverything you can do in the admin UI can also be done via the API — useful for scripting, CI/CD integration, and automation.\n\n## 💻 CLI\n\nCreate 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](docs-site/docs/about/roadmap.md):\n\n```bash\n# Docker\ndocker exec -it \u003ccontainer\u003e proxy user create --username alice --password secret\ndocker exec -it \u003ccontainer\u003e proxy user create --username rescue --password secret --admin\n\n# From source\ncargo run -p proxy -- user create --username alice --password secret\n```\n\n## 🗺️ Roadmap\n\nSee [docs-site/docs/about/roadmap.md](docs-site/docs/about/roadmap.md) for planned features including shadow mode, governance workflows, and more.\n\n## 🤝 Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, build instructions, and development setup.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetbetweenrows%2Fbetweenrows","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgetbetweenrows%2Fbetweenrows","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetbetweenrows%2Fbetweenrows/lists"}