All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- [Admin UI] Sidebar logo and favicon — added a
logo.svgnext to the "BetweenRows" brand text in the admin sidebar header, and afavicon.svgfor the browser tab. - [Docs] Public documentation site sidebar restructure — reorganized the VitePress sidebar into five top-level sections (Start, Concepts, Features, Guides, About), with Policy Types, Template Expressions, and Decision Functions nested under Policies. Deleted standalone reference pages (
reference/policy-types,reference/audit-log-fields,reference/cli,reference/admin-rest-api,about/changelog,about/license) — content either folded into the guide it documents or replaced with an external link to the canonical source on GitHub (LICENSE,CHANGELOG.md).
- [Docs] Repositioned from "alpha" to "beta" — single canonical page (
docs-site/docs/about/license.md, now "License & Beta Status") describes pre-1.0 stability, what's stable vs. less stable, and the recommended posture for early adopters. Scattered "alpha" caveats across README, SECURITY.md, the VitePress sidebar/footer, and a dozen docs pages have been deleted or reframed — substance kept (pin tags, read changelog, file issues), positioning dropped from places that were just repeating the canonical disclaimer. Skipping the alpha→beta transition removes a fuzzy intermediate gate; the next stage is 1.0 with API stability commitments.
- [Docs] Switch public documentation site domain to
docs.betweenrows.dev— centralize URLs + OG image indocs-site/docs/.vitepress/constants.ts, add a path-gated Cloudflare Pages deploy workflow (.github/workflows/docs-site-deploy.yml), and enable Cloudflare Web Analytics via automatic edge injection. Remove obsoletedocs-site/internal/notes and the commented-out docs-site CI job. - [Docs] Tokenize release version across docs pages — markdown files use a
{{VERSION}}token substituted at build time fromconstants.tsvia a Vite pre-transform. Covers rendered HTML, the copy-as-markdown.mdoutput, andllms-full.txt. Future releases bump one file instead of ~13. - [Docs] README polish and marketing alignment — headline + pillars rewritten to match the
wwwlanding page (A fully customizable data access governance layer.), addeddocs.betweenrows.devlink and a screenshots table linking intodocs-site/docs/public/screenshots/, replaced the stale0.11.0Docker tag example with a pointer to the Tags page, and routed the permission-system and roadmap cross-references into the public docs. Relaxed the documentation-architecture rule in.claude/CLAUDE.mdsoREADME.md,SECURITY.md, andCONTRIBUTING.mdmay link intodocs-site/for self-contained GitHub rendering (code trees still may not). - [Admin UI] "Report an issue" footer link — bumped to the new
docs.betweenrows.devdomain. - [Repo]
.github/ISSUE_TEMPLATE/config.yml— directs security reports to GitHub Security Advisories and questions to Discussions. - [Repo]
SECURITY.mdlinks — rewritten to reference in-repo files instead of external docs URLs, so the GitHub Security tab is self-contained.
- [Docs] Public documentation site — full VitePress site at
docs-site/published todocs.getbetweenrows.com.- Installation (Docker, Fly, from source), Start (introduction, quickstart), Concepts (architecture, policy model, security overview, threat model), Guides (policies, users/roles, attributes, data sources, decision functions, audit debugging, recipes), Reference (REST API, config, CLI, policy types, template expressions, audit fields, glossary, demo schema), Operations (backups, upgrading, troubleshooting, known limitations, rename safety), About (roadmap, changelog, license, report an issue).
concepts/threat-model.mdauto-transcludesdocs/security-vectors.mdso the public threat model stays in lockstep with the design source.
- [Docs]
SECURITY.md— root-level vulnerability disclosure policy.
- [Admin UI] List-page truncation polish — Attributes, Policies, and Roles list tables now truncate long names and descriptions with tooltip titles instead of letting them push the table layout.
- [Admin UI] "Report an issue" footer link — now points at
docs.getbetweenrows.com/about/report-an-issueinstead of the GitHub issues page. - [Docs] Threat model H1 —
docs/security-vectors.mdretitled from "Security Vectors" to "Threat Model" so the transcluded public page has the right heading.
- [Both]
/docs-synccommand — new Claude Code workflow that detects drift betweendocs/+ source anddocs-site/, presents findings for review, and applies approved edits.- Diff mode (
/docs-sync <range>), full-codebase audit (--full), and single-page audit (--page <path>). - Runs automatically as step 2 of
/release.
- Diff mode (
- [Docs]
docs-site/.gitignore—**/.vitepress/{dist,cache}/now matches at any depth to guard against stray builds from the wrong cwd.
- [Proxy] Extensive policy enforcement tests for aggregates, HAVING, window functions, CTEs, and subqueries — +1240 lines in
policy_enforcement.rscovering how column masks, column denies, and column allows interact withCOUNT(DISTINCT),GROUP BY/HAVING,ROW_NUMBER() OVER (ORDER BY ...), CTEs, and subqueries. Ensures masked values cannot leak through aggregates or window ordering. - [Docs] Security vectors documentation overhaul — major expansion of
docs/security-vectors.mdwith new attack vectors, defenses, and test back-references;docs/permission-system.mdupdated in lockstep. - [Demo] Ecommerce demo refresh — new
compose.demo.yaml, newsetup.shautomation script, updatedschema.sqlandseed.py, refreshedpolicies.yamlandrequirements.txt, and a rewritten README.
-
[Proxy] BREAKING:
ctx.query.tablesis now an array of objects, not strings — decision functions withevaluate_context = "query"previously receivedctx.query.tablesasstring[](e.g.["public.orders"]). It is nowArray<{datasource, schema, table}>, so decision function JS must access the fields explicitly. Bare references likeSELECT * FROM ordersnow also resolve to the session's default schema (e.g.public) rather than an empty schema segment, so qualified and unqualified references produce identical entries.Migration for any decision function that inspected
ctx.query.tables:// Before: ctx.query.tables.includes("public.orders") ctx.query.tables.some(t => t.startsWith("public.")) // After: ctx.query.tables.some(t => t.schema === "public" && t.table === "orders") ctx.query.tables.some(t => t.schema === "public")
-
[Admin UI] Form polish — small tweaks to
CatalogDiscoveryWizard,DecisionFunctionModal,PolicyForm, andDataSourceEditPage.
- [Proxy] Security: bare table references could bypass schema-scoped policies — unqualified references like
FROM orderspreviously used an empty schema segment as the policy lookup key, so a policy targetingschemas: ["public"]would not match and could be bypassed by omitting the prefix. Bare references now fall back to the session's default schema, which DataFusion is already configured with at connect time (SET search_pathis blocked upstream byReadOnlyHook). Tracked as vector #71 indocs/security-vectors.md.
- [CI] Pre-commit hook runs
docs-siteVitePress build when docs-site changes are staged — guarded by adocs-site/node_modulescheck so fresh clones without docs deps installed are not blocked. - [CI]
docs-siteGitHub Actions job added then disabled — the job is commented out untildocs-site/lands in the repo.
- [Both] Remove git commit hash from version display — simplifies the build and
/healthendpoint- drops
GIT_COMMIT_SHORTenv var and git-basedbuild.rslogic from the proxy - sidebar now shows
vX.Y.Zinstead ofvX.Y.Z (abc1234)
- drops
- [Admin UI] Polish admin UI tables and forms
- attribute definitions table: combine display name + description into a single column, show
entity.keyas a monospaced code chip, and render value type as a type signature (e.g.list<string> ∈ {us, eu}) - roles list: fold description under role name, drop the standalone description column
- user form: add a permissions description explaining what admin access grants
- attribute definitions table: combine display name + description into a single column, show
- [Admin UI] Rename audit nav and standardize route paths
- sidebar section renamed from "Activity" to "Audit"
- nav labels changed to "Query Logs" / "Admin Logs"
/auditroute renamed to/query-auditfor consistency with/admin-audit
- [Both] Entity search, copyable IDs, and audit improvements — server-side search for attribute definitions, entity search dropdowns on audit pages, copyable UUID components across list pages, debounce hook, and new admin/query audit page tests
- Proxy: search filter on
GET /attribute-definitions, copyable IDs in audit responses, policy enforcement test coverage for missing attribute defaults - Admin UI:
CopyableIdcomponent,EntitySelectcomponent,useDebouncehook, admin audit & query audit page tests
- Proxy: search filter on
- [Both] Version display — app version and git commit hash shown in sidebar footer
- Proxy:
/api/versionendpoint serving version fromCargo.toml+ build-time git commit - Admin UI:
useVersionhook, version display in Layout
- Proxy:
- [Both] Debounced search and
keepPreviousData— replaced form-submit search with real-time debounced search across all list pages (Users, Roles, Policies, Data Sources, Attributes); addedkeepPreviousDatato prevent layout flash during transitions - [Admin UI] Sidebar navigation redesign — grouped nav into Access Control / Data / Activity sections with Heroicons; added "Report an issue" link in footer; username prefixed with
@ - [Admin UI] Default value UX improvements — type-specific placeholders, inline NULL badge when empty, icon-based clear button in attribute definition form
- [Admin UI] Attribute definitions table — added entity type column, reordered entity type filter before search input
- [Admin UI] Audit timeline ��� reduced page size from 20 to 5 for inline timelines; left-aligned pagination
- [Admin UI] Table header styling — consistent
text-xssizing across all list page headers - [Both] NULL terminology standardized — replaced inconsistent "no default (null)" phrasing with explicit "NULL" across UI, docs, code comments, and security vectors
- [Proxy] Zero-column scan — fixed
EmptyProjectionFixRulehandling when all columns are denied
- Tenant as custom attribute — the built-in
tenantcolumn onproxy_userhas been removed; tenant is now managed entirely through the ABAC attribute definition system- Migration 055 drops the
tenantcolumn fromproxy_user {user.tenant}template variable still works — resolves from the user's custom attributesBR_ADMIN_TENANTenv var removed (was already deprecated)tenantis no longer a reserved attribute key — can be created/deleted like any other custom attribute- Admin UI tenant field removed from user forms and list pages
- Migration 055 drops the
- BetweenRows rebrand — renamed from QueryProxy to BetweenRows across CLI, admin UI, Dockerfile, and configuration files
- Auto-persisted secrets — encryption key and JWT secret now follow a three-tier resolution: env var → persisted file → auto-generate and save
- Keys are persisted to
.betweenrows/state directory alongside the database, surviving container restarts without explicit env vars - Persistence warning on startup if the state directory is missing alongside existing data (likely unmounted volume)
- Data directory inferred from
BR_ADMIN_DATABASE_URLfor consistent state file placement
- Keys are persisted to
- Startup banner — displays version and tagline on boot
- Linux aarch64 support for Javy —
build.rsnow downloads the correct Javy binary forlinux/aarch64(ARM servers, Graviton, etc.) - Docker quickstart compose —
compose.quickstart.yamlfor one-command local setup - Governance workflows roadmap — detailed design for three-tier governance (none → draft → code) with sandboxes, YAML-as-code, and CI/CD deployment
- README rewritten as user-facing quickstart — streamlined for new users with Docker quick start, 5-minute walkthrough, configuration reference, and policy overview
- Developer docs moved to CONTRIBUTING.md — architecture details, data model, API reference, and performance notes relocated from README
- Fly.io deployment docs — extracted to
docs/deploy-fly.md - SQLx logging suppressed below DEBUG —
sqlx_loggingnow only enabled whenRUST_LOGincludes DEBUG or lower, reducing noise in defaultinfomode - Dockerfile sets
BR_ADMIN_DATABASE_URL— explicitly sets the SQLite path to/data/proxy_admin.dbfor consistent data directory detection
.betweenrows/added to.gitignore— auto-persisted state directory excluded from version control
- Decision function test context — mock context in the expression editor nested user attributes under an
attributeskey instead of flattening them as top-level fields onctx.session.user, causing runtime errors when testing functions that access custom attributes (e.g.ctx.session.user.departments). Added cross-reference comments betweencontext.rsandDecisionFunctionModal.tsxto prevent future drift.
- Flatten user attributes in decision function context — Custom attributes are now first-class fields on
ctx.session.user(e.g.,ctx.session.user.region) instead of nested underctx.session.user.attributes. Built-in fields (id,username,tenant,roles) always take priority on collision.- BREAKING: Existing decision functions referencing
ctx.session.user.attributes.*must be updated toctx.session.user.*
- BREAKING: Existing decision functions referencing
- Expression editor with autocomplete — Filter and mask expression fields in PolicyForm now use a CodeMirror editor with
{user.*}template variable autocomplete (built-in + custom attribute definitions). - Server-side expression validation — New
POST /policies/validate-expressionendpoint and "Check" button in the expression editor to validate filter/mask syntax before saving. - ORM-derived reserved attribute keys — Reserved user attribute keys are now computed from the
proxy_userORM columns (+ virtual fields likeroles), preventing accidental collisions with DB-level field names. - Conditional policy documentation — Comprehensive ABAC expression patterns and conditional policy examples for all five policy types added to
docs/permission-system.md. Conditional Policies marked as resolved in roadmap (covered byCASE WHENexpressions + decision functions).
- List attribute type for ABAC user attributes — new
"list"value type for attribute definitions, storing arrays of strings (max 100 elements)- Use with
IN ({user.KEY})in filter expressions; list expands into comma-separated placeholders - Empty lists expand to
NULL(effectively returning no rows) - API validates list values as JSON arrays of strings;
allowed_valuesconstrains individual elements - Decision function context includes list attributes as JSON arrays
- Admin UI: tag/chip input for free-form lists, multi-select checkboxes for lists with allowed values
- Extracted
AttributeDefinitionFormcomponent (matchesRoleForm/DataSourceFormpattern) - Added PolicyForm validation: blocks submit when decision function toggle is on but no function is attached or reference is stale
- DecisionFunctionModal: autocomplete hints for per-attribute
ctx.session.user.attributes.*, test context pre-populated from current user's real attributes - Config JSON validation: blocks save on invalid JSON instead of silently defaulting to
{}
- Use with
- User Attributes (ABAC) — schema-first attribute system for attribute-based access control
attribute_definitiontable defines allowed keys with types (string/integer/boolean), entity type scoping, optional enum constraints, and reserved key protection- User attribute values stored as JSON column on
proxy_userwith full-replace semantics and write-time validation - Typed
{user.KEY}template variables in filter/mask expressions (Utf8/Int64/Boolean literals) - User attributes available in decision function context as
ctx.session.user.attributeswith typed JSON values time.now(RFC 3339 evaluation timestamp) added to decision function context for time-windowed access- Admin UI: attribute definition list/create/edit pages, user attribute editor with type-aware inputs
- CRUD API with
?force=truecascade delete (SQLitejson_remove()/ PostgreSQLjsonb -) - 3 new migrations (052–054)
- Save-time expression validation — filter and mask expressions are validated at policy create/update time; unsupported SQL syntax returns 422 immediately instead of failing silently at query time
- CASE WHEN expression support added to the expression parser
- Shared WASM runtime — consolidated
WasmDecisionRuntimeinto a singleArcsingleton created at startup, shared byPolicyHook,EngineCache, andAdminState(replaces per-use instantiation) - Security vectors doc renamed —
docs/permission-security-tests.md→docs/security-vectors.md; added vectors 59–68 covering predicate probing, aggregate inference, EXPLAIN leakage, HAVING bypass, CASE expression bypass, window function ordering, timing side channels, and ABAC-specific vectors
- Decision functions — custom JavaScript functions that control when policies fire, evaluated in a sandboxed WASM runtime
- Two evaluation contexts:
session(evaluated once at connect) andquery(evaluated per query) - Configurable error handling:
skip(policy doesn't fire) ordeny(query blocked) - Console log capture with configurable log levels
- CRUD API with test endpoint for dry-running functions against mock contexts
- Integrated into visibility-level evaluation:
column_denyandtable_denypolicies respect decision function results at connect time
- Two evaluation contexts:
- Decision function admin UI — modal for creating/editing decision functions with CodeMirror editors
- JavaScript and JSON editors with
ctx.*/config.*autocomplete - Templates for common patterns in create mode
- Test panel with client-side pre-check and server-side WASM execution
- Fire/skip/error result badges, shared function warning, optimistic concurrency
- PolicyForm integration: toggle-based attachment (create new / select existing / edit / detach)
- JavaScript and JSON editors with
- Stale decision function reference dead-end — detaching a deleted function now correctly reveals create/select buttons instead of leaving the user stuck
- Testcontainers leak — label containers and clean up orphans to prevent Docker resource exhaustion during test runs
- RBAC with transactional audit enforcement — role-based access control with
AuditedTxnwrapper- Roles with hierarchical membership (BFS traversal, cycle detection, depth cap)
- Policy assignments scoped to user, role, or all
AuditedTxnensures every admin mutation is atomically committed with its audit log entries- Role deactivation/reactivation cascades to policy visibility
- Datasource access gating by role membership
- Remove LICENSE from git history — dropped LICENSE commit from history, added
LICENSE*/LICENCE*to.dockerignore
- Scan-level column masking — Column masks now apply at the
TableScanlevel viatransform_upinstead of only at the top-level Projection, preventing CTE and subquery nodes from bypassing masks by changing the DFSchema qualifier.- Masks run before row filters so filters evaluate against raw (unmasked) data
- Integration tests for multi-table JOINs with scoped column deny, CTE mask bypass prevention, subquery mask enforcement, and combined mask+deny+filter scenarios
- Dependency upgrades — Rust 1.94, DataFusion 52.3, Vite 7, TypeScript 5.9
- Read-only hook test assertions — use
as_db_error()instead ofto_string()for reliable error matching - Row filter projection expansion — fix CI test failures related to projection expansion and
table_denyaudit status - Unused variable warning — fix compiler warning in
catalog_handlers
- CI actions upgraded to v5 —
actions/checkoutandactions/setup-nodeupdated from v4 to v5 for Node.js 24 support
- Flat policy type model — replaced the obligation model with a flat
policy_typefield- 5 types:
row_filter,column_mask,column_allow,column_deny,table_deny - Removed
column_accessaction field; type alone determines behavior - 5 new migrations (019–023) to migrate existing schema
- 5 types:
- Zero-trust column model — qualified projection for JOINs; per-user column visibility enforcement
- Cast support — SQL type cast expressions now handled in query processing
- Catalog hints — contextual hints surfaced in the catalog discovery UI
- Policy-centric assignment panel — rule assignment UI redesigned around policies rather than datasources
- Datasource assignment on create — assign a datasource when creating a new rule
- Audit status tracking — queries now record a status field in the audit log
- Audit write rejections — rejected write queries are now captured in the audit log
- Container-based integration tests — replaced manual test scripts with a Docker-based test suite
- Column mask and row filter bugs — fixed incorrect mask application and cross-policy row filter interactions
- Audit duration and rewritten query — fixed these fields not being recorded correctly
- SPA routing — production build now serves
index.htmlfor client-side routes
- Policy system — configurable row filtering, column masking, and column access control via named policies assigned to datasources and users
policy,policy_version,policy_obligation,policy_assignment,query_audit_logdatabase tables (migration 007)PolicyHookreplacesRLSHook; supportsrow_filter,column_mask, andcolumn_accessobligation types- Template variables (
{user.tenant},{user.username},{user.id}) with parse-then-substitute injection safety - Wildcard matching (
schema: "*",table: "*") in obligation definitions access_modeon datasources:"policy_required"(default) or"open"- Optimistic concurrency via
versionfield (409 Conflict on mismatch) - Immutable
policy_versionsnapshots on every mutation for audit traceability - Deny policies short-circuit with error before plan execution
- 60-second per-session cache with
invalidate_datasource/invalidate_userhooks
- Policy API — admin-only CRUD and assignment endpoints
GET/POST /policies,GET/PUT/DELETE /policies/{id}GET/POST /datasources/{id}/policies,DELETE /datasources/{id}/policies/{assignment_id}
- Query audit log — async logging of every proxied query
GET /audit/querieswith pagination and filtering by user, datasource, date range
- Visibility-follows-access — per-connection, per-user filtered
SessionContext- Users only see tables and columns their policies permit
- Policy changes take effect immediately without reconnect
- JSON/JSONB support —
jsonandjsonbcolumns via DataFusion v52 anddatafusion-functions-json->/->>operators and JSON UDFs available in queries- Filter pushdown to upstream PostgreSQL for supported operators
- EXPLAIN support —
EXPLAIN <query>returns a PostgreSQL-compatible single-columnQUERY PLANresponse - Admin UI — Policies — list, create, and edit policies with an obligation builder; inline enable/disable toggle
- Admin UI — Policy Assignments — assign/remove policies per datasource with optional user scope and priority
- Admin UI — Query Audit — paginated audit log with original query, rewritten query, and applied policy snapshots
- Demo e-commerce schema —
scripts/demo_ecommerce/with schema, seed script, and example policies - Docs —
docs/permission-system.md(user guide) anddocs/security-vectors.md(security test plan)
- Arrow encoding — migrated to
arrow-pg; handler simplified; removedarrow_conversionandsql_rewritemodules
- CI/CD — split into CI (tests on every push to
main) and CD (publish + deploy onv*tag only)- Docker images tagged
X.Y.ZandX.Y; deploy uses explicit version tag for prod traceability workflow_dispatchadded for manual redeployment of an existing version
- Docker images tagged
- Password toggle visibility on login/password fields
- Password complexity validation
- Catalog viewer page for browsing the discovered catalog
- Button on the data source list view to open the catalog viewer
tsc -btypecheck failure inclient.test.ts; alignedtypecheckscript accordingly
- TypeScript errors in test files (
as unknown ascasts, unused imports) that were silently ignored by Vitest/esbuild but caught bytscduring Docker build
- Add
typecheckscript (tsc --noEmit) toadmin-uiand run it in the pre-commit hook before tests, so type errors are caught locally before CI
/commitslash command for Claude Code/releaseskill with semver Docker image tagging
- Admin-ui test suite with Vitest, integrated into CI
- Multi Data Source Management: The proxy now supports connecting to multiple, dynamically configured upstream data sources.
- Data Source Admin API & UI: New endpoints and UI pages for creating, editing, and testing data source configurations.
- User-to-Data Source Access Control: Implemented a many-to-many permission model to assign users to specific data sources.
- Encryption at Rest: Sensitive data source configuration fields (e.g., passwords) are now encrypted with AES-256-GCM in the database.
- Engine Cache: Implemented a cache for DataFusion
SessionContexts, one for each active data source, to improve performance and resource management. - Structured Logging: Replaced
println!withtracingfor structured, level-based logging.
- Authentication Flow: The PostgreSQL
databaseparameter in the connection string is now used to select the target data source. - Project Version: Incremented crate versions to
0.2.0to reflect new feature set. - Schema Alias Support: Catalog discovery now supports schema aliases for more flexible data source mapping.
- Per-Column Selection: Catalog discovery wizard allows selecting individual columns per table.
- Idle Connection Timeout: pgwire proxy now closes idle connections after a configurable timeout.
- Fly.io Auto Stop/Start: Deployment is configured to automatically stop and start machines based on traffic.
- Initial implementation of the PostgreSQL wire protocol proxy.
- Authentication for proxy users via Argon2id password hashing.
- Basic query processing using the Apache DataFusion engine.
- Rudimentary admin REST API for user management.
- Initial Admin UI for listing and creating users.