---
title: Connect Snowflake
description: Set up key-pair authentication for a read-only Snowflake user, add the data source, and keep the per-refresh credit cost predictable.
updated: 2026-08-04
tier: pro
engines: [snowflake]
---

Snowflake, via key-pair authentication. Dashies signs a JWT with your private key
and calls the SQL API. There is no password and no host to enter.

## Before you start

Two things about Snowflake catch people out more than anything else on this
page, so read them first.

:::danger{title="Use the hyphenated account identifier"}
The identifier must be the hyphen form, `myorg-myaccount`. The dotted org form
`myorg.myaccount` is rejected outright with:

```bash
account_identifier is invalid. Use the hyphenated form (for example myorg-myaccount), not the dotted org form.
```

A dotted identifier is accepted only in the legacy locator-with-region shape,
such as `xy12345.ap-southeast-2.aws`.
:::

:::warning{title="An account network policy will look like an auth failure"}
If your account has a `NETWORK POLICY` that allowlists office or VPN addresses,
it blocks Dashies, and Snowflake reports that as a rejected sign-in rather than a
blocked address. Check for one before you go hunting for a key problem. The
Dashies error text names this possibility for the same reason.
:::

## 1. Create the key pair

Run these on your own machine. The private key stays with you until you paste it
into the Dashies form; the public key goes on the Snowflake user.

```bash
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out dashies_key.p8 -nocrypt
openssl rsa -in dashies_key.p8 -pubout -out dashies_key.pub
```

`dashies_key.p8` is the PKCS#8 private key you paste into Dashies.
`dashies_key.pub` holds the public key you register on the Snowflake user in the
next step. When you register it, paste only the base64 body, without the
`-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines.

## 2. Create the warehouse, role, and user

Run this in a Snowflake worksheet as `ACCOUNTADMIN`. It creates a dedicated
X-Small warehouse, a read-only role, and a service user carrying the public key.

```sql
create warehouse if not exists DASHIES_WH
  warehouse_size = XSMALL
  auto_suspend = 60
  auto_resume = true
  initially_suspended = true;

create role if not exists DASHIES_RO;

grant usage on warehouse DASHIES_WH to role DASHIES_RO;
grant usage on database ANALYTICS to role DASHIES_RO;
grant usage on schema ANALYTICS.PUBLIC to role DASHIES_RO;
grant select on all tables in schema ANALYTICS.PUBLIC to role DASHIES_RO;
grant select on future tables in schema ANALYTICS.PUBLIC to role DASHIES_RO;

create user if not exists DASHIES_SVC
  type = SERVICE
  login_name = 'DASHIES_SVC'
  default_role = DASHIES_RO
  default_warehouse = DASHIES_WH;

grant role DASHIES_RO to user DASHIES_SVC;

alter user DASHIES_SVC set rsa_public_key = 'PASTE_THE_BASE64_BODY_OF_dashies_key.pub';
```

Replace `ANALYTICS` and `ANALYTICS.PUBLIC` with your own database and schema, and
repeat the two `grant select` lines for every schema you want readable.

:::note{title="LOGIN_NAME must match what you type into Dashies"}
The JWT identity Dashies presents is derived from the user name you enter. If the
user's `LOGIN_NAME` differs from it, Snowflake rejects the sign-in. Keeping them
identical, as above, avoids the whole class of problem.
:::

The `grant select on future tables` line matters for the same reason
`alter default privileges` does on Postgres: without it, a table created later is
invisible and the refresh that needed it breaks with no warning.

## 3. Add the data source

Open [dashies.xyz/app/connections](https://dashies.xyz/app/connections), click to
add a data source, and pick **Snowflake**.

| Field | Required | Example | Notes |
|---|---|---|---|
| Account identifier | yes | `myorg-myaccount` | Hyphen form. See above. |
| User | yes | `DASHIES_SVC` | The login name of the read-only user. |
| Warehouse | yes | `DASHIES_WH` | The warehouse refresh queries run on. |
| Role | no | `DASHIES_RO` | |
| Database | no | `ANALYTICS` | |
| Private key | yes | a PKCS#8 PEM block | The contents of `dashies_key.p8`. |
| Display name | no | `Analytics warehouse` | Up to 120 characters. |

Warehouse, role, and database are upper-cased on the way in, because an unquoted
Snowflake identifier resolves upper case.

The host is derived from the account identifier, so there is nothing to enter and
no address to allowlist on our side.

## 4. It verifies in one step

Snowflake is a single-step connect. Creating the data source stores the key,
mints the key-pair JWT, and runs two metadata probes, then comes back **active**
with the databases it found, or **error** with a class. Up to 200 databases are
stored as the allowlist. There is no Resync button on Snowflake.

If it fails:

| Message | Meaning |
|---|---|
| `Snowflake denied access. Check that the key pair is registered on the user (RSA_PUBLIC_KEY) and that the account identifier is correct. An account-level network policy can also block access.` | Any authentication failure. |
| `The warehouse was not found or the user's role cannot use it. Check the warehouse name and its grants.` | The warehouse name is wrong, or the role has no `usage` on it. |
| `We couldn't read that account. Check the account identifier and try again.` | The account identifier resolves but the probes did not succeed. |

## Cost

:::warning{title="A cold refresh costs at least 60 seconds of credits"}
Snowflake bills a **60-second minimum** every time a warehouse auto-resumes. A
scheduled refresh that finds the warehouse suspended therefore costs at least one
minute of that warehouse's credits, however short the query is.

Two things keep this predictable:

- Use a **dedicated X-Small warehouse with `auto_suspend = 60`**, as the script
  above creates. Do not point Dashies at a large shared warehouse.
- Match the refresh cadence to how fast the data actually moves. Hourly refresh
  of a month-grained cube buys nothing and pays the 60-second floor 24 times a
  day. See [Set a refresh schedule](/guides/refresh-schedule).
:::

Connecting itself is free. The verification runs `SHOW DATABASES` and
`SHOW WAREHOUSES LIKE`, which are metadata-only and burn **zero** warehouse
credits.

## Caps

| Limit | Value | What happens past it |
|---|---|---|
| Rows per dataset query | 100,000 | Refused before any partition is fetched. |
| Inline result partitions | 32 | Query errors. |
| Statement wait | 30 seconds, then polling | Query errors if it never completes. |
| Data island, whole dashboard | 8,388,608 bytes | Publish is refused. |
| Compiled dashboard body | 5,242,880 bytes | Publish is refused. Usually binds first. |

There is no execution-time byte cap the way there is on Postgres. The adapter
reads Snowflake's own row total up front and refuses before fetching. A wide cube
is still bounded, one layer later, by the island and body limits.

Snowflake supports every materialization, including Parquet offload for a
row-level dataset.

How these ceilings relate to each other, and which one binds first, is in
[sizes and ceilings](/concepts/dataset-modes#sizes-and-ceilings).

## Dialect notes

Cube SQL is Snowflake SQL.

- Table references are database-qualified: `from ANALYTICS.PUBLIC.ORDERS`.
- **Bucket dates in your business time zone, in the SQL**:

  ```sql
  select date_trunc('MONTH', convert_timezone('UTC', 'America/Los_Angeles', ordered_at)) as month,
         sum(amount) as revenue
  from ANALYTICS.PUBLIC.ORDERS
  group by 1
  order by 1
  ```

- Relative window: `dateadd('month', -12, current_timestamp())`. Conditional
  count: `count_if(c)`. Exact median:
  `percentile_cont(0.5) within group (order by x)`.
- Snowflake folds an unquoted output alias to **upper** case. That is handled for
  you: a case-only difference between the alias and your declared measure key is
  canonicalized at publish and at refresh, so `sum(amount) as revenue` against a
  measure named `revenue` is correct. You do not need upper-case manifest keys.
  What is refused, loudly and naming both, is two output columns that differ only
  by case landing on one declared key.
- A `VARIANT`, `OBJECT`, or `ARRAY` column has to be addressed into before it is
  usable, and `lateral flatten` **multiplies rows**. Nothing rejects that, so the
  cube runs and the numbers are simply wrong. Aggregate back to the grain you
  meant. See [Verify your numbers](/guides/verify-your-numbers).

## Rotating the key

Edit the data source and paste a new PKCS#8 PEM under **Private key**, then
register the matching public key on the Snowflake user.

## Check it worked

1. The data source reads **active** on
   [dashies.xyz/app/connections](https://dashies.xyz/app/connections), and lists
   the databases it discovered.
2. Ask your AI tool to introspect it and run one query:

   > Introspect my Snowflake data source, then validate this cube SQL against it:
   > `select 1 as ok`

3. Then [author a dashboard against it](/guides/author-a-dashboard).
