Documentation

Run and configure AccessFlow.

An operator and admin guide — how to install AccessFlow in different environments, how to start it for the first time, and how to configure every entity that drives the review pipeline: organizations, users, roles, datasources, review plans, AI, OAuth, SMTP, and notification channels. Everything here is sourced from the engineering chapters and the project README.

Read this first

AccessFlow is a self-hosted query proxy. Your team connects to AccessFlow instead of directly to PostgreSQL, MySQL, MariaDB, Oracle, Microsoft SQL Server (or any other JDBC-compatible engine, via an admin-uploaded driver), the NoSQL document engines MongoDB and Couchbase, the NoSQL key-value engines Redis and Amazon DynamoDB, the NoSQL wide-column engines Apache Cassandra and ScyllaDB, the NoSQL search engines Elasticsearch and OpenSearch, or the NoSQL graph engine Neo4j. Every statement is parsed, classified, optionally AI-reviewed, and routed through a configurable human-approval workflow before it touches the database. Customer-database credentials never leave the proxy.

This guide is split into three parts: running AccessFlow (pick one of three deployment modes), first-time setup (browser wizard or GitOps env vars), and configuration (every entity an admin manages through the UI or REST API).

Versions referenced. Internal store is PostgreSQL 18; cache is Redis 8 (used for token revocation and scheduler locks). The backend service and the React-based admin SPA are both shipped as container images — see the Install section on the homepage for the canonical pinned versions, and the Manual / from source section below for build-time toolchain requirements.
Sizing. Two reference points:
  • Evaluation — 2 vCPU · 4 GB RAM · 10 GB SSD on a single host. Fits the Docker Compose demo (backend + frontend + Postgres + Redis) with headroom.
  • Production — backend is stateless: run two or more replicas of (2 vCPU · 2 GB RAM) behind any L7 load balancer. Internal Postgres needs roughly 2 vCPU · 4 GB RAM · 50 GB SSD (audit log dominates disk growth — provision higher IOPS for high-volume orgs). Redis needs ~1 vCPU · 1 GB RAM. Frontend is static — front it with a CDN or any nginx pod.
Numbers are starting points for an org of roughly fifty reviewers and a few hundred daily queries. Scale backend replicas first when you outgrow them.

Running AccessFlow

Pick one of three modes. Docker Compose is the fastest path to a running instance. Helm is the production-recommended path on Kubernetes. The manual / from-source path is for contributors and for environments where containers aren't available.

Docker Compose

The repo root ships a zero-config demo stack — it pulls the published images from GHCR and starts Postgres + Redis + backend + frontend with insecure demo keys baked in so a fresh clone runs with one command:

shell — demo
git clone https://github.com/bablsoft/accessflow.git
cd accessflow
docker compose up -d
# open http://localhost:5173 — the in-app setup wizard creates the first admin
Demo only. The root docker-compose.yml embeds insecure JWT_PRIVATE_KEY and ENCRYPTION_KEY defaults inline so it works on a fresh clone. Do not deploy this to anything but a sandbox. For real environments, use the production-style compose below or the Helm chart.

For a production-style compose, generate real keys and supply them via .env:

.env — production
# 32-byte hex — AES-256-GCM for datasource credential encryption
ENCRYPTION_KEY=$(openssl rand -hex 32)

# RSA-2048 PEM — JWT RS256 signing key
JWT_PRIVATE_KEY="$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null)"

DB_PASSWORD=change-me
# Password for the dedicated audit-writer role (issue #67). Provisioned by
# deploy/postgres-init/01-audit-role.sql — see "audit_log role separation".
AUDIT_DB_PASSWORD=change-me-audit
CORS_ALLOWED_ORIGIN=https://accessflow.company.com
ACCESSFLOW_PUBLIC_BASE_URL=https://accessflow.company.com

See docs/09-deployment.md → Docker Compose for the full production-style compose (including the optional ollama profile for self-hosted AI).

Structured logs for ELK / OpenSearch. Set ACCESSFLOW_LOGGING_STRUCTURED_FORMAT=logstash (or ecs / gelf) on the backend container and every log line becomes a single JSON object — `traceId` and `spanId` from the Micrometer tracing bridge are top-level fields, so correlation works out of the box. The Spring Boot ASCII banner is hidden by default; set SPRING_MAIN_BANNER_MODE=console to restore it.

Tracing & metrics. Set OTEL_EXPORTER_OTLP_ENDPOINT to your collector's full OTLP/HTTP traces URL (e.g. http://tempo:4318/v1/traces) and AccessFlow exports the proxy-pipeline spans — parse → AI analyze → pool acquire → execute — to Tempo / Jaeger / Honeycomb (sampling via ACCESSFLOW_TRACING_SAMPLING_PROBABILITY; export is off until an endpoint is set). Prometheus metrics are exposed at /actuator/prometheus (unauthenticated for in-cluster scraping — keep /actuator off the public ingress). The Helm chart ships two pre-built Grafana dashboards behind dashboards.enabled=true covering query volume, approval SLAs, AI usage/cost, rejection rates, and connection-pool stats — see examples/values-observability.yaml.

Kubernetes & Helm

The Helm chart ships at charts/accessflow/ and is published to https://bablsoft.github.io/accessflow. With defaults, no Secrets need to be pre-created — the chart auto-generates the encryption key, the JWT private key, and the PostgreSQL password on first install, and preserves them across helm upgrade:

shell
# 1. add the chart repo
helm repo add accessflow https://bablsoft.github.io/accessflow
helm repo update

# 2. install — chart auto-generates secrets on first run
helm install accessflow accessflow/accessflow \
  --namespace accessflow --create-namespace \
  -f values.yaml

Prefer to manage the secrets yourself (sealed-secrets, External Secrets, Vault, …)? Pre-create them and point config.encryptionKey.existingSecret, config.jwtPrivateKey.existingSecret, and postgresql.auth.existingSecret at your own resources:

shell
kubectl -n accessflow create secret generic accessflow-encryption-key \
  --from-literal=value="$(openssl rand -hex 32)"

kubectl -n accessflow create secret generic accessflow-jwt-key \
  --from-file=value=<(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048)

# Postgres Secret needs BOTH keys: `password` (AccessFlow user) + `postgres-password` (admin)
kubectl -n accessflow create secret generic accessflow-pg-secret \
  --from-literal=password="$(openssl rand -base64 24)" \
  --from-literal=postgres-password="$(openssl rand -base64 24)"

The chart bundles Bitnami subcharts for Postgres and Redis (toggle off with postgresql.enabled=false / redis.enabled=false to point at external instances). TLS on the Ingress is off by default; set ingress.tls.enabled=true with a secretName and (optionally) a cert-manager annotation to terminate HTTPS. Full values reference: docs/09-deployment.md → Kubernetes & Helm and charts/accessflow/README.md.

Ready-made starting points live under charts/accessflow/examples/ — each file is a minimal override on top of the chart's values.yaml. They split into deployment shapes (cluster-level: replicas, ingress, secrets model) and bootstrap slices (declarative admin config). The intended pattern is one of each, plus your own site-specific overrides.

Deployment shapes:

Bootstrap slices (each declares organization + first admin user and is meant to layer on a deployment shape):

shell
helm install accessflow accessflow/accessflow \
  --namespace accessflow --create-namespace \
  -f charts/accessflow/examples/values-production.yaml \
  -f charts/accessflow/examples/values-bootstrap-oauth2-sso.yaml \
  -f my-site-overrides.yaml

Manual / from source

For contributors and air-gapped builds. Requires JDK 25 and Node.js 24 on the host:

shell
git clone https://github.com/bablsoft/accessflow.git
cd accessflow

# 1. infrastructure — Postgres 18 + Redis 8 + Mailcrab (dev-only compose)
docker compose -f backend/docker-compose-dev.yml up -d

# 2. backend
cd backend
./mvnw spring-boot:run

# 3. frontend (in another shell)
cd frontend
npm install
npm run dev

# open http://localhost:5173

For full coding standards, test commands, and the dev loop, see docs/11-development.md.

Beta / pre-release channel

Pre-release builds are cut for internal testing ahead of a stable release. They are published with a -beta.N / -rc.N version tag and a moving :beta Docker tag — never :latest — so a beta never disturbs production: a plain docker compose up or helm install stays on the last stable release until you opt in.

Internal testing only. Betas carry no upgrade-path guarantees toward the GA they precede. Treat their databases as disposable.

Docker Compose — drop a docker-compose.override.yml next to the demo file (Compose merges it automatically), then docker compose pull && docker compose up -d:

docker-compose.override.yml
services:
  backend:
    image: ghcr.io/bablsoft/accessflow-backend:beta    # or :1.2.0-beta.1
  frontend:
    image: ghcr.io/bablsoft/accessflow-frontend:beta   # or :1.2.0-beta.1

Helm — pre-release chart versions are hidden from default resolution, so opt in with --devel and pin the exact version:

shell
helm repo update
helm install accessflow accessflow/accessflow \
  --version 1.2.0-beta.1 --devel \
  --namespace accessflow --create-namespace

See docs/09-deployment.md → Installing a pre-release / beta build for the full consumer guide.

First-time setup

There are two ways to bring up a brand-new AccessFlow deployment.

Option A — Browser setup wizard

The default. Open the frontend (http://localhost:5173 for the demo stack, or whatever URL serves the SPA in your environment). When no organization exists yet, the app routes to /setup and walks you through:

  1. Create the organization.
  2. Create the first admin user (email + password).
  3. Optional: configure system SMTP so invitation emails work.
  4. Optional: add a first datasource and a default review plan.

After the wizard finishes, log in as the admin and finish configuring the system from the admin pages — see Configuration below.

Option B — Bootstrap via env vars (GitOps)

Set ACCESSFLOW_BOOTSTRAP_ENABLED=true and supply ACCESSFLOW_BOOTSTRAP_* properties for the organization, admin user, review plans, AI configs, datasources, SAML, OAuth2, Langfuse, notification channels, and system SMTP. On every startup the bootstrap module reconciles the declared configuration into the database — declared rows are upserted, omitted rows are left alone.

Reconciliation order. Organization → admin user → notification channels → AI configs → review plans → datasources → SAML → OAuth2 → Langfuse → SMTP. Secret fields (passwords, API keys, webhook secrets) are read from Kubernetes Secrets, never from a ConfigMap. Full property tree: docs/09-deployment.md → Bootstrap configuration.
Multi-replica safety. Bootstrap is safe under replicaCount.backend > 1: every pod races for a Redis-backed bootstrapReconcile lock (the same Redis instance that powers ShedLock and JWT refresh tokens), so exactly one replica performs the upserts per startup wave. The other replicas log an INFO line and keep serving traffic. No additional env vars to configure.
Auditability. Bootstrap upserts are recorded in audit_log with actor_id = NULL and metadata.source = "BOOTSTRAP", so operators can answer "who changed this — me, or a helm upgrade?" from a single source of truth. Restarting with unchanged env vars writes zero new rows (the reconciler caches a SHA-256 fingerprint of each declared spec in bootstrap_state and short-circuits on a match).

Configuration

Everything below is admin-only. This page walks through each entity in the admin UI; every screen has a REST equivalent in docs/04-api-spec.md if you'd rather automate. Sign in as the admin you created in first run, then use the left-hand Admin sidebar to jump between sections.

Organizations & quotas

What it is. Multi-tenancy. A single AccessFlow deployment hosts one or more organizations, each a fully-isolated tenant — every user, datasource, query, and audit row belongs to exactly one org, and a tenant can never reach another's data. Reach for it when you host several teams or customers on one deployment and want per-tenant resource caps; most operators run a single org and can ignore the cross-org controls.

Platform admin (super-admin). Managing organizations across the cluster requires the platform-admin capability — a flag on a user, separate from the four roles below. A platform admin keeps their normal org role and additionally sees a Platform navigation group with the /admin/organizations screen. The first admin you create (via the setup wizard or the bootstrap env vars) is provisioned as a platform admin automatically.

The AccessFlow platform-admin Organizations page listing each tenant organization with its per-org datasource, user, and daily-query quotas.
/admin/organizations — platform admins manage every tenant and its quotas from one screen.

Configure it. Platform admins manage tenants and their quotas from /admin/organizations:

  1. Create an organization. From /admin/organizations, click New organization, give it a name (an optional URL-safe slug is derived from the name when blank), and optionally set quotas. The org starts enabled and empty — add its first admin and datasources afterwards.
  2. Set per-org quotas. Cap how much a tenant may consume: max datasources, max users, and max queries per day (a rolling trailing-24-hour count). Leave a field blank or set it to 0 for unlimited. Quotas are enforced when the resource is created — a breach is rejected with HTTP 409 QUOTA_EXCEEDED and a message naming the limit, so the datasource / user / query simply isn't created.
  3. Edit name & quotas. Open /admin/organizations/<id> to change the name or raise / lower a quota, and to see live usage bars (current count vs. limit) for each cap.
  4. Disable a tenant. Use Disable as a kill-switch — its users are blocked at login (local and SSO) and every in-flight session stops on its next request. The block is immediate (no cache, no waiting for token expiry). Enable restores access.

Every organization action (create, update, disable, enable) is written to the audit log against the target org. Per-org login pages / SSO routing across multiple orgs are not yet available — see the roadmap.

Users

What it is. The people who can sign in to AccessFlow and the role each one carries. Create accounts directly, or let SAML / OAuth users be auto-provisioned on first sign-in when SSO is enabled (see SAML and OAuth).

Configure it. Manage everyone from /admin/users:

Invite a teammate modal in the AccessFlow admin Users page, with fields for email, display name, and a role dropdown defaulting to ANALYST.
/admin/usersInvite via email. Enter the recipient's email, pick a role, and AccessFlow emails a one-time signup link.
  1. Invite via email (default). From /admin/users, click Invite via email, fill in the recipient's email, optional display name, and role, then submit. AccessFlow generates a signup token and emails it; the link expires after ACCESSFLOW_SECURITY_INVITATION_TTL (default 7 days).
  2. Create with a password. Use the dropdown next to the invite button → Create with password to provision a user directly. Useful when SMTP isn't configured yet, or when you want to seed an account synchronously.
  3. Edit or deactivate. Click any row to change the role or flip the active toggle. Deactivated users can't sign in but their audit trail is preserved.
  4. Pending invitations are listed below the user table; resend or revoke them from there.

Tune it. ACCESSFLOW_SECURITY_INVITATION_TTL (invite-link lifetime, default P7D), ACCESSFLOW_SECURITY_PASSWORD_RESET_TTL (reset-link lifetime, default PT1H), and ACCESSFLOW_SECURITY_PASSWORD_RESET_RESET_BASE_URL (link base, default http://localhost:5173).

User roles & RBAC

What it is. Role-based access control. Every user carries one org-wide role that caps what they can do; on top of it, per-datasource permissions decide which databases they may touch. Pick the lowest-privilege role that still lets someone do their job — only ADMIN can change configuration, and a user can never approve their own query, whatever their role.

Configure it. Assign one of the five roles when you create or edit a user on /admin/users; the matrix below is what each role may do.

Platform admin is separate from the four roles. The platform-admin capability is an orthogonal flag, not a fifth role — a platform admin keeps whatever role their home org assigns and is additionally allowed to manage organizations across the cluster (/admin/organizations). It grants no extra capability inside any single org; the matrix below still governs every tenant-scoped action.

Capability READONLY ANALYST REVIEWER ADMIN AUDITOR
Submit SELECT queries
Submit DML (INSERT / UPDATE / DELETE)
Submit DDL (CREATE / ALTER / DROP)
View own query history
View all queries in the org
Approve / reject queries
Request time-bound datasource access (JIT)
Review / approve access requests
Manage datasources
Manage users
Manage user groups
Manage review plans
View audit log
Manage notification channels
Configure AI
Configure SAML / OAuth
View / export compliance reports
View behavioural anomalies (UBA)
Acknowledge / dismiss anomalies
Break-glass / emergency execution
View break-glass log
Acknowledge break-glass events

Break-glass / emergency execution is not granted by role — it is gated by a separate per-user, per-datasource can_break_glass permission that an admin grants explicitly (required for everyone, including admins; time-boxed). A user can break glass only on a datasource they hold that grant for, and only for query types they already have the capability for.

Which role for what. Use READONLY for people who only need to look at production data (analysts, on-call engineers reading dashboards). Use ANALYST for people who write data through reviewed queries. Use REVIEWER for people who approve other users' queries — typically senior engineers or DBAs. Use ADMIN for the platform-team operators who configure the system itself. Use AUDITOR for a dedicated, read-only compliance reviewer — it sees only the compliance dashboard (/admin/auditor): pre-built PII/PCI/GDPR access and DDL/DELETE reports with signed PDF/CSV export, and nothing else.

Datasource-level permissions. Role is the org-wide ceiling. On top of it, every user needs an explicit per-datasource permission grant to access a given database — it controls read / write / DDL per datasource, row caps, allowed schemas / tables, and restricted columns (which are masked as *** in SELECT results). See docs/07-security.md for the full authorization matrix.

Group-based access grants. Rather than a row per person, an admin can grant a user group access to a datasource or an API connector (same read / write / DDL / break-glass controls); every member inherits the grant, and adding someone to the group gives them access without a new grant. When a user has both a direct grant and one or more group grants, their effective access is the most-permissive union — capabilities are OR-ed, allow-lists merge, restricted-column masks apply only where every grant restricts, and each grant's expiry is honoured independently.

Just-in-time (JIT) access requests. Instead of an admin pre-granting a datasource permission, any user can request temporary, scoped access from /access-requests — pick a datasource, the capabilities they need (read / write / DDL), an optional schema/table scope, and a duration. The request runs through the same reviewer-eligibility and multi-stage approval engine as query review (a requester can never approve their own). Admins are the backstop approver: an admin sees and can approve every pending access request from /admin/access-requests — even on datasources with no review plan — so a request is never stuck waiting for an approver who was never configured. On final approval AccessFlow writes a time-boxed permission grant (expiring at now + duration); it's revoked automatically on expiry, and an admin can revoke an active grant early from /admin/access-requests. Tune the revocation cadence with ACCESSFLOW_ACCESS_GRANT_EXPIRY_POLL_INTERVAL (default PT5M) and the allowed duration window with ACCESSFLOW_ACCESS_MIN_DURATION / ACCESSFLOW_ACCESS_MAX_DURATION (defaults PT15M / P30D).

The /admin/access-requests queue listing a pending just-in-time access request — requester, datasource, requested capabilities, duration, and approve / reject / revoke actions.
/admin/access-requests — pending JIT access requests; admins approve, reject, or revoke an active grant.

Break-glass / emergency access. For genuine emergencies — production is down and approvers are unreachable — an admin can grant a user the can_break_glass permission on a datasource (a checkbox on the permission grant, alongside read / write / DDL, time-boxed via the same expires_at). With that grant, an Emergency access button appears on the editor for that datasource: the user supplies a mandatory justification and the query executes immediately, bypassing review — but still through every proxy guard (schema/table allow-list, dynamic masking, row-level security, row caps). The grant is required for everyone, including admins. Each break-glass execution fires instant notifications to all admins (including PagerDuty), writes a prominently-tagged QUERY_BREAK_GLASS_EXECUTED audit row, and opens a mandatory retro-review on the /admin/break-glass log that an admin — never the submitter — must acknowledge after the fact. The executed query keeps its normal terminal state; the retro-review is tracked alongside it.

The AccessFlow break-glass log listing emergency-access executions awaiting an admin's mandatory retro-review acknowledgement.
/admin/break-glass — every emergency execution opens a mandatory retro-review here for an admin (never the submitter) to acknowledge.

User groups

What it is. Named, organisation-scoped collections of users. Use them to (1) bundle reviewers so you can attach a single group — instead of ten individual users — to a datasource as eligible reviewers, (2) grant a whole team data or API access (a datasource or API-connector grant on a group is inherited by every member, so you don't add a row per person), and (3) act as the target of IdP group mappings so SAML / OAuth2 logins keep membership in sync automatically.

Configure it. Manage groups from /admin/groups:

  1. Create a group. Go to /admin/groupsCreate group. Pick a name (e.g. Billing Reviewers) and an optional description.
  2. Add members. Open the group, click Add member, and pick users from the dropdown. Manually-added members are tagged Manual and stay put regardless of the IdP sync.
  3. Use the group. On a datasource's Reviewers tab (/datasources/<id>/settings), add the group as a reviewer. From that point on, members of the group can see and decide queries against that datasource (in addition to plan-approver rules). On the same page's Permissions tab (and an API connector's Permissions tab) you can also grant the group access — switch the grant target from User to Group and every member inherits the read / write / DDL / break-glass grant.
  4. Optional: IdP-managed memberships. Configure group_mappings on the SAML or OAuth2 admin pages so an IdP group claim auto-maps to the AccessFlow group. On every login, AccessFlow replaces the user's IdP-sourced memberships with the mapped set; Manual memberships are never touched.

Per-datasource reviewer scoping. Once a datasource has at least one assigned reviewer (a user or a group), only those reviewers see its queries. Datasources with none fall back to the review-plan approvers — so adopting groups is purely additive, no migration required.

User groups list at /admin/groups showing a table of groups with name, description, member count, created date, and edit/delete actions, plus a Create group button.
/admin/groups — organisation-scoped user groups; open one to manage members.

Datasources

What it is. A governed connection to one of your databases. Every query a user runs against it passes through AccessFlow's review, masking, and row-security guards instead of hitting the database directly — so a datasource is where you decide who may run what against which data. PostgreSQL, MySQL, MariaDB, Oracle, and MS SQL Server are built in; MongoDB, Couchbase, Redis, Cassandra / ScyllaDB, Elasticsearch / OpenSearch, DynamoDB, and Neo4j install from the connector catalog; any other JDBC engine works by uploading its driver and choosing Custom.

Configure it. Create one with the four-step wizard at /datasources/new:

Add datasource wizard, step one (Database type), showing a grid of bundled drivers — PostgreSQL marked Bundled and MySQL, MariaDB, Oracle, MS SQL Server marked Unavailable until their driver is downloaded.
/datasources/new — four-step wizard: Database type → Connection details → Connection test → Configuration.
  1. Database type. Pick a bundled driver tile (PostgreSQL ships built-in; other drivers download on first use and are verified against a pinned SHA-256 checksum). Pick Custom to use a JDBC driver you uploaded under Admin → Custom JDBC drivers.
  2. Connection details. Name the datasource, then enter host, port, database name, service-account username, and password. SSL mode defaults to REQUIRE; switch to VERIFY_FULL in production. For Cassandra and ScyllaDB the wizard also requires a local datacenter (the driver's load-balancing datacenter); this field is unused for every other engine. For Elasticsearch and OpenSearch the wizard offers an Authentication toggle — basic (username + password) or an API key — and the database-name field is optional. For Amazon DynamoDB the connection is cloud credentials, not host/port: the wizard hides host/port and instead asks for the AWS region (the database-name field), the access key ID and secret access key (the username/password fields), and an optional custom endpoint (DynamoDB Local / VPC; blank for AWS). For Neo4j the wizard takes the standard host/port/database/username/password (the SSL mode is encoded in the Bolt scheme) plus an optional Bolt connection URI (advanced) — a full bolt:// / neo4j+s:// URI for Neo4j Aura or clustered routing that, when set, overrides host/port. The password (and API key / secret access key) are AES-256-GCM encrypted on write, decrypted once into the connection pool, and never returned in any GET response.
  3. Connection test. AccessFlow opens a real JDBC connection, runs a heartbeat query, and surfaces any SSL / authentication errors before you save.
  4. Configuration. Pick the Review plan that gates this datasource, toggle Require review on reads / writes, and (optionally) enable AI analysis and/or text-to-query + pick an AI configuration. The AI configuration is shared by both features, so it is required whenever either toggle is on. With text-to-query on, users can draft a query from a natural-language prompt in the editor — in the engine's native query language (SQL or a NoSQL query) — and the draft still flows through the normal review pipeline. Pool size, max rows, and statement timeout default sensibly but can be tightened per datasource.

Read-replica routing (optional). On the datasource settings page, the Read replica card accepts an optional JDBC URL plus username and password. When set, AccessFlow opens a separate connection pool and routes every query classified as SELECT to the replica; INSERT / UPDATE / DELETE / DDL and transactional BEGIN … COMMIT batches always hit the primary. The replica must use the same database engine as the primary (it reuses the primary's JDBC driver). Replica credentials are AES-256-GCM encrypted with the same ENCRYPTION_KEY as the primary. Click Test replica to validate the URL + credentials live without persisting; leaving the password field blank reuses the saved replica password. If the replica is unreachable at query time, AccessFlow falls back to the primary so the query still runs and writes a DATASOURCE_REPLICA_FALLBACK audit row so the failure is visible at /admin/audit-log. Clear the JDBC URL field to disable replica routing. Replica pools reuse the same ACCESSFLOW_PROXY_* connection-pool tuning as the primary — no separate environment variables.

Grant a user access. Open the datasource → Permissions tab and add a row per user — can read / can write / can DDL, allowed schemas, allowed tables, and restricted columns (masked as *** in SELECT results). Without a permission row, a user can't see or query the datasource at all. The allowed schemas / allowed tables lists are enforced when a query is submitted: every table it references — across joins, subqueries, CTEs, and BEGIN; …; COMMIT; batches — must appear in allowed tables or live in an allowed schema, or the query is rejected before it runs. Matching is case-insensitive, and an unqualified table name (FROM users) only matches an unqualified entry in allowed tables. Leave both fields empty to allow every table.

Schema explorer & ER diagram. Each datasource also carries Schema and ER diagram tabs alongside Configuration / Permissions. The schema view introspects the live database (cached and refreshable from the UI) and renders a searchable object tree — one filter matches across schema, table, and column names. Click any table to open a sample-data preview: a small, read-only set of rows fetched through the same governance path as a real query, so row-level security filters the rows and column masking redacts sensitive values (masked columns show ***, never the raw value). The same searchable tree and preview are available in the query editor sidebar. The ER tab lays those tables out as a node-and-edge graph with PK/FK badges and column types so reviewers and operators can sanity-check what a query is touching without leaving AccessFlow.

Datasource settings page with the ER diagram tab active, showing entity boxes for users, datasources, query_requests, audit_log and related tables connected by relationship edges.
/datasources/<id>/settingsER diagram. Auto-laid-out via dagre; node positions persist after manual edits.

Masking policies. The datasource Masking tab adds per-column dynamic data masking on top of the static restricted-columns masking above. Each policy targets a schema.table.column and picks a strategy — full (***), partial (keep the last N characters), hash (stable SHA-256), email (j***@domain), or format-preserving — with an optional reveal-to condition. A query submitter whose role, group, or user id is listed in reveal to sees the unmasked value; everyone else sees the strategy output. A live preview shows how a sample value will render. Masking is applied at result-read time before results are serialized or stored, so unmasked values never persist, and the ids of the policies that applied are recorded in the execution's audit metadata. Reveal is explicit — there is no implicit admin bypass.

The datasource Masking tab listing a per-column masking policy — column reference, strategy (email), and reveal-to roles.
/datasources/<id>/settingsMasking. Per-column dynamic masking with role / group / user reveal conditions.

Row security policies. The datasource Row security tab adds row-level security: per-table predicates the proxy injects into the parsed SQL so a scoped user only sees (SELECT) or affects (UPDATE/DELETE) the rows they are authorised for. Each policy is a structured column operator value predicate where the value is a fixed literal or a :user.* variable — the built-in :user.id / :user.email / :user.role / :user.groups, or an admin-set per-user attribute (the Attributes key/value editor on Admin → Users). The applies to roles / groups / users scope it (empty = everyone, no implicit admin bypass — the inverse of masking's reveal to). Values are bound as parameters, never concatenated; an unresolved variable filters out every row (fail-closed); and a query the engine can't safely rewrite (a policied table inside a UNION, CTE, sub-select, or join-onto-another-policied-table) is rejected rather than run unfiltered. Applied policy ids are recorded in the execution's audit metadata.

The datasource Row security tab listing a per-table predicate — table, column, operator, and a :user.id variable value, scoped by applies-to roles.
/datasources/<id>/settingsRow security. Per-table predicates injected into the parsed SQL; values bound as parameters.

Data classification. The datasource Classification tab tags tables and columns with one or more data classifications — PII, PCI, PHI, GDPR, FINANCIAL, or SENSITIVE — and derives stricter handling automatically. Tagging a column auto-applies a masking policy from the classification's default strategy (PII / GDPR / FINANCIAL → partial, PCI / PHI → full, SENSITIVE → hash), so you don't hand-configure masking for every sensitive field; a table-level tag (no column) is informational. A query that references a tagged table gets an automatic AI risk-score bump, and a derivation preview suggests a stricter review posture (AI review, human approval, minimum approvals) aggregated across the datasource's tags — a suggestion you apply on the datasource's review plan, never auto-changed. Tags are immutable (create / delete) and audited; deleting a tag keeps the masking policy it derived. The classifications appear as badges in the schema explorer, and Admin → Data classifications (/admin/data-classifications) lists every tag across all datasources as the evidence base for compliance reporting.

Tune it. Per-datasource fields above set row caps and review behaviour; these environment variables set the engine-level connection and execution ceilings (defaults shown):

  • Connection pools (all JDBC engines): ACCESSFLOW_PROXY_CONNECTION_TIMEOUT (30s), ACCESSFLOW_PROXY_IDLE_TIMEOUT (10m), ACCESSFLOW_PROXY_MAX_LIFETIME (30m), ACCESSFLOW_PROXY_LEAK_DETECTION_THRESHOLD (0s = off).
  • Statement execution (all engines): ACCESSFLOW_PROXY_EXECUTION_MAX_ROWS (10000), ACCESSFLOW_PROXY_EXECUTION_STATEMENT_TIMEOUT (30s), ACCESSFLOW_PROXY_EXECUTION_DEFAULT_FETCH_SIZE (1000).
  • MongoDB: ACCESSFLOW_PROXY_MONGO_CONNECT_TIMEOUT (PT10S), …_SERVER_SELECTION_TIMEOUT (PT10S), …_MAX_POOL_SIZE (10).
  • Couchbase: ACCESSFLOW_PROXY_ENGINES_COUCHBASE_CONNECT_TIMEOUT (PT10S), …_WAIT_UNTIL_READY_TIMEOUT (PT10S), …_SCAN_CONSISTENCY (request-plus).
  • Redis: ACCESSFLOW_PROXY_ENGINES_REDIS_CONNECT_TIMEOUT (PT5S), …_SOCKET_TIMEOUT (PT5S), …_MAX_POOL_SIZE (10).
  • Cassandra / ScyllaDB: ACCESSFLOW_PROXY_ENGINES_CASSANDRA_CONNECT_TIMEOUT / …_SCYLLADB_CONNECT_TIMEOUT (PT10S) and the matching …_REQUEST_TIMEOUT (PT10S).
  • Elasticsearch / OpenSearch: ACCESSFLOW_PROXY_ENGINES_ELASTICSEARCH_CONNECT_TIMEOUT / …_OPENSEARCH_CONNECT_TIMEOUT (PT10S) and …_SOCKET_TIMEOUT (PT30S).
  • DynamoDB: ACCESSFLOW_PROXY_ENGINES_DYNAMODB_CONNECT_TIMEOUT (PT10S), …_API_CALL_TIMEOUT (PT30S).
  • Neo4j: ACCESSFLOW_PROXY_ENGINES_NEO4J_CONNECT_TIMEOUT (PT10S), …_MAX_CONNECTION_POOL_SIZE (100).

API connectors

What it is. API Access Governance lets you govern outbound API calls — REST, SOAP, GraphQL, and gRPC — with the same review, approval, and audit machinery as a database query. An API connector (/api-connectors) is a registered API target: a base URL, a protocol, and an authentication method.

Configure it (admin). Create a connector with a base URL, protocol, and an auth method — None, API key, Bearer token, Basic, OAuth2 client-credentials, Custom header, or mTLS. Secrets are AES-256-GCM encrypted at rest and never returned. For OAuth2 client-credentials, AccessFlow fetches, caches, and refreshes the upstream access token itself — configure the token endpoint, client ID/secret, scopes, audience, grant type (client-credentials, refresh-token, or resource-owner password), and client-auth method (Basic header or POST body); the token is reused across calls and refreshed on expiry or a 401, so no token needs to be pasted by hand. Define default headers sent on every governed call (users see them but cannot change them) and, optionally, rename the trace-context header keys AccessFlow uses to propagate W3C traceparent. Optionally upload a schema (OpenAPI / WSDL / GraphQL SDL / gRPC .proto) by pasting it, uploading a file, or pointing at a URL to fetch; AccessFlow parses it into a normalized operation catalog with read/write classification. Set the review plan, AI-analysis toggle + AI config, text-to-API toggle, require-review-on-reads/writes, and the max response size (default 10 MiB — the full response is stored and downloadable up to this cap, with only a bounded preview shown inline). A Test connection button probes reachability (and, for OAuth2 connectors, exercises the token fetch).

Share with the team. Grant per-user access on a connector (can read / can write / can break-glass, an optional expiry, an allowed-operations subset, and response fields to mask). Users see only the connectors they are granted.

Mask & classify responses. On the connector's Masking and Classification tabs (admin), define connector-level masking policies that redact response fields before the snapshot is stored. Because API responses aren't columnar, a policy targets a field four ways — a schema field (operation + field from the parsed catalog), a JSON path, an XML path (XPath), or a regex — each with a masking strategy (full, partial, hash, email, format-preserving) and role / group / user reveal scoping (a requester in a reveal list sees the unmasked value; no implicit admin bypass). Data-classification tags (PII, PCI, PHI, GDPR, FINANCIAL, SENSITIVE) on a field auto-derive a masking policy and raise the AI risk score for calls to the operation, with a derivation preview of the suggested handling. Masking is applied once, before storage, so raw values never persist, and the policy ids that applied are recorded in the execution audit.

Use it. In the API editor (/api-editor) a user picks a connector, searches the operation catalog (or writes a free-form method + path), and composes the call like Postman — query parameters, custom headers (over the connector's read-only default headers), and a body that can be raw, x-www-form-urlencoded, multipart form-data, or a binary file upload. A user can schedule the call for later, sees a debounced AI risk preview, and submits. Plain-English text-to-API drafts a call for schema-backed connectors. Every call flows through AI risk scoring → routing → human review (no self-approval) → guarded execution that injects the connector's auth and a W3C traceparent, caps and field-masks the response, and stores an immutable response snapshot. The full stored response can be downloaded in its original format, and the request list is filterable by submitter, trace id, and span id. Break-glass and scheduled execution mirror the query path. Note: gRPC connectors register and review today; gRPC call execution is a follow-up — REST/SOAP/GraphQL execute fully.

Connectors

What it is. The catalog of databases AccessFlow can talk to, and the one-click way to enable them. Use it to turn on support for an engine before you add a datasource for it — so the image doesn't have to ship every database driver. The Admin → Connectors marketplace (/admin/connectors) groups the SQL (relational) family separately from the NoSQL family.

Configure it. Click Install on a connector card; AccessFlow downloads its driver, verifies it against a pinned checksum, and caches it. PostgreSQL ships Installed; everything else — MongoDB, Couchbase, Redis, Cassandra, ScyllaDB, Elasticsearch, OpenSearch, Amazon DynamoDB, Neo4j, and the other SQL drivers — shows an Install action.

Built-in connectors — SQL:

  • PostgreSQL — bundled (ships in the image, no download).
  • MySQLcom.mysql:mysql-connector-j · 1-click install.
  • MariaDBorg.mariadb.jdbc:mariadb-java-client · 1-click install.
  • Oracle Databasecom.oracle.database.jdbc:ojdbc11 · 1-click install.
  • Microsoft SQL Servercom.microsoft.sqlserver:mssql-jdbc · 1-click install.
  • ClickHousecom.clickhouse:clickhouse-jdbc · 1-click install (a CUSTOM-dialect connector, the first engine beyond the built-in five).

Built-in connectors — NoSQL:

  • MongoDB — native engine plugin (not JDBC) · 1-click install; the engine downloads on demand like a JDBC driver and is cached in the same driver-cache directory (pre-seed it for air-gapped installs). Users write queries in the mongo shell form (db.users.find({ … })) or a JSON command document, chosen in the editor; results show in both a JSON document view and a flattened table. AI risk analysis, human approval, row-level security ($match injection), and field masking all apply. Configure it with the standard host/port/database/username/password/SSL fields.
  • Couchbase — native engine plugin (not JDBC) · 1-click install; same on-demand download/verify/cache model as MongoDB. Users write SQL++ (N1QL) statements with SQL-style highlighting and formatting in the editor; results show in both a table and a JSON document view. AI risk analysis, human approval, row-level security (predicates ANDed into the WHERE clause with parameter binding; unrewritable shapes rejected), and field masking all apply; CURL(), JavaScript UDFs, and system:* keyspaces are rejected up front. The database field holds the bucket; plain connections bootstrap on port 11210 (couchbase://), TLS on 11207 (couchbases://) — pick the matching port or use a connection-string override.
  • Redis — native key-value engine plugin (not JDBC) · 1-click install; same on-demand download/verify/cache model as MongoDB. Users submit redis-cli commands (GET user:42, HGETALL session:abc, SCAN 0 MATCH orders:* COUNT 100, SET, DEL) classified onto the same approval workflow; server-side scripting and blast-radius commands (EVAL, CONFIG, FLUSHALL, SHUTDOWN) are rejected at submission. Field masking applies to returned hash fields / values; row-security policies on a Redis datasource fail closed (row predicates have no key-value meaning). The database field holds the numeric DB index (default 0); plain connections use port 6379 (redis://), TLS uses rediss://.
  • Apache Cassandra — native wide-column engine plugin (not JDBC) · 1-click install; same on-demand download/verify/cache model as MongoDB. Users write CQL statements classified onto the same approval workflow (SELECT / INSERT / UPDATE / DELETE plus CREATE/ALTER/DROP of table/keyspace/index/type/materialized-view and TRUNCATE); server-side code (BEGIN … BATCH, CREATE/DROP FUNCTION/AGGREGATE) is rejected up front. Row-level security is key-aware and fails closed — predicates splice into the WHERE clause only on partition/clustering key columns with CQL-filterable operators (=, IN, <, <=, >, >=); a non-key column, !=/NOT IN, or INSERT into a policied table is rejected rather than injecting ALLOW FILTERING. Field masking applies to returned columns. The database field holds the keyspace; the per-datasource local datacenter field is required (the driver's load-balancing datacenter). Default port 9042.
  • ScyllaDB — native wide-column engine plugin (not JDBC) · 1-click install; CQL-compatible and served by the same engine plugin as Apache Cassandra. Identical governance: CQL classification, key-aware fail-closed row-level security, field masking, and rejected server-side code. The database field holds the keyspace; the per-datasource local datacenter field is required. Default port 9042.
  • Elasticsearch — native search engine plugin (not JDBC) · 1-click install; same on-demand download/verify/cache model as MongoDB. Users write a JSON query envelope ({"search":"logs-*","query":{…}}, plus count, get/mget, index/bulk, update_by_query/delete_by_query, and index management) classified onto the same approval workflow; server-side scripting (script, runtime_mappings, Painless) and cluster/system-index APIs are rejected up front. Row-level security injects bool.filter clauses on keyword fields (fail-closed on writes into a policied index), and field masking applies recursively to _source fields including nested dot-paths. Authenticate with basic auth (username + password) or an API key — pick the method in the connection wizard. The database field is optional (it only scopes introspection — the index is named in the query). Default port 9200, SSL REQUIRE.
  • OpenSearch — native search engine plugin (not JDBC) · 1-click install; wire-compatible and served by the same engine plugin as Elasticsearch. Identical governance: JSON query envelope classification, bool.filter row-level security on keyword fields, nested field masking, rejected scripting/cluster APIs, and basic-or-API-key auth. The database field is optional. Default port 9200.
  • Amazon DynamoDB — native key-value engine plugin (not JDBC) · 1-click install; same on-demand download/verify/cache model as MongoDB (AWS SDK for Java v2 over the url-connection HTTP client — no Netty). Users write PartiQL (SELECT / INSERT / UPDATE / DELETE), and table management arrives as a JSON command document ({"CreateTable": {…}}, DeleteTable, UpdateTable); transaction/batch statements are rejected. Row-level security splices predicates into the PartiQL WHERE clause with parameter binding on any attribute (DynamoDB filters via Scan), failing closed on INSERT-into-policied and deny-all; field masking applies recursively by dot-path including nested maps/lists. Its connection is cloud credentials + region, not host/port: the database field holds the AWS region, the username/password hold the access key id / secret access key, and an optional custom endpoint targets DynamoDB Local / VPC. Default port 8000 (DynamoDB Local; AWS uses the SDK regional endpoint).
  • Neo4j — native graph engine plugin (not JDBC) · 1-click install; same on-demand download/verify/cache model as MongoDB (native Neo4j Java driver over the Bolt protocol). Users write Cypher; the query type is the strongest write clause present (DELETE/REMOVE → DELETE, CREATE/MERGE → INSERT, SET → UPDATE, else a MATCH … RETURN / SHOW read → SELECT), with index / constraint / database / role schema commands as DDL. LOAD CSV, procedure calls outside a read-only allow-list, and multi-statement input are rejected. Row-level security ANDs property predicates onto each MATCH's WHERE with parameter binding (node-label policies), failing closed on anonymous or write-creates-policied-label shapes; field masking is label-aware and recursive. Connection is host/port + database (the Neo4j database, required) with the SSL mode encoded in the Bolt scheme, or a full bolt:// / neo4j+s:// URI in the optional Bolt connection URI field (Neo4j Aura / clustered routing). Default port 7687 (Bolt).

Adding a new database to the catalog is a data change, not a code change — see the connectors design doc if you want to contribute one.

  1. Install. Click Install on a connector card. The driver is fetched from the configured Maven repository (ACCESSFLOW_DRIVERS_REPOSITORY_URL), SHA-256-verified, and cached to ACCESSFLOW_DRIVER_CACHE.
  2. Use it. Installed connectors appear in the datasource create wizard. The five core engines are first-class database types; additional engines (e.g. ClickHouse) are created as a Custom datasource backed by the connector — you fill in host / port / database and AccessFlow builds the JDBC URL from the connector's template.
  3. Air-gapped. Pre-seed the driver cache and set ACCESSFLOW_DRIVERS_OFFLINE=true; connectors whose JAR isn't cached then report Unavailable.

Custom JDBC drivers

What it is. The escape hatch for any JDBC database not in the connector catalog. Upload its driver JAR and datasources can bind to it by choosing Database type → Custom in the create wizard.

Configure it. Manage uploads at /admin/drivers:

Custom JDBC drivers admin page with an Upload driver call-to-action and an empty-state explaining that uploaded drivers extend support beyond the bundled five.
/admin/drivers — manages the per-org JDBC driver registry.
  1. Upload driver. Click Upload driver, attach the JAR, and supply the vendor name, target database type, the driver class identifier shown in the vendor's documentation, and the expected SHA-256 checksum.
  2. Verification. The checksum is re-verified every time a pool initializes; uploaded drivers are loaded in isolated runtimes to keep providers from interfering with each other.
  3. On-disk cache. JARs cache to ACCESSFLOW_DRIVER_CACHE (default ~/.accessflow/drivers). Mount it as a persistent volume in Kubernetes so pods don't re-download on restart, and set ACCESSFLOW_DRIVERS_OFFLINE=true for air-gapped installs.

Review plans

What it is. The approval policy attached to a datasource — it decides how a submitted query gets from "submitted" to "executed". Use it to require AI scoring, one or more human sign-offs, or both, and to auto-approve low-risk reads. Each plan is a sequence of stages; each stage names approvers (by role or specific user) and a minimum number of approvals before the query advances.

Configure it. Build plans at /admin/review-plans — start from a built-in template or from scratch:

Review plans admin page with the create-from-template dropdown open, listing four templates — Strict (writes need 2 approvals), Lenient (reads auto-approved), AI-only (no human approval), and Standard (AI plus one reviewer).
/admin/review-plans → caret next to Add review plan exposes four built-in templates that prefill the create modal.
Add review plan modal with name and description fields, AI-review and human-approval toggles enabled, minimum approvals set to 1, approval timeout 24 hours, and an approver row defaulting to the REVIEWER role at stage 1.
/admin/review-plansAdd review plan. Stack approver rows to build multi-stage chains.
  1. Open /admin/review-plans and click Add review plan, or pick a built-in template from the dropdown caret next to it (Strict — writes need 2 approvals, Lenient — reads auto-approved, AI-only — no human approval, Standard — AI + 1 reviewer) to prefill the modal with sensible defaults.
  2. Name and describe the policy — e.g. "Production writes — two reviewers".
  3. Pick the gates. Toggle Require AI review to score every query before it queues for humans; toggle Require human approval to demand at least one reviewer sign-off. Auto-approve LOW-risk reads lets SELECTs skip humans entirely when AI risk is below the threshold.
  4. Set thresholds. Minimum approvals is the number of distinct reviewers needed before the query advances; Approval timeout (hours) is when AccessFlow auto-rejects an idle PENDING_REVIEW query (it scans on a cadence set by ACCESSFLOW_WORKFLOW_TIMEOUT_POLL_INTERVAL, default PT5M).
  5. Build the approver chain. Click Add approver for each stage; each row names a role (or a specific user) and a stage number. Stages advance sequentially, and a single REJECTED decision at any stage terminates the query.

Reviewer decisions. Reviewers can Approve, Reject, or Request changes. Reject and Request changes both require a non-empty comment — the server enforces this (HTTP 400 VALIDATION_ERROR), and the UI disables the confirm button until the textarea is populated. The comment is persisted on the decision row, rendered on the rejected stage of the timeline on /queries/<id>, and surfaced to the submitter as a "Changes requested" alert whenever the latest decision is REQUESTED_CHANGES and the query is still PENDING_REVIEW. Approve still treats the comment as optional.

Query status transitions. A query moves through these states:

PENDING_AI → PENDING_REVIEW → APPROVED → EXECUTED
                           ↘ REJECTED   (manual reviewer rejection)
                           ↘ TIMED_OUT  (approval-timeout auto-reject)
PENDING_REVIEW → CANCELLED (submitter only)
APPROVED → FAILED (execution error)

A single REJECTED decision at any stage terminates the query. If a query sits in PENDING_REVIEW past the plan's approval timeout, AccessFlow auto-rejects it (it scans on a cadence set by ACCESSFLOW_WORKFLOW_TIMEOUT_POLL_INTERVAL, default PT5M). Auto-approve reads lets SELECTs skip human approval entirely; Require AI review still scores the read but won't block on a human.

Routing policies

What it is. Policy-as-code that decides a query's path automatically, after AI analysis and before reviewers see it. Use it to auto-approve routine reads, hard-block dangerous patterns, or escalate sensitive ones — instead of sending everything through the same review plan. Policies run in ascending priority and the first enabled one whose condition matches wins; anything unmatched falls through to the datasource's review plan exactly as before.

Configure it. Manage them at /admin/routing-policies (the Routing policies entry in the Security nav group):

  1. Open /admin/routing-policies (the Routing policies entry in the Security nav group, next to Review plans) and click Add policy.
  2. Name the policy and optionally scope it to one datasource — leave the datasource blank for an org-wide rule. Set its priority (unique per organisation; lower runs first) and the enabled toggle.
  3. Build the condition with the guided builder: pick match ALL (AND) or match ANY (OR), then add leaf conditions — each can be negated (NOT). Operands include query type, referenced tables (glob, e.g. payroll.*), AI risk level, AI risk score (with a comparison operator), requester role, requester group, time-of-day window, day-of-week, presence of a WHERE clause, presence of a LIMIT clause, and the transactional (BEGIN…COMMIT) flag.
  4. Choose the action. Auto-approve (skip human review), Auto-reject (block the query), Require approvals (force human review with an absolute minimum number of approvers), or Escalate (force human review, adding a delta on top of the review plan's minimum). The approver count applies only to the last two actions.
  5. Reorder policies any time with the per-row up/down controls — the order is the evaluation order.
How it routes. Time-of-day and day-of-week conditions are evaluated in the server's local timezone (overnight windows wrap around midnight). On datasources with AI analysis disabled, risk-based conditions never match (there's no AI signal); routing does not run when AI analysis fails — the query goes to a human instead. Every automated decision is recorded in the audit log (QUERY_APPROVED / QUERY_REJECTED with source: "ROUTING_POLICY"), and the query detail page shows which policy matched. Routing policies are managed via the ADMIN-only /api/v1/admin/routing-policies CRUD and /reorder endpoints.
The /admin/routing-policies page listing ordered routing policies by priority — name, scope, condition summary, action (auto-approve / escalate), and per-row reorder controls.
/admin/routing-policies — ordered, attribute-based auto-decision rules; first match by priority wins, unmatched falls through to the review plan.

AI configurations

What it is. The AI brain behind query risk analysis and natural-language "text-to-query". Configure it per organization and point it at the provider that fits your data-egress policy; AccessFlow then scores every submitted query and, where enabled, drafts queries from plain-language prompts. It ships adapters for five providers — pick one:

  • Anthropic — default model claude-sonnet-4-20250514.
  • OpenAI — default model gpt-4o.
  • Ollama — self-hosted, set Endpoint to the Ollama server URL.
  • Custom (OpenAI-compatible) — any OpenAI API–compatible backend (vLLM, LM Studio, Together, Groq, OpenRouter, …). Set Endpoint to the server's base URL (required); the API key is optional for keyless self-hosted servers.
  • Hugging Face — default model meta-llama/Llama-3.3-70B-Instruct. Endpoint defaults to the hosted Inference Providers router (https://router.huggingface.co/v1, authenticated with a HF token) and can point at a local / self-hosted Text Generation Inference (TGI) server or a Dedicated Inference Endpoint. Keyless-capable, so local TGI runs without a token.
New AI configuration wizard, Connection step, with the model pre-filled as claude-sonnet-4-20250514, an empty API key field, and timeout / max prompt tokens / max completion tokens at their defaults.
/admin/ai-configs/new — three-step wizard: Provider → Connection → Test.

Configure it. Create a configuration with the three-step wizard at /admin/ai-configs:

  1. Provider. Open /admin/ai-configs, click New configuration, and pick a provider tile (OpenAI / Anthropic / Ollama / Custom OpenAI-compatible / Hugging Face). The wizard pre-fills the default model for the provider you chose.
  2. Connection. Name the configuration, optionally override the model, and paste the API key (left empty for Ollama and custom keyless servers; for Ollama, Custom OpenAI-compatible, and Hugging Face providers supply an Endpoint URL instead — required for the custom provider, pre-filled with the router URL for Hugging Face and editable to a local TGI / Dedicated Endpoint). Tune Timeout, Max prompt tokens, and Max completion tokens if your provider has stricter limits.
  3. Test. The wizard sends a synthetic SQL snippet to the provider and shows the score, risk level, and any issues so you can confirm the credentials and model are working before you save.

Provider switches take effect immediately — no restart is required. Once saved, link the configuration to a datasource (DatasourcesConfiguration step) or to a review plan, and AccessFlow scores every matching submission against it.

Editable system prompt. Each configuration has an optional System prompt field (on both the create wizard's Connection step and the edit page). Leave it blank to use AccessFlow's built-in analyzer prompt, or paste your own to add house rules, change tone, or steer the analysis. A custom prompt must contain the {{sql}} placeholder (the query under review is substituted there); the optional {{schema_context}}, {{db_type}}, and {{language}} placeholders are substituted too. Click Load / reset to default to pull the built-in template into the editor as a starting point — saving it blank again reverts to the default. Prompt changes take effect immediately, just like provider switches.

Multi-model orchestration & voting. On the configuration's edit page, enable Multi-model orchestration to run several models in parallel against the same query. The primary model above votes alongside the Additional models you add (each with its own provider, model, optional endpoint / API key, and weight). Pick a Voting strategyWeighted average (default), Highest risk, or Majority vote — to combine their risk verdicts; issues and optimization suggestions from all models are merged. A common setup pairs a fast, cheap local model with a deeper cloud model. Per-model token cost and latency are recorded for every analysis and charted on the AI analyses dashboard.

Guardrails. Add one or more Guardrail patterns (case-insensitive regular expressions) to block queries whose text matches a pattern before any model is called — useful for prompt-injection strings or content you never want sent to a provider. A blocked editor preview returns an error inline; a blocked submitted query is recorded as a failed (critical) analysis. Each pattern is validated as a regex when you save.

Langfuse integration. AccessFlow optionally connects to Langfuse for LLM observability and prompt management, configured per organization at /admin/langfuse (enable the integration, set the host — defaults to https://cloud.langfuse.com — plus the public/secret key, which is stored encrypted and never shown again). Two independent toggles: Send analysis traces emits a trace of every AI analysis (input SQL, structured output, model, token usage, latency) to Langfuse; Use Langfuse-managed prompts lets each AI configuration point at a Langfuse prompt by name + label (the Langfuse prompt name / Langfuse prompt label fields on the AI config pages) so you can iterate on prompts in Langfuse without redeploying. Both are best-effort and non-blocking — a Langfuse outage never affects query workflow. Use the Test connection button to verify your credentials.

The /admin/langfuse configuration page with the Connection section (Enabled toggle, Host URL, public and secret keys) and the Features section (Send analysis traces, Use Langfuse-managed prompts), plus Save and Test connection buttons.
/admin/langfuse — per-org tracing + managed prompts; keys are stored encrypted, Test connection verifies them.

RAG knowledge base. Each AI configuration can carry a retrieval-augmented-generation knowledge base. Toggle Enable RAG on the create wizard's Connection step or the edit page, pick a Vector storeIn-app (pgvector), which stores vectors in AccessFlow's own PostgreSQL, or Qdrant (supply the endpoint, collection, and optional API key) — and configure a dedicated Embedding provider + model (OpenAI, Ollama, OpenAI-compatible, or Hugging Face; Anthropic has no embeddings API). Then add Knowledge documents (data-governance policies, naming conventions, schema notes) on the edit page; each is chunked, embedded, and stored. At analysis and text-to-query time the most relevant chunks are retrieved (tune Top-K and Similarity threshold) and injected into the prompt, so the AI follows your house rules. Use Test RAG connection to verify the embedding model and vector store are reachable. Retrieval is best-effort — a store outage never blocks analysis.

The AI configuration wizard Connection step with Enable RAG toggled on, revealing the vector store select (In-app pgvector), Top-K and similarity-threshold inputs, and the embedding provider and model fields.
/admin/ai-configs/newEnable RAG. Pick a vector store + embedding model; add knowledge documents on the edit page.
pgvector requirement. The in-app store needs the PostgreSQL vector extension. The bundled Docker Compose / Helm deployments provision it automatically (a superuser init step); for external / managed PostgreSQL, install the vector extension on the AccessFlow database before starting the backend, and set ACCESSFLOW_RAG_PGVECTOR_DIMENSIONS (default 1536) to match your embedding model's output dimension before the first migration. If the extension is missing, AccessFlow still starts — the in-app store is disabled (a banner appears on the RAG settings) and external Qdrant remains available. Set ACCESSFLOW_RAG_PGVECTOR_ENABLED=false to opt out explicitly, or ACCESSFLOW_RAG_PGVECTOR_AUTO_PROVISION=false to disable the best-effort CREATE EXTENSION attempt at startup.
What the AI returns. A 0–100 risk score, a risk level (LOW / MEDIUM / HIGH / CRITICAL), and a list of issues categorised as anti-patterns, missing indexes, restricted-column access, etc. The score is informational — only human reviewers can finalize approval unless the plan opts in to auto-approve reads.

Tune it. Langfuse observability: ACCESSFLOW_LANGFUSE_DEFAULT_HOST (https://cloud.langfuse.com), ACCESSFLOW_LANGFUSE_PROMPT_CACHE_TTL (PT60S), ACCESSFLOW_LANGFUSE_CONNECT_TIMEOUT (PT5S), ACCESSFLOW_LANGFUSE_REQUEST_TIMEOUT (PT10S). RAG: ACCESSFLOW_RAG_PGVECTOR_ENABLED (true), ACCESSFLOW_RAG_PGVECTOR_AUTO_PROVISION (true), ACCESSFLOW_RAG_PGVECTOR_DIMENSIONS (1536), ACCESSFLOW_RAG_CHUNK_SIZE (800), ACCESSFLOW_RAG_MAX_DOCUMENT_CHARS (100000). Per-org guardrails: ACCESSFLOW_AI_RATE_LIMIT_REQUESTS_PER_MINUTE (30; <= 0 disables) and ACCESSFLOW_AI_RATE_LIMIT_TOKENS_PER_MONTH (0 = unlimited).

AI analyses dashboard

What it is. An org-wide trend view of every AI analysis. Use it to spot risk-pattern drift, see which queries the AI consistently flags, compare each model's cost and latency, and decide whether a stricter review plan or schema-level guardrails are warranted. /admin/ai-analyses charts the average risk score over a configurable window, the most frequent issue categories, the most active submitters, and — per model — token cost and average latency.

Configure it. Nothing to set up — the dashboard fills in automatically once a datasource has AI analysis enabled.

AI analyses dashboard with a configurable date range and datasource filter, a Risk score over time line chart, and a Top issue categories panel.
/admin/ai-analyses — risk trends, hottest issue categories, and top submitters across every analyzed query.

Datasource health

What it is. An operational dashboard for spotting a datasource in trouble — pool exhaustion, a database that has turned slow, or a sudden volume spike. Use it as the first place to look when queries start failing or hanging.

Configure it. Nothing to set up. /admin/datasource-health (admin-only, read-only) auto-refreshes every 30 seconds; each card shows live connection-pool utilisation (active / idle / free against the configured maximum) plus a trailing 24-hour summary of query volume, p50 / p95 execution latency, and error count. Pool gauges read "pool not initialised" until the datasource's first query runs (pools are created on demand).

Tune it. ACCESSFLOW_PROXY_HEALTH_CACHE_TTL caches each snapshot so the auto-refresh stays cheap (default PT30S).

Datasource health dashboard at /admin/datasource-health showing per-datasource cards with connection-pool status and a 24-hour summary of query volume, p50/p95 execution latency, and error count, plus a Refresh button.
/admin/datasource-health — one card per datasource, auto-refreshing every 30 s.

Notification channels

What it is. Where AccessFlow sends review notifications — when a query is submitted, approved, rejected, times out, or scores CRITICAL on AI risk. Wire up the channels your team already lives in so reviewers act fast. Configure as many as you like per organization (e.g. one Slack channel per team).

Configure it. Manage channels at /admin/notifications:

Add notification channel modal with name, channel type selected as EMAIL, active toggle on, and conditional SMTP fields revealed — host, port, user, password, TLS toggle, from address, and from name.
/admin/notificationsAdd channel. The form swaps its lower half between Email, Slack, Webhook, Discord, Telegram, Microsoft Teams, and PagerDuty fields when you change Type.
  1. Open /admin/notifications and click Add channel.
  2. Pick a type:
    • Email — supply SMTP host, port, optional user / password, TLS, and the From address / display name. Leave the SMTP fields blank to fall back on the system SMTP.
    • Slack — paste an incoming-webhook URL for one-way Block Kit messages (header, SQL preview, and a View in AccessFlow link). For interactive Approve / Reject buttons, configure a Slack app instead (see below).
    • Webhook — supply a target URL and a signing secret. Every POST carries X-AccessFlow-Signature: sha256=... computed from the body and the secret.
    • Discord — paste a channel webhook URL (Server Settings → Integrations → Webhooks). Optional username and avatar URL override the bot identity per-message. Messages render as a rich embed with the SQL preview in a fenced code block.
    • Telegram — create a bot via @BotFather, add it to the target group/channel, and configure the bot token plus numeric chat ID. Messages use MarkdownV2 formatting.
    • Microsoft Teams — paste the channel's Incoming Webhook (or Power Automate webhook) URL. Messages render as an Adaptive Card with a View in AccessFlow action button.
    • PagerDuty — paste an Events API v2 integration routing key, pick a default severity (critical / error / warning / info), and choose which triggers page the channel: AI critical-risk query and/or review timeout. Other events never fire it. AccessFlow sends a trigger event with a query-stable dedup key so repeat events fold into one incident.
  3. Send a test event. After saving, each channel card has a Test button — the dispatcher fires a synthetic event and reports delivery success.
  4. Edit or remove. Each card also has Edit and Delete buttons. Deleting a channel is immediate (hard delete) — there is no soft-delete, but webhook retries already scheduled before the delete still complete.

Sensitive config fields (SMTP password, webhook secret, Telegram bot token, PagerDuty routing key) are AES-256-GCM encrypted at rest and are never shown in plaintext after the channel is saved.

Webhook retry policy. One immediate attempt plus three scheduled retries at +30s, +2 min, +10 min. Tune the delays via ACCESSFLOW_NOTIFICATIONS_RETRY_FIRST / _SECOND / _THIRD. Failed deliveries log ERROR but never affect query workflow state.

Tune it. ACCESSFLOW_PUBLIC_BASE_URL sets the link embedded in messages (default http://localhost:5173); retry delays are ACCESSFLOW_NOTIFICATIONS_RETRY_FIRST / _SECOND / _THIRD (PT30S / PT2M / PT10M). For air-gapped installs that route through an internal proxy, override ACCESSFLOW_NOTIFICATIONS_TELEGRAM_API_BASE_URL (https://api.telegram.org/) and ACCESSFLOW_NOTIFICATIONS_PAGERDUTY_API_BASE_URL (https://events.pagerduty.com/).

Slack app — Approve / Reject from Slack

Beyond the one-way Slack webhook channel, an admin can connect a Slack app at /admin/slack so reviewers act on requests without leaving Slack. Create the app in Slack's console, then paste its App ID, Bot token, Signing secret, and a default channel ID. When an app is active, review-request messages are posted via the bot token and carry Approve and Reject buttons.

  1. Each reviewer links their Slack identity once. On /profileSlack account, click Generate link code, then run /accessflow link <code> in Slack. The code is single-use and short-lived (ACCESSFLOW_NOTIFICATIONS_SLACK_LINK_CODE_TTL, default 10 min).
  2. Clicking Approve / Reject runs through the same review service as the UI — a reviewer can never approve their own query, and role / stage checks apply identically. The original Slack message updates in place with the decision.

Inbound clicks are verified by the Slack signing secret (HMAC over the raw body, with a timestamp/replay window set by ACCESSFLOW_NOTIFICATIONS_SLACK_SIGNATURE_TOLERANCE, default 5 min). The bot token and signing secret are AES-256-GCM encrypted at rest and never returned by the API. Point your Slack app's Interactivity request URL at /api/v1/integrations/slack/actions and its slash command at /api/v1/integrations/slack/commands.

Slack app configuration at /admin/slack with fields for App ID, default channel ID, masked bot token and signing secret, an Active toggle, and Save / Send test message buttons.
/admin/slack — connect the interactive Slack app (bot token + signing secret).

System SMTP

What it is. The fallback mail server for transactional email — password resets, user invitations, and any email notification channel that doesn't carry its own SMTP settings. Set it once per org so those emails can go out.

Configure it. On /admin/notifications, the first card is System SMTP:

System SMTP configuration modal with host, port (587), optional username, password, STARTTLS toggle, from-address, and optional display-name fields.
/admin/notificationsSystem SMTP card → Configure.
  1. Open /admin/notifications. The first card is System SMTP — click Configure (or Edit if it's already set).
  2. Fill the form. Host, port (587 for STARTTLS, 465 for implicit TLS), optional username and password, STARTTLS toggle, and the From address — required and validated as an email. The password field shows "Leave blank to keep the existing password" when editing, so secrets aren't echoed back.
  3. Send a test email. After saving, the card exposes a sink-address input + Send test button so you can verify deliverability without firing a real workflow event.

The password is AES-256-GCM encrypted at rest. Delete the row from the same card if you'd rather rely solely on per-channel email overrides.

OAuth 2.0 / OIDC

What it is. Single sign-on through an external identity provider, so people log in with accounts they already have instead of an AccessFlow password. Google, GitHub, Microsoft, and GitLab are built in; two more tabs cover self-hosted GitHub Enterprise and GitLab (self-managed) (you provide the instance base URL, e.g. https://github.acme.corp, and AccessFlow appends the well-known sub-paths); and a generic OpenID Connect tab integrates any other OIDC provider (Keycloak, Auth0, Okta, Authentik, Zitadel). It all lives in the database, so adding a provider needs no restart.

Configure it. Manage providers at /admin/oauth2:

OAuth providers admin page with the Google tab selected. An info callout shows the redirect URI to register at the provider with a copy button, followed by Client ID and Client secret fields.
/admin/oauth2 — one tab per built-in provider (Google / GitHub / Microsoft / GitLab), two self-hosted tabs (GitHub Enterprise / GitLab self-managed), plus a generic OpenID Connect tab. Each tab carries its own redirect URI and provider-specific guidance.
  1. Register an OAuth app at the provider using the redirect URI shown in the AccessFlow tab (copy it directly from the info callout). The format is {ACCESSFLOW_PUBLIC_BASE_URL}/api/v1/auth/oauth2/callback/{provider}.
  2. Open /admin/oauth2, pick the provider tab, and paste the Client ID and Client secret. Microsoft additionally needs a Tenant ID; other providers expose optional Scopes override for custom claims. For GitHub Enterprise and GitLab (self-managed), also enter the Server base URL of your self-hosted instance (origin only — no path, no query; https:// only). AccessFlow appends /login/oauth/authorize, /login/oauth/access_token, and /api/v3/* for GitHub Enterprise, and /oauth/authorize, /oauth/token, /oauth/userinfo, /oauth/discovery/keys for self-managed GitLab. For the OpenID Connect tab, also enter the IdP's Display name (rendered on the login button as "Continue with …"), Authorization endpoint URL, Token endpoint URL, UserInfo endpoint URL, JWK set URL, and Issuer URL — most IdPs publish all five at /.well-known/openid-configuration. Optional attribute-claim fields (User-name claim, Email claim, Email-verified claim, Display-name claim) default to the standard OIDC names (sub, email, email_verified, name) and only need to be set if your IdP uses non-standard claims.
  3. (Optional) Restrict who may sign in. Use Allowed organizations and Allowed email domains on the same tab:
    • Google — populate Allowed email domains with your Workspace domain(s); the organization list is ignored.
    • GitHub — list the org logins users must belong to (e.g. bablsoft) in Allowed organizations. AccessFlow calls GET /user/orgs, so the Scopes override must include read:org — saving an active config without it returns a 422 with a clear message.
    • Microsoft — list AAD group object IDs in Allowed organizations. The Entra app registration must be configured (Token configuration → groups) to emit the groups claim.
    • GitLab — list full group paths from the OIDC groups claim (e.g. acme/team).
    • GitHub Enterprise — same as GitHub but the orgs call hits {base_url}/api/v3/user/orgs; read:org scope is still required.
    • GitLab (self-managed) — same as GitLab (OIDC groups claim) but resolved against your self-hosted instance.
    • OpenID Connect — set the Groups claim field to the claim name your IdP uses (often groups or roles), then list the group identifiers users must belong to in Allowed organizations. Leave the claim field blank to disable group enforcement and rely on Allowed email domains instead.
    Empty lists mean no restriction. Users who fail the check are redirected to the login page with OAUTH2_ORG_NOT_ALLOWED or OAUTH2_EMAIL_DOMAIN_NOT_ALLOWED.
  4. Pick a default role for first-time sign-ins (defaults to READONLY) and flip Active on to enable the provider's button on the login page.
  5. Set ACCESSFLOW_OAUTH2_FRONTEND_CALLBACK_URL if the frontend lives at a different origin than the backend's CORS origin (default {CORS_ALLOWED_ORIGIN}/auth/oauth/callback).

Email verification is required before an OAuth account is linked, and account-linking is conservative — a provider-supplied email will not silently take over an existing local user. Implementation details: docs/07-security.md → Authentication.

Tune it. ACCESSFLOW_OAUTH2_FRONTEND_CALLBACK_URL (where the provider round-trip lands, default ${CORS_ALLOWED_ORIGIN}/auth/oauth/callback) and ACCESSFLOW_OAUTH2_EXCHANGE_CODE_TTL (one-time exchange-code lifetime, default PT1M).

SAML 2.0 SSO

What it is. Single sign-on via a SAML 2.0 identity provider (Okta, Azure AD, OneLogin, …) — the enterprise-SSO alternative to OAuth / OIDC. Configure one record per organization; both SP-initiated and IdP-initiated logins work.

Configure it. Fill the single form at /admin/saml:

SAML / SSO admin page with three sections — Identity provider (IdP metadata URL, IdP entity ID, signing certificate PEM textarea), Service provider, and Attributes — each with empty form fields ready to fill.
/admin/saml — a single config form covering IdP metadata, SP entity, and attribute mapping.
  1. Open /admin/saml.
  2. Identity provider. Paste the IdP metadata URL (preferred — AccessFlow refreshes it periodically), or fall back to IdP entity ID + Signing certificate (PEM) when the IdP doesn't publish metadata over HTTP.
  3. Service provider. Set SP entity ID, ACS URL, and SLO URL — register these with your IdP.
  4. Attributes. Map the IdP assertion attributes that carry email, display name, and role. Pick a default role for users whose assertion doesn't include one, then flip Active on to enable the SAML button on the login page.

SAML users rely on the IdP's MFA rather than AccessFlow's TOTP. Full SP metadata, signing-cert rotation, and assertion validation rules live in docs/07-security.md.

Tune it. ACCESSFLOW_SAML_FRONTEND_CALLBACK_URL (default ${CORS_ALLOWED_ORIGIN}/auth/saml/callback) and ACCESSFLOW_SAML_EXCHANGE_CODE_TTL (default PT1M). To pin the service-provider signing keypair instead of the auto-generated one, set ACCESSFLOW_SAML_SP_SIGNING_KEY_PEM and ACCESSFLOW_SAML_SP_SIGNING_CERT_PEM.

Audit log

What it is. A complete, tamper-evident record of everything that happens — logins, query submissions and decisions, datasource changes, channel edits. It's your answer to "who did what, when" for security reviews and compliance. Records are append-only and cryptographically chained, so a deleted or altered entry is detectable after the fact (query result data is never stored).

Configure it. Nothing to switch on — it captures automatically. Review it at /admin/audit-log:

Audit log admin page with action and resource filters, an actor / resource ID search, an Export CSV button, a Verify chain button, and a chronological list of recent USER_LOGIN events with actor avatars and IP addresses.
/admin/audit-log — filter, paginate, verify the HMAC chain, and export to CSV.
  1. Filter and search. Narrow by action, resource type, actor user id, or resource id; an optional start/end date pair scopes the window.
  2. Verify chain. The Verify chain button re-walks every row's HMAC link in order and surfaces the first mismatch — useful as a recurring auditor check.
  3. Export CSV. Streams the current filter as RFC 4180 CSV with the same columns shown in the UI. Long-running exports respect the same query budget as the table view (use date filters to keep them bounded).

Tune it. The chain-signing key defaults to a per-deployment value derived from ENCRYPTION_KEY; set AUDIT_HMAC_KEY (hex, ≥ 32 bytes) explicitly when you want to manage or rotate it yourself. Inserts run through a dedicated AUDIT_DB_USER / AUDIT_DB_PASSWORD role that has no UPDATE / DELETE rights on the log.

Compliance reports & signed exports

What it is. Ready-made compliance reporting with audit-grade exports. Two pre-built reports answer common auditor questions over a chosen period: classified-data access (which executed queries touched PII / PCI / PHI / GDPR / FINANCIAL / SENSITIVE data, joined to your data-classification tags) and a regulatory audit trail of DDL / DELETE operations with the approvers' names. Use it to hand a regulator or internal auditor evidence they can verify themselves.

Configure it. Build and export reports from the compliance dashboard at /admin/auditor — open to the read-only AUDITOR role and to admins. Each report exports as a digitally signed PDF or CSV that an auditor can verify offline against the public key at /api/v1/admin/compliance/signing-certificate; every export is itself recorded in the audit log with its content hash, so it's tamper-evident end to end.

Tune it. ACCESSFLOW_COMPLIANCE_MAX_REPORT_PERIOD (largest window, default P366D) and ACCESSFLOW_COMPLIANCE_MAX_ROWS (row cap before a report is marked truncated, default 50000). Signing reuses JWT_PRIVATE_KEY — no extra secret required.

The read-only AccessFlow Auditor dashboard with the compliance report builder for classified-data-access and regulatory audit-trail reports over a chosen period.
/admin/auditor — the read-only Auditor role builds and signs compliance reports over the immutable query snapshots.

Access recertification campaigns

What it is. Recurring attestation campaigns that make someone periodically re-confirm who still needs standing datasource access — the review control SOC 2 and ISO 27001 auditors ask for. An admin schedules an org- or datasource-scoped campaign; when it opens it snapshots the current standing grants into one item per grant and notifies the eligible reviewers (multi-channel, plus an attestation.campaign_opened WebSocket event). Reviewers work a certify / revoke worklist that reuses the review-queue patterns (self-review blocked, bulk-certify); a revoke routes through the normal permission-revoke path, so access is actually removed.

Configure it. Manage campaigns from /admin/attestation and certify items from the reviewer worklist at /reviews/attestations. Two clustered-safe jobs run the lifecycle: one opens SCHEDULED campaigns at their scheduled_open_at, the other closes OPEN campaigns at their due_at and applies each campaign's pending-default (KEEP or REVOKE) to anything a reviewer never got to. A completed campaign exports as a CSV evidence file (who reviewed what, decisions, timestamps), and every transition — ATTESTATION_CAMPAIGN_OPENED/CLOSED, ATTESTATION_ITEM_CERTIFIED/REVOKED — lands in the tamper-evident audit log.

Tune it. ACCESSFLOW_ATTESTATION_OPEN_POLL_INTERVAL and ACCESSFLOW_ATTESTATION_CLOSE_POLL_INTERVAL (open / close scan cadence, both default PT5M) and ACCESSFLOW_ATTESTATION_MAX_EVIDENCE_ROWS (row cap before an evidence CSV is marked truncated, default 50000).

The AccessFlow access-recertification campaign list at /admin/attestation, showing scheduled and open attestation campaigns with their due dates and pending-item counts.
/admin/attestation — admins schedule recurring access-recertification campaigns; reviewers certify or revoke each snapshotted grant.

Behavioural anomaly detection (UBA)

What it is. Spots when a user's database activity drifts from their own normal pattern — a spike in query volume, access at odd hours, reaching for tables they've never touched, unusual query types or row counts, or a jump in errors — and flags it for an admin. Use it to catch compromised accounts, insider misuse, or credential sharing that static rules would miss. It learns each user's baseline from audit-log metadata only — never query result data.

Configure it. Nothing to switch on — it runs automatically once a user has built up enough history (until then they're left alone). Flagged anomalies appear on /admin/anomalies and fire a notification across every active channel (including PagerDuty), and a flagged user's next query is escalated to stricter review. ADMIN and the read-only AUDITOR role can review anomalies; only ADMIN can acknowledge (triaged) or dismiss (false positive) one. Each anomaly can also carry an optional AI natural-language explanation.

The AccessFlow Behavioural anomaly detection dashboard listing flagged out-of-pattern user activity with z-score detail, ready to acknowledge or dismiss.
/admin/anomalies — UBA flags out-of-pattern activity from audit-log baselines; admins acknowledge or dismiss each one.

Tune it. Sensitivity and cadence via the ACCESSFLOW_AI_ANOMALY_* environment variables: ACCESSFLOW_AI_ANOMALY_DETECTION_POLL_INTERVAL (how often it runs, default PT15M), …_LOOKBACK_WINDOW (aggregation window, PT1H), …_Z_SCORE_THRESHOLD (how far from normal counts as anomalous, 3.0), …_IQR_MULTIPLIER (1.5), …_MIN_SAMPLE_SIZE (history required before a user is scored, 7), …_MAX_BASELINE_SAMPLES (90), …_OFF_HOURS_THRESHOLD (0.02), and …_SUMMARY_ENABLED (AI explanations on/off, true).

AI rate limit & cost budget. Two per-organization guardrails protect the provider API key from a runaway editor or compromised account, enforced before every AI analysis call (editor preview, text-to-SQL, and the async analysis on query submit). ACCESSFLOW_AI_RATE_LIMIT_REQUESTS_PER_MINUTE (default 30; <= 0 disables) caps requests per minute via a Redis counter, and ACCESSFLOW_AI_RATE_LIMIT_TOKENS_PER_MONTH (default 0 = unlimited / opt-in) caps the summed prompt + completion tokens of the org's analyses in the current calendar month. When a limit is hit, the synchronous editor paths return HTTP 429 and the async path records a CRITICAL "AI budget exhausted" / "AI rate limit exceeded" analysis row so human review still proceeds.

Personalized dashboard & weekly digest

The default post-login home (/dashboard) is self-scoped — every user sees only their own data, no admin role required: pending approvals as a reviewer, their recent queries with status/risk trend sparklines, an AI optimization-suggestion backlog they can dismiss or open in the editor, and their own behavioural-anomaly alerts. Widgets are customizable (show/hide, collapse, drag-and-drop reorder) and persist per browser. Users can export the week's summary as a digitally signed PDF/CSV on demand, and opt in to a weekly email digest delivered to their email and any configured chat channels. The digest job is clustered-safe; tune it with ACCESSFLOW_DASHBOARD_WEEKLY_DIGEST_POLL_INTERVAL (how often the job wakes, default P1D) and ACCESSFLOW_DASHBOARD_WEEKLY_DIGEST_PERIOD (minimum gap between digests per user, default P7D).

Data lifecycle & right-to-erasure

Admin. Define retention/erasure rules at /admin/lifecycle/policies — per datasource, target a table / column set / classification tag with a retention window (ISO-8601, e.g. P30D or P7Y) plus arbitrary conditions (a structured, parameter-bound predicate builder and a JSqlParser-validated raw-WHERE escape hatch — SQL datasources only) and an action: hard-delete, soft-delete, or pseudonymize (salted SHA-256 / format-preserving / tokenization), with an optional cron schedule. A dry-run preview reports impact without executing. The scan job stages eligible work (honouring the cron); tune it with ACCESSFLOW_LIFECYCLE_POLICY_SCAN_INTERVAL (default PT1H). Staged runs now execute automatically through the proxy — ACCESSFLOW_LIFECYCLE_POLICY_EXECUTION_INTERVAL (default PT5M).

Any user can file a right-to-erasure request at /lifecycle/erasure using the same rich configuration (subject identifier and/or target table + conditions). It flows through AI-assisted scope detection and review-plan-based peer review: any eligible REVIEWER or admin reviews it at /lifecycle/erasure-reviews (per the datasource review plan, multi-stage; the submitter can never approve their own), and stale reviews auto-reject via ACCESSFLOW_LIFECYCLE_REVIEW_TIMEOUT (default PT168H). Approved requests are executed through the proxy — soft-deleted rows vanish from reads, DELETEs become marker updates, aged PII resolves to an irreversible salted hash at read time — with tamper-evident proof-of-deletion audit records and a retention-adherence compliance export. Tune the executor with ACCESSFLOW_LIFECYCLE_ERASURE_EXECUTION_INTERVAL (default PT1M).

End-user workflows

The pages above configure the system. This section is the operator's view of how analysts and reviewers actually use it day-to-day — the editor, scheduled queries, and the bulk-approval review queue that landed in v1.1.

Submitting a query

/editor hosts the SQL editor: dialect-aware syntax highlighting, schema autocomplete fed by /datasources/<id>/schema, a debounced AI analyze pass, and an in-pane review-plan summary. The submitter picks a datasource, writes SQL, fills in a justification, and submits — at which point the workflow takes over.

SQL editor at /editor with a schema browser on the left, CodeMirror SQL pane with a SELECT example, a Justification textarea, a Scheduled execution row, and an AI analysis side panel.
/editor — schema browser, SQL pane, justification, scheduled-execution row, and AI analysis side panel.

Drafting queries from natural language

When a datasource has text-to-query enabled and an AI configuration bound (see DatasourcesConfiguration), the editor shows a Describe your query bar above the query pane. Type a plain-language request (e.g. "order numbers for the last 5 days"), click Generate query, and the draft is written into the editor for you to review and edit — in the datasource engine's native query language (SQL for relational engines, plus MongoDB shell/JSON, Cypher, CQL, the Elasticsearch Query DSL, redis-cli, SQL++, and PartiQL for the NoSQL engines; the editor mounts the matching syntax automatically). The generated query is never executed directly — it still flows through the full AI analysis and review pipeline like any hand-written submission.

The /editor page with the text-to-query bar above the query pane: a Describe your query textarea with a natural-language prompt and a Generate query button, and the generated SELECT statement rendered in the CodeMirror editor below.
/editorDescribe your query. AI drafts the query; the draft still goes through the normal review pipeline.

Query templates library

The editor's toolbar has a Templates drawer button and a Save as template shortcut. Saved templates are scoped to your organisation and have two visibilities: Private (only you can see and load them) or Team (every user in the org can read; only the owner can edit or delete). Templates can optionally be pinned to a datasource so they surface with a "pinned to current datasource" badge when that datasource is selected. Template bodies can include :placeholder tokens — when you load a template, AccessFlow prompts you for each placeholder value and substitutes them client-side. Submission still flows through the standard AI analysis and review pipeline; templates are a pure save / load surface, never a bypass.

Every save records an immutable version. Open a template's History tab to see how it evolved — pick any two revisions for a side-by-side Git-style diff, and restore a prior version when an edit went wrong. Restoring creates a new version rather than discarding history, so the full trail is always preserved.

SQL editor with the Query templates drawer open, listing saved templates with Private and Team visibility badges, tags, owner, a search box, All/Mine/Team filter tabs, and Open / Delete actions per row.
/editorTemplates — load a saved query (Private or Team), with :placeholder prompts on open.

Tracking submitted queries

/queries (labelled Query history in the sidebar) lists every query the current user has submitted or has read access to. Filter by status, risk, datasource, or date range, and export the visible rows as CSV. Click a row to open /queries/<id> for the full timeline, AI analysis, decisions, and (when executed) the result preview.

Query history list at /queries with status, type, risk, datasource, and date filters, an Export CSV button, and rows showing a mix of PENDING REVIEW and APPROVED queries.
/queries — submitter view of every query, with Export CSV.

Scheduling a query

Submitters can pick a Scheduled execution datetime instead of running immediately. The query still goes through review; once approved, it stays in APPROVED until scheduled_for arrives, at which point ScheduledQueryRunJob (cadence ACCESSFLOW_WORKFLOW_SCHEDULED_RUN_POLL_INTERVAL, default PT1M) triggers execution. The submitter can cancel an APPROVED scheduled run any time before it fires.

SQL editor with the scheduled execution date picker open showing a month-view calendar; the help text reads 'If approved, the query runs automatically at that time.'
/editorScheduled execution row → date picker. Leave blank for immediate execution.

Request chaining & grouping

A single real task often spans several steps across systems — a schema change on PostgreSQL, a follow-up update on MongoDB, then a REST call downstream. The group builder at /request-groups/new bundles those steps into one grouped request that is reviewed and approved as a single element, then executed as an ordered sequence. Add steps — each a query against a datasource or an API call against a governed connector — drag-reorder them, and watch a per-step AI risk preview plus an aggregate risk badge. You can only add a member targeting a datasource / connector you're permitted to use; a break-glass group requires can_break_glass on every member target.

The group is reviewed as one element: the required approvers are the union across all member plans, and the group reaches APPROVED only when every member plan's per-stage approval requirement is satisfied — no member's policy is weakened by bundling, and you can never approve your own group. Reviewers see it as one expandable element in their queue. On execute, members run in sequence_order; on the first failure (with continue-on-error off) the run stops, the remaining members are SKIPPED, and the group becomes PARTIALLY_EXECUTED (or FAILED if the first member fails). With continue-on-error on, all members run and the group is EXECUTED with mixed per-member outcomes.

There is no distributed rollback. An approved group is not atomic — already-applied members stay (you cannot roll back a committed Postgres DDL because a later Mongo write failed). The detail page at /request-groups/:id shows ordered step-by-step progress (running / done / failed / skipped) live over the WebSocket, and each member records its own snapshot + audit row alongside the group-level audit.

Operator env vars. A scheduled group runs at its scheduled_for time via ScheduledGroupRunJob (cadence ACCESSFLOW_REQUESTGROUPS_RUN_POLL_INTERVAL, default PT1M); a group left awaiting review past the review timeout is auto-rejected to TIMED_OUT by GroupTimeoutJob (cadence ACCESSFLOW_REQUESTGROUPS_TIMEOUT_POLL_INTERVAL, default PT5M). Both are clustered-safe (ShedLock).

Reviewing & bulk approval

Reviewers land on /reviews for the queue assigned to them. Each row shows the query id, classification, AI risk, datasource, submitter, and elapsed time. Three tabs scope the view: Assigned to you, All pending (visible to anyone the plan grants approver eligibility), and Recently decided.

Review queue at /reviews showing pending queries with checkbox column, ID, type, risk, datasource, submitter, and created columns, and tabs for Assigned to you, All pending, and Recently decided.
/reviews — reviewer queue, no rows selected.

Selecting one or more rows surfaces a bulk-action bar with Approve selected, Reject selected, and Request changes — the same decisions available per row, but applied to every selected query in one round-trip. Reject and Request changes still require a comment for every query in the batch (the server enforces this; the UI prompts for each one inline before submitting).

Review queue at /reviews with four rows selected and a bulk-action bar showing Request changes, Reject selected, Approve selected, and Clear selection buttons.
/reviews — bulk-action bar appears the moment a row is checked.

Reviewers can never approve their own queries; the backend enforces this regardless of plan configuration, and self-submissions are filtered out of the queue before the page renders.

Mobile approvals & one-tap push

What it is. Approve or reject from your phone. Reviewers get a push notification the moment a query needs them and can act on it in one tap — without keeping a browser tab open. Install AccessFlow to your home screen and the review queue even works offline.

Configure it. A reviewer taps Enable push approvals on /reviews and grants the browser notification permission. From then on each review request arrives as a push notification with Approve and Reject actions; tapping one opens a focused page showing the query and asks for a quick re-verification — your password, or a TOTP code when 2FA is enrolled — before the decision commits. A single tap never approves a query, and the "can't approve your own query" guard is enforced server-side on every channel.

Tune it. Push works out of the box — a signing keypair is generated and stored on first use (encrypted with ENCRYPTION_KEY). To pin your own, set ACCESSFLOW_PUSH_VAPID_PUBLIC_KEY / ACCESSFLOW_PUSH_VAPID_PRIVATE_KEY (raw base64url, e.g. from web-push generate-vapid-keys) and a ACCESSFLOW_PUSH_VAPID_SUBJECT contact URL. The re-verification token lifetime is ACCESSFLOW_SECURITY_STEP_UP_TTL (default PT5M).

Comparing repeated runs

When the same submitter re-runs the same query against the same datasource, AccessFlow links the new execution to its previous one. On the query detail page a diff panel shows the change in rows_affected, returned row count, and execution time versus the prior run — so reviewers and submitters can spot when a query that used to touch a handful of rows suddenly affects thousands, or when latency drifts. Matching uses a canonical form of the SQL (comments stripped, whitespace collapsed, keywords upper-cased), so cosmetic reformatting still pairs the runs. The first execution of a query simply shows no prior run to compare against.

Seeing why a query failed

When an approved query fails at execution, the detail page no longer shows only a red Failed badge. An execution-result card surfaces the verbatim database error — for example ERROR: invalid input value for enum query_status: "PENDING" — alongside how long the attempt ran, and the same cause is echoed in the approval timeline. The submitter, reviewers, and admins who can read the query see the real reason, so a failed run can be debugged without digging through server logs.

Infrastructure as Code (Terraform / OpenTofu & CI Actions)

Beyond the env-driven GitOps bootstrap, AccessFlow ships an official Terraform / OpenTofu provider (bablsoft/accessflow) and reusable GitHub Actions + a GitLab CI template for managing governance resources declaratively over the REST API. Both authenticate with an API key — the provider manages datasources, review plans, routing / row-security / masking policies, AI configs, and notification channels with the same authoritative-upsert semantics as the bootstrap reconciler.

Service-account API keys

A pipeline needs credentials without an interactive login. Bootstrap can seed a service account — an API-key-only user (password login disabled) whose raw key you supply from a Secret (only its hash is stored, rotated in place when it changes). Set these operator env vars (or the equivalent Helm bootstrap.serviceAccounts[] with an apiKeySecretRef):

  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_EMAIL
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_DISPLAY_NAME
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_ROLE — default ADMIN
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_API_KEY_NAME
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_API_KEY — the raw af_-prefixed token
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_API_KEY_EXPIRES_AT — optional ISO-8601

You can also mint a key interactively at POST /api/v1/me/api-keys.

Terraform / OpenTofu provider

main.tf
terraform {
  required_providers {
    accessflow = { source = "bablsoft/accessflow" }
  }
}

provider "accessflow" {
  endpoint = "https://accessflow.example.com" # or ACCESSFLOW_ENDPOINT
  api_key  = var.accessflow_api_key            # or ACCESSFLOW_API_KEY
}

resource "accessflow_datasource" "prod" {
  name     = "prod-postgres"
  db_type  = "POSTGRESQL"
  host     = "postgres.prod.internal"
  port     = 5432
  ssl_mode = "REQUIRE"
}

Works with both tofu and terraform, and is published at registry.terraform.io/providers/bablsoft/accessflow (and the OpenTofu registry as bablsoft/accessflow). Write-only secrets (password, api_key, notification config values) are never returned by the API, so the provider applies changes to them but can't detect drift — treat the HCL as the source of truth.

CI Actions

The provision-datasource and run-query GitHub composite actions (referenced as bablsoft/accessflow/.github/actions/<name>@v1) and the include-able GitLab template wrap provisioning a datasource and submitting a governed query from a pipeline. run-query waits for a terminal status and sends an X-AccessFlow-CI header so context-aware routing policies recognise the CI origin.

Full provider reference, all resources, and the registry-publishing runbook (the provider is released to a dedicated terraform-provider-accessflow repo that opentofu.org and registry.terraform.io ingest) are in docs/16-iac.md.

Further reading

This page covers what an operator needs to run and configure AccessFlow. For deeper internals — module boundaries, the proxy engine, the audit log's HMAC chain, the full REST + WebSocket spec — see the engineering chapters in the docs/ folder on GitHub: