Skip to content

Trust

Warehouse credentials

Where the credential you enter is stored, what can read it, and exactly how much of read-only Dashies enforces on each warehouse engine.

Connecting a warehouse hands Dashies a credential that reaches your own data. It is the most sensitive thing you give us, and it is handled differently from everything else.

The path a credential takes

You type it into the connect form in the Dashies web app. That is the only way in.

From there it goes straight to storage:

  • It is posted over HTTPS from the form to a Dashies server route.
  • It is written into Supabase Vault, encrypted at rest.
  • The connection's own record stores a reference to that vault entry, never the credential itself. Everything visible on the record is non-secret metadata: a label, the engine, a host, a status.

Nothing later reads it back out to a person or a client. There is no screen that displays a stored credential, no API that returns one, and no support flow that recovers one. Changing it means replacing it.

Your AI tool never receives it

This is worth stating carefully because it is the question people actually have.

No publishing tool takes a credential as a parameter. Not a password, not a key, not a token. The tool that lists your data sources returns ten fields, and they are all descriptive: id, kind, label, engine, status, last_verified_at, last_error, health_state, health_error, and health_source. No password, no secret, no host, and no internal database detail.

And the publishing service could not read one if it tried. That is the part worth stating carefully, because "we do not do that" is a weaker promise than "the system is not built so that we could".

Two independent things stand in the way, and both would have to change together:

  • The service your AI tool connects to holds no administrative database key. It is not that the key is unused there; the configuration that would carry it does not exist on that service. It authenticates as you, with your own permissions.
  • Every routine that decrypts a stored credential is executable only by a server-side administrative role, and is explicitly revoked from the ordinary signed-in role. So even a service holding your session could not call one.

The first of those is enforced by the build rather than by review: code in the publishing service that tries to reach for an administrative key fails to compile, and that check runs on every change before it can ship. We verified this by making the change deliberately and confirming the build rejects it.

The credential path and the authoring path are therefore separate systems that meet only at a connection id. Your AI passes the id; the server does the rest.

What reads it, and what is recorded

Only server-side routines, and only in three situations: testing a connection when you click Test, introspecting or validating while you author, and running a scheduled refresh.

Where the credential has to be pulled out of storage into a server process to call the vendor's API, that read writes a row to the connection's own append-only event log. Append-only here means enforced by the database rather than by convention: updates, deletes, and truncation of that log are rejected at the storage layer.

PostgreSQL and SQL Server work differently, and better. On those two the credential is used inside the database, by the confined routine that runs your query, and is never materialized in a server process at all. So there is no extraction event to log on that path, because nothing was extracted. Do not read an empty log there as the absence of a check; read it as the absence of a handover. The Postgres extract path, which does pull a connection string out, does write its own entry.

Errors never carry your credential or your host

When a warehouse connection fails, the underlying driver's error typically contains the host it dialled, the resolved address, the port, and the user it tried. None of that is passed on.

The server reads the driver's detail only far enough to classify the failure into a fixed vocabulary, then discards it and raises only the class. That is why a failure in the app reads as something like "the credential was rejected" rather than quoting your database's message, and why the run history stores a category rather than a sentence. The one exception is deliberate: an error in your own cube SQL is returned verbatim, because that text is yours and you need it.

How much of read-only is actually enforced

This is the part where the honest answer differs per engine, and where a general reassurance would be misleading.

Grant a read-only credential on every engine. How much Dashies adds on top of it varies by engine, and on none of them does it replace the credential, so the credential is the one control that is doing work no matter which engine you picked.

The cube must be a read-only query, but the mechanism is not the same everywhere

One property holds on all six of the engines this section covers: your SQL is rejected unless it begins with select or with. That is the whole of the shared floor, and the two checks people assume sit beside it each have an exception.

Oracle Database is not in this section, and its absence is measured rather than pending

This section covers six engines. Oracle Database is connectable and can back a refreshing dashboard, and it is left out here because the shared floor above is not what stops a write on it, so counting it in would assert a check it does not have.

Measured 2026-09-18, the same non-select statement sent to both engines through the authoring channel: on SQL Server it is refused by Dashies, naming the rule and the verbs; on Oracle Database it reaches the warehouse, which refuses it itself with ORA-00903. The publish path answers the same way. So what stands between an Oracle Database cube and a write is the read-only user, plus the fact that Dashies wraps your statement as a subquery, which a non-select cannot be. Both are real and neither is the text check this section is about.

The read-only user is checked more often than "at connect" suggests. The connect probe verifies it and refuses to create the data source without it, and then every extract re-probes the same privileges before your SQL is compiled and refuses the run on anything it finds. So it is a per-refresh check rather than a one-time one, which is the strongest of the protections named here.

This is a decided difference rather than a gap awaiting a fix, and the code says so where the check would live: Oracle Database has no database-side guard to port, so a text check here would be one Dashies invented, and one strict enough to reject a write verb would also reject a column legitimately named insert and refuse SQL that is perfectly valid. #3339 is where that decision is recorded and would be revisited. Do not add Oracle Database to the counts or the table below while it stands.

Bounding the query to a single statement holds on five. It does not hold on SQL Server, where the statement terminator is optional, so no check on the text can reliably tell how many statements a query contains. That is not an oversight, and it is the specific reason SQL Server is the one engine where a read-only login is required and verified rather than recommended.

Stripping comments and string literals before checking holds on five, so a keyword cannot hide inside one. What hides a keyword differs per dialect: dollar-quoting on Redshift, raw string prefixes and backtick identifiers on Databricks, bracketed identifiers on SQL Server. PostgreSQL does not strip; it refuses. Anything containing a ; or a /* is rejected outright and the leading keyword is matched against the raw text. That is more conservative than stripping, not less: there is no stripper to outwit.

Above that floor the engines diverge, and the difference is worth knowing rather than smoothing over:

EngineWhat else stands between your cube and a write
PostgreSQLThe query runs inside a read-only transaction. The database refuses a write itself, whatever the text says.
BigQuery, Snowflake, Redshift, Databricks, SQL ServerWrite verbs are rejected as whole words, including into, because select ... into creates a table on several of these engines and would otherwise pass a leading-select check. SQL Server rejects the DDL verbs as whole words too; on the other four a DDL statement begins with its own verb, so the floor above is what refuses it.

Until 2026-09-12 that table had a third row, and BigQuery and Snowflake were in it. Those two had the shared floor and no keyword reject, so the privileges on your credential were the only thing above the floor. This page said so at the time, and it is recorded here rather than quietly removed.

What the keyword reject is, said exactly. It is a list of words checked against your query once comments and quoted text have been removed, and it refuses a write verb anywhere in what is left rather than only as the first word. It is not the database refusing anything, and it is not a permission. Two limits follow from that and neither is theoretical: a list of words can only name the writes someone thought to list, and a verb hidden by something our reader of your dialect gets wrong is invisible to it. So on those five engines the privileges attached to the credential are still what actually limits a write.

PostgreSQL's read-only transaction is stronger in kind than either check on the text, because it is the database enforcing it rather than a reading of the query.

PostgreSQL: structural confinement, plus the role you create

Each connection gets its own private schema and its own no-login, no-inherit database role with no memberships. The routine that runs your cube SQL is owned by that role and runs as it, natively, with no privilege switch to undo. It refuses to run at all if it is ever re-owned, and attempting to switch roles from inside cube SQL is rejected by the database.

References to system catalogues are also blocked, so one tenant's query cannot enumerate another's connection topology.

What Dashies does not do here: it does not check that the login you supplied is read-only. The grant select-only role you create is doing real work. The connect form gives you the exact SQL for it. See Connect PostgreSQL.

SQL Server: a read-only login is required and verified

The only engine where Dashies refuses your credential outright. On the credential axis this is the strongest of the six; PostgreSQL is the strongest on the query axis, above. They are different guarantees, not a ranking.

When you test the connection, Dashies probes the login's actual privileges and refuses it if it holds any write or administrative privilege, with readonly_login_required. A server administrator account is rejected, not warned about.

It is required rather than recommended because of the gap noted above: with an optional statement terminator, no check on the query text can reliably bound how many statements it contains. So on this engine the read-only login is the boundary, and the SQL check is only defense in depth. See Connect SQL Server.

Transport is also fixed rather than configurable: encryption is always on and server certificate validation is never disabled.

BigQuery, Snowflake, Redshift, and Databricks: your grant is the boundary

These four are reached over the vendor's own HTTPS API rather than a database connection, so there is no schema, role, or read-only transaction for Dashies to confine your query inside. All four get the keyword reject described above, and it is a reading of your query text rather than a permission, so on all four the privileges attached to the credential are what actually limits it.

Concretely, that means a service account, key pair, IAM identity, or service principal with more than read access has more than read access, and Dashies will not stop it. Each engine's guide gives the minimum grant. Follow it.

Reaching your network

Dashies connects outward to the warehouse you name; nothing connects inward to you.

For engines where you supply a host, the address is validated before anything is dialled, and private and internal address ranges are refused, including addresses written in unusual numeric forms. Which ports are permitted is fixed per engine.

IP allow-listing works for SQL Server, Oracle Database, and a PostgreSQL database you add as only accepting known addresses. Every connection to those comes from the two fixed addresses for your workspace's region, listed on IP addresses, so your firewall can admit those and nothing else. A PostgreSQL data source added without that setting connects from other addresses, so it has to accept connections from the public internet. A Snowflake network policy, a Databricks IP access list or a BigQuery service perimeter that admits only known addresses blocks Dashies today.

For BigQuery, Snowflake, and Redshift you never supply a host at all: it is derived server-side from the account or region identifier, so there is nothing there to point somewhere else. Databricks takes a workspace host, constrained to Databricks' own domains.

What this does not protect you from

Stated plainly, because a security page that only lists strengths is not useful.

  • An over-privileged credential. SQL Server is the only engine where Dashies checks whether the login you supplied is read-only. Everywhere else, including PostgreSQL, a credential with write access has write access, and the read-only SQL check is the only thing standing in front of it. That is your decision, not ours.
  • Data you choose to expose. A cube that selects raw personal data ships raw personal data into the dashboard. See dashboard isolation for who can then read it.
  • Your warehouse's own access controls. Dashies reads with the identity you gave it. If that identity can see a table, so can a dashboard built on it.