Documentation

Datasources.

Last updated

Datasources

There is a guide for this. Add your first datasource walks the wizard end to end and then narrows the datasource down with an allow-list, masking and row security. This chapter is the reference behind it.

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, Neo4j, Snowflake, BigQuery, and Databricks install from the connector catalog; any other JDBC engine works by uploading its driver and choosing Custom.

What is a datasource in AccessFlow?

A datasource is a governed connection to one of your databases. Users never receive its credentials — they submit queries to AccessFlow, which reviews them and then executes them over the pooled connection on their behalf. Masking, row-level security, schema allow-lists, and row caps are all configured per datasource.

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 is pre-filled from the engine's own default rather than one global value — PostgreSQL starts at VERIFY_FULL, several NoSQL engines start at DISABLE, and the rest at REQUIRE. Check it rather than assuming it, and prefer 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. For Snowflake the wizard asks for the account host (<account>.snowflakecomputing.com; the port field is hidden — always 443), the database, the user, a credential that is either a password or a PKCS#8 private key (PEM) for key-pair authentication, an optional private key passphrase (only for a passphrase-protected key, which is what Snowflake's own openssl instructions produce), and an optional JDBC URL override — a full jdbc:snowflake:// URL carrying warehouse / role / schema parameters. For Google BigQuery the connection is cloud credentials: the wizard hides host/port/username and asks for the GCP project (optionally project.dataset to pin a default dataset) and the service-account key JSON, plus an optional custom endpoint (BigQuery emulator). For Databricks SQL the wizard asks for the workspace host, the required warehouse HTTP path (/sql/1.0/warehouses/<id> from the warehouse's connection details), an optional Unity Catalog catalog, and a personal access token. 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. When an external secrets manager is enabled (see Run & deploy), any credential field also accepts a secret reference — vault:<mount>/<path>#<field>, aws:<name-or-arn>[#jsonField], or azure:<secret-name> — stored as-is and resolved through the store at connection time; the form shows the syntax hints for whichever providers are enabled.
  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. An optional Environment (Development, Test, Staging or Production) picks which SQL review ruleset applies to queries on this datasource — leave it unset to use the organization default.

Read replicas & load balancing (optional). On the datasource settings page, the Read replicas card takes any number of replica endpoints (JDBC URL plus optional username and password per endpoint — blank credentials reuse the primary's). AccessFlow opens one connection pool per endpoint and load-balances every query classified as SELECT round-robin across the healthy replicas; INSERT / UPDATE / DELETE / DDL and transactional BEGIN … COMMIT batches always hit the primary. Replicas must use the same database engine as the primary (they reuse the primary's JDBC driver), and credentials are AES-256-GCM encrypted with the same ENCRYPTION_KEY. Per-node health checks (a background prober plus a circuit breaker) take a failed endpoint out of rotation for a cooldown (ACCESSFLOW_PROXY_REPLICA_COOLDOWN, default 30s) and its health shows on the Datasource health dashboard; only when every replica is down does the read fall back to the primary, with one DATASOURCE_REPLICA_FALLBACK audit row visible at /admin/audit-log. Click Test replica on any row to validate its URL + credentials live without persisting; leaving the password blank reuses that endpoint's saved password. Remove every endpoint to disable replica routing. Replica pools reuse the same ACCESSFLOW_PROXY_* connection-pool tuning as the primary; the health checks are tuned by the ACCESSFLOW_PROXY_REPLICA_* variables.

SELECT result caching (optional). The settings page's Performance card opts a datasource into a Redis-backed result cache for repeated identical SELECTs, with a per-datasource TTL (1–86,400 seconds; blank uses ACCESSFLOW_PROXY_CACHE_DEFAULT_TTL, default 60s). Caching is security-safe by construction — entries are keyed over the row-security-rewritten query and the caller's masking scope, so masking and row-level security always apply — and any write executed through AccessFlow to a referenced table (including GDPR erasure and retention deletes) immediately invalidates the affected entries. Note that writes made outside AccessFlow are invisible to the cache and are served stale until the TTL expires, so pick a TTL that matches how the datasource is written. ACCESSFLOW_PROXY_CACHE_ENABLED=false switches the feature off deployment-wide.

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, restricted columns (masked as *** in SELECT results), denied schemas and tables, denied columns, and denied query shapes. 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.

Denied schemas and tables — everything except. Sometimes it is easier to say what a user may not touch. Allow the schema crm and deny the table crm.salary, and the user can query every table in crm — including tables created later — except crm.salary. A query that touches a denied table is refused before it runs:

  • A denial always wins. It is checked after the allowed schemas and tables, and it works on its own too, with no allowed list at all.
  • Name tables with their schema. An entry written as just salary denies a table called salary in every schema. crm.salary denies that table, and also a query that writes plain salary, because AccessFlow cannot tell which schema the database would pick.
  • Denying a schema. Every table in a denied schema is refused. While any schema is denied, the user must write table names with their schema (crm.customer, not customer); an unqualified name is refused for the same reason as above.
  • Hidden, not just refused. Denied tables and schemas disappear from the schema tree, autocomplete, the table preview, AI query drafting and the AI agent tools.
  • Several grants add up. If a user holds their own grant and group grants, a table denied by any one of them stays denied — a wider group grant cannot undo it, and a group's denial applies to every member. Denied columns work the same way (below).
  • Who it does not bind. Administrators with query-admin rights skip per-datasource permission checks.
  • Just-in-time access keeps denials. A just-in-time access request cannot add a denied schema or table, and approving one never removes a denial: denials from every grant add up, and when the approval replaces the user's own expiring grant, that grant's denials carry over to the new one.
  • How to write entries. A denied schema is a single name, such as hr — not analytics.hr. A denied table is table, schema.table, or schema.* for a whole schema. Other wildcards and empty parts are refused when you save the grant.
  • Tricky names are refused, not guessed. SQL Server's db..salary (default schema) is treated as matching any schema, an Oracle database link (hr.salary@remote) does not get around a denial, and a name pattern such as the Elasticsearch index pattern sal* is refused whenever the grant denies anything.
  • Works on every datasource type. Denied schemas and tables apply to relational, NoSQL and warehouse datasources alike. On datasources whose objects have no schema — MongoDB collections, DynamoDB tables, Redis keys — a denied schema refuses every query, so use denied tables there. A denial can only catch the tables AccessFlow sees in the query: MongoDB $lookup, $unionWith and $graphLookup stages, a Neo4j MATCH (n) with no label, and a Redis KEYS pattern are not caught. The allowed lists share this limit.
  • Removing a grant can widen access. A denial belongs to the grant that carries it. Revoke that grant, let it expire, or revoke it in an access review, and its denial goes with it — another grant the user still holds may then let them reach the table. Check the user's other grants first.

Denied columns — block instead of mask. A restricted column can still be queried; only its value is hidden. For a column that must never be read at all, list it under Denied columns as table.column or schema.table.column. A query that uses it is refused before it runs:

  • What counts as using it. Selecting it, filtering, joining, grouping or sorting on it, or reading its whole table through SELECT *, TABLE t or a whole-row value such as row_to_json(t). Spell out the columns you need instead of *. The table preview on the Schema tab reads every column, so it is refused on a table with a denied column.
  • Joins. A column written without its table in a query that joins several tables is refused if any of those tables denies a column of that name. Prefix it with the table to avoid this.
  • Deny beats mask. A query that uses a column that is both restricted and denied is refused.
  • Who it does not bind. Administrators (any role with query-admin rights) skip per-datasource permission checks, so a denied column does not stop them. If a user holds several grants on the datasource — their own and their groups' — a column denied by any one of them stays denied. A wider grant that denies nothing cannot undo it, and a group's denied columns apply to every member. A temporary just-in-time grant that replaces a user's own expiring grant keeps that grant's denied columns.
  • Supported datasources. PostgreSQL, MySQL, MariaDB, Oracle, SQL Server and custom JDBC. The field is not offered for NoSQL or cloud data-warehouse datasources.

Denied query shapes — limit how a query is written. Some grants should allow a table but not every way of querying it — for example, simple lookups but no joins or totals. Pick the query shapes to refuse under Denied query shapes: joins, set operations (UNION, INTERSECT, EXCEPT), subqueries, WITH clauses, GROUP BY, HAVING, aggregate functions and window functions. Denying all of them leaves plain single-table queries such as SELECT … FROM … WHERE … ORDER BY; shapes don't change whether the grant can write, so keep can write off for a read-only grant. A query that uses a denied shape is refused before it runs:

  • Anywhere in the query. A join inside a subquery or a WITH clause counts, and in a BEGIN; …; COMMIT; batch every statement is checked.
  • Aggregate functions. The built-in ones — COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG, the statistics functions and similar. A custom aggregate function defined in your database, or a rarely used built-in, may not be recognised, so pair this setting with database permissions where it must hold.
  • When in doubt, refuse. If AccessFlow cannot work out a query's shape, it treats the query as using every shape the grant denies.
  • Who it does not bind. Administrators with query-admin rights skip per-datasource permission checks when they submit a query. Emergency (break-glass) queries are still checked, for everyone.
  • Several grants add up. A shape denied by any of a user's grants stays denied, and a just-in-time grant that replaces a user's own expiring grant keeps its denied shapes.
  • Supported datasources. PostgreSQL, MySQL, MariaDB, Oracle, SQL Server and custom JDBC. The field is not offered for NoSQL or cloud data-warehouse datasources.
  • Rather review than refuse? Use the Query shape condition in a routing policy instead, for example to send every joined query to a second reviewer.

Users only see the tables they are granted. The same lists decide what a user can browse. The schema tree in the query editor, autocomplete, AI query drafting and the AI agent tools show a user only the tables their allowed schemas and tables cover, leave out their denied schemas and tables, and leave out their denied columns. Administrators still see every table. If the same table name exists in more than one schema, write the entry as schema.table; an entry with just the name then shows neither table. Two places still list every table name on purpose: the just-in-time access request form, because asking for access to a table you cannot see yet is its whole purpose, and the automatic AI review of a submitted query, which reads the whole schema, so its comments may mention other tables.

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>/settings → ER 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), format-preserving, keep first N (0912******), constant (a fixed value such as REDACTED), nullify (NULL), regex replace, numeric bucket (a salary rounded down to its 10,000 band, an age to a band) or date generalization (a birth date to its year, quarter or month) — with an optional reveal-to condition. Strategy settings are checked when you save the policy, so a broken regular expression is rejected there rather than during someone's query, and a value a strategy cannot handle — text in a numeric bucket, a value the regex does not match — is fully masked instead of shown. 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>/settings → Masking. 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, or a service account's Attributes tab). 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, and the query's detail page keeps the effective SQL — the statement as it actually ran, with the policy's filter in place and its values shown as ? — so an auditor sees what executed even after the policy is later changed or deleted.

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>/settings → Row security. Per-table predicates injected into the parsed SQL; values bound as parameters.

Simulate a policy before you save it. Both the Masking and Row security forms have a Simulate button that dry-runs the draft against this datasource's own past queries, so you see the blast radius first. Pick a date range (up to 90 days) and AccessFlow replays that traffic twice — once against the policies in place today, once with the draft added or replacing the one you are editing — then reports the difference: for masking, which columns would start (or stop) being hidden, in how many past queries, and for whom; for row security, which queries would newly come back filtered, come back empty, or be rejected outright because the engine cannot safely apply the predicate to that shape. Redis is the clearest case — a row rule has no meaning over a key-value store, so the simulation lists exactly the commands the policy would start refusing. The same button sits on the routing policy form.

What a simulation is, and is not. It is strictly a preview: nothing is saved, no query is re-run, and AccessFlow never connects to your database to produce it — row rules are worked out on the stored query text alone. It compares policies against policies — today's rules versus the draft — rather than against what actually happened, because a past result may have come from an emergency, a ticket, or a standing grant the draft has no say over. The results name their own limits: roles and group memberships are read as they stand today, masking is matched on the column name alone (so a name two tables share can be over-counted), and where an engine cannot work out offline what a row rule would do — Cassandra and ScyllaDB need live key information — those queries are listed as unclassifiable rather than counted as unaffected. Simulating is always optional; nothing blocks you from saving.

Row limits. The datasource Row limits tab caps how many rows a query may return when it reads a particular table, so two tables on the same database can have different limits and one team can be held tighter than another on the same table. Each policy names a table (and optionally its schema), a maximum number of rows, and the applies to roles / groups / users it covers (empty = everyone, admins included). A row limit can only ever lower the cap: the datasource's Max rows per query and any per-user limit on the access grant still apply, and the smallest number wins. A query that joins several limited tables gets the lowest of their limits. A policy with a schema also catches queries that name the table without one or with a database name in front, so neither gets anyone more rows. Results that hit the limit are marked as truncated, the table preview obeys the same limit, and when a policy's limit is the one that applied it is recorded in the query's audit entry.

Bytes-scanned cap — limit what a warehouse query can cost. On BigQuery, Snowflake and Databricks you pay for the data a query reads, not the rows it returns, so a query that returns ten rows can still read a terabyte. Before every query runs, AccessFlow asks the warehouse how much data it expects to read. Set Max bytes scanned per query in the datasource's Limits & policies section (entered in MB, GB, TB or PB) and a query expected to read more is refused, with a message that names the estimate and the cap:

  • Checked twice. Once when the query is submitted, before anyone reviews it, and again just before it runs. That covers scheduled, recurring and grouped queries, emergency (break-glass) runs, and a cap you lowered after the query was approved.
  • When there is no estimate. Sometimes the warehouse cannot give one, for example for a CREATE TABLE. Choose what happens under When no bytes estimate is available: Require human review (the default) sends the query to a reviewer even if it would otherwise be approved automatically, and Reject the query refuses it. With Require human review, the check just before a query runs lets it through once it has been approved, and an emergency (break-glass) run, which has no reviewer, also goes ahead and is reviewed afterwards.
  • Tighter for some people. An access grant can set its own, lower Bytes-scanned cap. The smallest cap that applies wins, across the datasource and all of a user's own and group grants, and administrators are bound by it too.
  • An estimate, not a bill. The warehouse's estimate can be higher or lower than what a query really reads. Leave the field empty for no cap.
  • Supported datasources. BigQuery, Snowflake and Databricks — the warehouses that report how much a query will read. The fields are not offered for other datasources.
  • Rather review than refuse? Use the Estimated bytes scanned condition in a routing policy to send expensive queries to an extra reviewer instead.

Data budgets — limit how much one person reads over time. Row limits and the bytes-scanned cap stop one big query. They cannot stop someone copying a table out a few thousand rows at a time over a week. A data budget can: on the datasource Data budgets tab, set how many rows, how much result data (entered in MB, GB, TB or PB), or both, a person may read from this datasource over a rolling window, from one hour up to 31 days:

  • Everyone gets their own allowance. Choose the roles, groups or people the budget applies to, or leave all three empty for everyone who uses the datasource. A budget for a group gives each member the full allowance; it is never shared between them.
  • What counts. Everything a person actually receives from a read: rows after row-level rules and every other cap (masked rows still count), and the size of the result. That includes their own queries, scheduled and recurring runs, grouped requests, table previews and emergency (break-glass) runs. Writes never count. Usage is counted from the moment a budget exists.
  • A rolling window. The budget looks back over the last hour, day or week you chose, measured from now, so older reads drop out gradually. There is no midnight reset.
  • While allowance is left, a query's result is cut short at the rows and data the person has left, and the result says the data budget was the reason.
  • Once it is used up, the budget does what you chose under When the budget is used up. Send to human review (the default) sends every new SELECT to a reviewer, even one that would otherwise be approved automatically, and runs it once a reviewer approves. An approval given earlier, while allowance was left, does not count. Reject refuses new reads outright. Table previews and grouped requests are refused either way. Emergency (break-glass) runs are counted but never cut short or refused by a budget; they are reviewed afterwards as usual.
  • Several budgets. When more than one applies to a person, the one with the least left wins, and Reject wins over review.
  • Warnings. Set Warn at (% used) and the person is notified once when they pass it. When a budget is used up, they and the administrators who manage data budgets are notified. People see what they have left beside the query editor, and admins can open Data usage on any user's row on the Users page.
  • Approximate, by design. Result size is AccessFlow's estimate of the result in memory, not network bytes, and two queries that run at the same moment can go over the limit by at most one query's worth.
  • Rather review than refuse early? Use the Data budget used (%) condition in a routing policy to send a heavy reader's queries to an extra reviewer before they run out.

Export policies. Masking and row security govern what a user sees; the datasource Export policy tab governs what leaves. Each policy sets a mode — allow, watermark, row cap, or deny when classified (optionally scoped to specific classifications) — and an applies to roles / groups / users target (empty = every exporter, no implicit admin bypass). When several policies apply, the most restrictive wins. The policies gate the signed CSV/PDF result download on the query detail page and the results attachment on recurring-run emails: a denied exporter sees a disabled export button with the reason, a watermarked download carries the exporter, timestamp, and query id baked into the signed bytes (the modal previews the exact stamp), and every export lands in the audit log as RESULT_EXPORTED — with an admin notification whenever a classified result leaves.

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.

Automated discovery. Instead of tagging hundreds of tables by hand, the datasource Discovery tab opts a datasource into a scheduled scanner that samples column data through the same governed sampling path, detects sensitive values with local regex + checksum detectors (emails, credit-card numbers with Luhn, US SSNs, IBANs, phone numbers) and — optionally — your bound AI analyzer, then proposes the classification tags in a review worklist. Confirming a finding applies the tag (deriving masking exactly like a manual tag); dismissing suppresses the proposal permanently. Raw sampled values never persist (findings store a redacted sample only), and the AI pass only ever sees column names, types, and redacted samples. Configure the per-datasource sample size (10–1000 rows, never more than the datasource's row cap) and cadence (1–720 hours), or hit Scan now for an immediate run; scans and decisions land in the audit log (DISCOVERY_SCAN_COMPLETED, DISCOVERY_FINDING_CONFIRMED / _DISMISSED). Operator knobs: ACCESSFLOW_DISCOVERY_SCAN_POLL_INTERVAL (PT15M), ACCESSFLOW_DISCOVERY_SCAN_TIME_BUDGET (PT10M), ACCESSFLOW_DISCOVERY_SAMPLE_STATEMENT_TIMEOUT (PT10S), ACCESSFLOW_DISCOVERY_MAX_TABLES_PER_SCAN (200), ACCESSFLOW_DISCOVERY_MAX_AI_TABLES_PER_SCAN (25), ACCESSFLOW_DISCOVERY_MAX_NESTED_DEPTH (5), ACCESSFLOW_DISCOVERY_MAX_NESTED_LEAVES_PER_ROW (100), ACCESSFLOW_DISCOVERY_STALE_SCANS_BEFORE_EXPIRY (3), ACCESSFLOW_DISCOVERY_SCAN_LOCK_AT_MOST_FOR (PT30M).

One scan per datasource at a time. A scan claims its datasource for as long as it runs, so the same tables are never sampled twice at once — including when you run AccessFlow on several servers, where a Scan now and a scheduled scan could otherwise start on different ones. Scan now is refused outright while a scan is under way, so you always know which run you are looking at; a scheduled scan that finds the datasource busy simply leaves it due and picks it up on the next round. ACCESSFLOW_DISCOVERY_SCAN_LOCK_AT_MOST_FOR above is only the safety net for a server that dies mid-scan: it caps how long the claim can outlive the machine holding it. Raising ACCESSFLOW_DISCOVERY_SCAN_TIME_BUDGET raises the claim with it, keeping a wide margin over the time a scan is expected to take — so leaving this one alone is normally right. To hand a datasource back sooner than the cap after a server has died, delete the Redis key job-lock:accessflow:shedlock:discoveryScan:<datasource id>.

Proposals that go quiet clean themselves up. When a column is dropped, its data is cleared, or you mask it by hand, the scan simply stops proposing it — and the old suggestion would otherwise sit in the worklist forever. Instead, a proposal the scanner keeps sampling but no longer finds is marked Stale after a few consecutive scans and drops out of the default Pending view; switch the status filter to Stale to see those proposals and dismiss the batch in one go. Nothing is thrown away: a stale proposal is still yours to confirm or dismiss, and if the data comes back the next scan returns it to Pending. Only tables a scan actually sampled can age this way, so a run cut short by its table cap or time budget never retires proposals it did not look at. Retirements are audited as DISCOVERY_FINDING_EXPIRED, up to 100 rows per scan — past that the scan's own audit entry carries the full count and flags the trail as truncated. How many consecutive misses it takes is ACCESSFLOW_DISCOVERY_STALE_SCANS_BEFORE_EXPIRY above.

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), ACCESSFLOW_PROXY_EXECUTION_INSERT_BATCH_CHUNK_SIZE (1000).
  • Heap protection (relational engines): ACCESSFLOW_PROXY_EXECUTION_MAX_RESULT_BYTES (52428800 — per-result byte cap; larger SELECT results are truncated), ACCESSFLOW_PROXY_EXECUTION_MAX_CONCURRENT (32 — global in-flight execution budget across all datasources), ACCESSFLOW_PROXY_EXECUTION_ACQUIRE_TIMEOUT (5s — wait before overflow executions are rejected with HTTP 503).
  • SELECT result cache: ACCESSFLOW_PROXY_CACHE_ENABLED (true), ACCESSFLOW_PROXY_CACHE_DEFAULT_TTL (PT60S), ACCESSFLOW_PROXY_CACHE_MAX_ENTRY_BYTES (1000000).
  • Data-budget usage history: ACCESSFLOW_CORE_DATA_BUDGET_USAGE_RETENTION (P32D — how long read usage is kept; never less than 31 days and an hour, so the longest window is always covered), ACCESSFLOW_CORE_DATA_BUDGET_PRUNE_INTERVAL (PT1H — how often older usage is cleaned up).
  • Read-replica health checks: ACCESSFLOW_PROXY_REPLICA_PROBE_INTERVAL (PT30S), ACCESSFLOW_PROXY_REPLICA_PROBE_TIMEOUT (PT5S), ACCESSFLOW_PROXY_REPLICA_COOLDOWN (PT30S).
  • 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).
  • Snowflake: ACCESSFLOW_PROXY_ENGINES_SNOWFLAKE_LOGIN_TIMEOUT (PT30S), …_NETWORK_TIMEOUT (PT60S).
  • BigQuery: ACCESSFLOW_PROXY_ENGINES_BIGQUERY_CONNECT_TIMEOUT (PT10S), …_READ_TIMEOUT (PT30S).
  • Databricks: ACCESSFLOW_PROXY_ENGINES_DATABRICKS_CONNECT_TIMEOUT (PT10S), …_WAIT_TIMEOUT (PT10S), …_POLL_INTERVAL (PT1S), …_RESULT_DISPOSITION (auto), …_MAX_RESULT_BYTES (52428800).

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.

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.