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: 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 SQL proxy. Your team connects to AccessFlow instead of directly to PostgreSQL, MySQL, MariaDB, Oracle, or Microsoft SQL Server (or any other JDBC-compatible engine, via an admin-uploaded driver). 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. Backend Java 25 + Spring Boot 4, frontend React 19 + Vite 8 + Node 24. Postgres 18 for internal state, Redis 8 for token revocation and scheduler locks. See the Install section on the homepage for the canonical pinned versions.

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
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).

Kubernetes & Helm

The Helm chart ships at charts/accessflow/ and is published to https://bablsoft.github.io/accessflow. Pre-create the required Secrets (the chart will refuse to start if they're missing or empty):

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

# 2. create namespace + Secrets
kubectl create namespace accessflow

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

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

kubectl -n accessflow create secret generic accessflow-pg-secret \
  --from-literal=DB_PASSWORD="$(openssl rand -base64 24)"

# 3. install
helm install accessflow accessflow/accessflow -n accessflow -f values.yaml

The chart bundles Bitnami subcharts for Postgres and Redis (toggle off with postgresql.enabled=false / redis.enabled=false to point at external instances). Full values reference and the Secret schema: docs/09-deployment.md → Kubernetes & Helm and charts/accessflow/README.md.

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.

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, 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 → 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.

Configuration

Everything below is admin-only. Each entity can be managed through the admin pages of the SPA or through the REST API directly (full payload reference in docs/04-api-spec.md). All API calls require an Authorization: Bearer <JWT> header.

Users

Local users are created directly by an admin. Federated users (SAML / OAuth) are auto-provisioned on first sign-in if the feature is enabled (see SAML and OAuth).

Create a user directlyPOST /api/v1/admin/users:

json
{
  "email":        "alice@company.com",
  "password":     "InitialPassword123!",
  "display_name": "Alice Reviewer",
  "role":         "REVIEWER"
}

Invite a user by emailPOST /api/v1/admin/users/invitations generates a one-time signup token and emails it. The token expires after ACCESSFLOW_SECURITY_INVITATION_TTL (default 7 days).

Other endpoints: GET /api/v1/admin/users (paginated list), PUT /api/v1/admin/users/{id} (change role or active flag), DELETE /api/v1/admin/users/{id} (deactivate).

User roles & RBAC

AccessFlow has four roles. Pick the lowest-privilege role for each user — admins are the only role that can change configuration. A user can never approve their own query, regardless of role.

Capability READONLY ANALYST REVIEWER ADMIN
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
Manage datasources
Manage users
Manage review plans
View audit log
Manage notification channels
Configure AI
Configure SAML / OAuth

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.

Datasource-level permissions. Role is the org-wide ceiling. On top of it, every user needs an explicit row in datasource_user_permissions to access a given datasource — that row controls can_read / can_write / can_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.

Datasources

A datasource describes one customer database that AccessFlow proxies. PostgreSQL, MySQL, MariaDB, Oracle, and MS SQL Server are bundled; any other JDBC engine works by uploading its driver JAR and setting db_type=CUSTOM.

Create a datasourcePOST /api/v1/datasources:

json
{
  "name":                  "Production PostgreSQL",
  "db_type":               "POSTGRESQL",
  "host":                  "db.company.com",
  "port":                  5432,
  "database_name":         "app_prod",
  "username":              "accessflow_svc",
  "password":              "encrypted at rest",
  "ssl_mode":              "REQUIRE",
  "require_review_reads":  false,
  "require_review_writes": true,
  "review_plan_id":        "uuid of a review plan",
  "ai_analysis_enabled":   true,
  "ai_config_id":          "uuid of an ai_config"
}

password is AES-256-GCM encrypted on write and is never returned in any response. The plaintext is decrypted once during HikariCP pool init and never stored on the heap beyond that.

Grant a user accessPOST /api/v1/datasources/{id}/permissions with { user_id, can_read, can_write, can_ddl, allowed_schemas, allowed_tables, restricted_columns, expires_at }. Without a permission row, a user cannot see or query the datasource at all.

Custom JDBC drivers. Upload a driver JAR with POST /api/v1/datasources/drivers (multipart: vendor_name, target_db_type, driver_class, expected_sha256). The SHA-256 is re-verified on every pool init and each driver runs in an isolated classloader. The on-disk cache path is ACCESSFLOW_DRIVER_CACHE (default ~/.accessflow/drivers) — mount this as a persistent volume in Kubernetes.

Review plans

A review plan is a per-datasource policy that decides how a submitted query reaches execution. Each plan describes a sequence of stages; each stage names approvers (by role or explicit user) and a minimum number of approvals before the query advances.

Create a planPOST /api/v1/admin/review-plans:

json
{
  "name":                    "Production writes — two reviewers",
  "description":             "DML on prod needs senior + DBA sign-off.",
  "requires_ai_review":      true,
  "requires_human_approval": true,
  "min_approvals_required":  2,
  "approval_timeout_hours":  24,
  "auto_approve_reads":      true,
  "approvers": [
    { "role": "REVIEWER", "stage": 1 },
    { "role": "ADMIN",    "stage": 2 }
  ]
}

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_hours, QueryTimeoutJob auto-rejects it. The job's cadence is configurable via ACCESSFLOW_WORKFLOW_TIMEOUT_POLL_INTERVAL (default PT5M). auto_approve_reads=true lets SELECTs skip human approval entirely; requires_ai_review=true still scores the read but won't block on a human.

AI configurations

AI analysis is per-organization. AccessFlow ships adapters for three providers — pick one that fits your data-egress policy:

  • Anthropic — default model claude-sonnet-4-20250514.
  • OpenAI (or any OpenAI-compatible endpoint) — default model gpt-4o.
  • Ollama — self-hosted, set endpoint to the Ollama server URL.

Set the AI configPUT /api/v1/admin/ai-configs:

json
{
  "provider": "ANTHROPIC",
  "model":    "claude-sonnet-4-20250514",
  "api_key":  "sk-ant-..."
}

For Ollama, add "endpoint": "http://ollama:11434" (and leave api_key empty). Provider switches take effect immediately — no restart is required. Verify the connection with POST /api/v1/admin/ai-configs/{id}/test before relying on it in a review plan.

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.

Notification channels

AccessFlow fires notifications when a query is submitted, approved, rejected, times out, or scores CRITICAL on AI risk. Channels are configured per organization; multiple channels of the same type can coexist (e.g. one Slack channel per team).

Three channel typesPOST /api/v1/admin/notification-channels:

  • Emailconfig carries smtp_host, smtp_port, smtp_user, smtp_password, smtp_tls, from_address, from_name. Falls back to the system SMTP when omitted.
  • Slackconfig carries an incoming-webhook webhook_url, optional channel override, optional mention_users. Messages use Slack Block Kit so the SQL preview and action buttons render natively.
  • Webhookconfig carries url, secret (used to compute X-AccessFlow-Signature: sha256=...), timeout_seconds, retry_attempts.

Sensitive config fields (smtp_password, webhook_secret) are AES-256-GCM encrypted at rest and are never returned in GET responses. Verify a channel with POST /api/v1/admin/notification-channels/{id}/test — the dispatcher sends a synthetic event and reports delivery success.

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.

System SMTP

System SMTP is the fallback for transactional emails — password resets, invitations, and any notification email channel that doesn't carry its own SMTP overrides. Configure once per org:

json — PUT /api/v1/admin/system-smtp
{
  "host":         "smtp.company.com",
  "port":         587,
  "username":     "smtp-user",
  "password":     "encrypted at rest",
  "tls":          true,
  "from_address": "accessflow@company.com"
}

Verify with POST /api/v1/admin/system-smtp/test — the server tries a real send to a sink address you supply. Remove with DELETE /api/v1/admin/system-smtp if you'd rather rely solely on per-channel email config.

OAuth 2.0 / OIDC

Sign-in with Google, GitHub, Microsoft, and GitLab is built-in — operators only need to register an OAuth app with the provider and store the client id / secret in AccessFlow. Other OIDC providers work the same way; the configuration lives in the database (oauth2_config rows), not in application.yml, so adding a new provider does not require a restart.

  1. Register an OAuth app at the provider; set the redirect URI to {ACCESSFLOW_PUBLIC_BASE_URL}/api/v1/auth/oauth/{provider}/callback.
  2. From the admin UI, paste the client_id + client_secret and pick the built-in template (Google / GitHub / Microsoft / GitLab) or fill in the OIDC discovery URL for a custom provider.
  3. Set ACCESSFLOW_OAUTH2_FRONTEND_CALLBACK_URL if the frontend lives at a different origin than the backend's CORS origin (default is {CORS_ALLOWED_ORIGIN}/auth/oauth/callback).
  4. Optional: enable auto-provisioning so first-time sign-ins create a user with a default role.

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.

SAML 2.0 SSO

For SAML-based SSO (Okta, Azure AD, OneLogin, etc.) configure one record per org via PUT /api/v1/admin/saml-config:

json
{
  "sp_entity_id":     "https://accessflow.company.com/saml",
  "idp_metadata_url": "https://idp.company.com/saml/metadata",
  "keystore":         "base64-encoded JKS or P12",
  "keystore_password":"encrypted at rest"
}

Both SP-initiated and IdP-initiated flows are supported. Attribute mapping (email, display name, role) and auto-provisioning behaviour are configurable; 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.

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: