---
title: Warehouse credentials
description: Where the credential you enter is stored, what can read it, and exactly how much of read-only Dashies enforces on each warehouse engine.
updated: 2026-09-22
---

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.

:::note{title="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](#sql-server-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:

| Engine | What else stands between your cube and a write |
|---|---|
| **PostgreSQL** | The query runs inside a **read-only transaction**. The database refuses a write itself, whatever the text says. |
| **BigQuery**, **Snowflake**, **Redshift**, **Databricks**, **SQL Server** | **Write** 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](/guides/connect-warehouse/postgres).

### 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](/guides/connect-warehouse/sqlserver).

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](/reference/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](/trust/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.
