NEWSupero Studio — generate, control, and deploy production apps.Start free →HIRINGSee the roles →

A governed analytics portal over Snowflake and BigQuery

A self-serve portal on the warehouse — with the two things it will not give your application: access control it can enforce without a warehouse admin, and a spend ceiling on every query your users type.

ConnectorsBuild estimate ~half a day9 min readAugust 25, 2026

Warehouses are excellent at answering questions and terrible at deciding who may ask them — or how much an answer is allowed to cost. Supero binds warehouse tables as live-read schemas, applies your roles on top, and runs every query under explicit row, byte and time caps.

The brief

The BI request that never ends

Sales leadership wants a portal: pipeline by region, revenue by segment, a drill-down into the underlying facts, and the ability to ask a question nobody thought to build a chart for. The data is already in Snowflake and BigQuery. It is modelled, it is clean, it is correct.

And yet the portal takes a quarter, because the warehouse solves exactly one of the four problems. It has the answers. It has policies for who may see which rows and columns — administered by warehouse admins, in warehouse terms, per warehouse. What it does not have is any of that expressed in your application's roles, a spend ceiling on a query one of your users typed, or a surface a regional manager will actually open. An unbounded SELECT * typed at 4pm on a Friday can still bill four figures.

0
rows copied — warehouses are live-read only
4
cost caps on every single query
~½ day
typical build for one developer
The mechanism

Two read paths, two different guarantees

This is the part most worth understanding before you design the portal, because the two paths differ in exactly one crucial way: whether the platform can safely inject a tenant filter.

Path A — a structured read (the default, fully governed)
PortalbrowserSupero APIquery endpointAccess engineroles · tenancyRead plannerwarehouse driverSnowflake / BigQueryyour account1POST /crud/{domain}/query {type: order_facts, filters}2resolve caller scope3authorized plan + hidden-field set4compile to real SQL · apply row/byte/time caps5statement (native timeout where the dialect accepts one)6submit + poll + drain, inside a 90 s deadline7map rows to the bound schema · attach identity envelope8{results, access_mode: "warehouse"}hit the 10,000-row cap? source_meta.scan = "partial" — and the read still SUCCEEDS
The response is stamped access_mode: "warehouse" so your UI can detect a read-only schema without a second call and hide write controls entirely.
Path B — the SQL console (powerful, and deliberately gated)
Analystor an AI assistantSupero APIrun_querySQL guardstatic validationWarehouseread-only role1run_query {sql, params, row_limit}2is this connector eligible at all?refused unless the warehouse role is attested read-only and no table is tenant-scoped by column3validate before anything is sent4400 + code: not_a_select | multiple_statements | forbidden_keyword | query_too_long | empty_queryIF IT PASSES5single read-only statement, parameterised6raw columns — no identity envelope7{rows, columns, result_count}over 64 MiB buffered, or past the row cap ⇒ the statement is cancelled and the call fails 413
The honest limitation, stated plainly: opaque caller-written SQL cannot have a per-row tenant filter injected into it, so run_query does not carry automatic tenant scoping. That is why it is refused for any connector whose tables use column-based tenant scoping, and why the warehouse role must be attested read-only. It is also not available on Microsoft Fabric.
The part finance cares about

Four caps on every query

Warehouse compute is billed by the warehouse. Supero's caps are designed to fail a runaway query loudly rather than quietly scan a terabyte.

ControlDefaultWhat happens when it is hit
Row cap10,000 rowsA structured read stops and marks source_meta.scan as partial — it never fails. run_query cancels and fails with 413, because opaque SQL has no safe way to signal "more available".
Byte cap64 MiB bufferedThe in-flight statement is cancelled; the call fails with 413 regardless of row count.
Query deadline90 secondsBounds the whole statement — submit, poll and drain — deliberately under the platform proxy timeout so Supero cancels first and returns a structured error.
Statement timeout60 secondsPushed to Snowflake and ClickHouse as a native query timeout that stops warehouse compute. BigQuery, Databricks and Redshift do not accept an equivalent — there the deadline bounds Supero's wait, and the query itself may run to completion. Lean on BigQuery's bytes-billed cap and your own LIMIT on those three.
  • BigQuery additionally enforces maximumBytesBilled, on by default, so an unbounded SELECT * cannot produce a surprise bill even if you never configured a cap. Override per connector with maximum_bytes_billed (0 removes it explicitly).
  • ClickHouse: if your role already constrains max_result_rows or max_result_bytes, Supero drops the specific cap the error names and retries once, keeping the other as a backstop — your source-side constraint stays the effective limit instead of the read failing.
  • Redshift has no native per-query byte cap and takes no pushed-down statement timeout, so your own LIMIT and narrow filters are doing the work — the 90-second deadline bounds Supero's wait, not Redshift's compute. Filter on partitioned or clustered columns and scope date ranges narrowly.
Step by step

How to build it on Supero

The whole path
WAREHOUSE TO PORTALRead-only rolein the warehouseConnectdialect configDiscoverpick tablesBind live_readentity vs eventPolicyroles · hidden colsPortal + agentlive

Mint a read-only warehouse role

Everything downstream depends on this, including whether the SQL console is available at all — run_query requires an attested read-only role. Grant SELECT on the specific tables the portal needs and nothing else.

⏱ ~15 minutes

Connect the warehouse

All six warehouses use plugin: "database" with a dialect-specific config. Snowflake authenticates with a key pair; BigQuery with a service account; Databricks with a token; Redshift with an IAM/Data-API credential. Secrets are held as secret_refs and resolved server-side at query time — a GET on a connector never echoes them.

json
// Snowflake
{ "plugin": "database", "config": {
    "db_type": "snowflake", "account": "ORG-ACCOUNT", "warehouse": "COMPUTE_WH",
    "database": "ANALYTICS", "schema": "PUBLIC", "role": "READER",
    "username": "SVC_SUPERO", "private_key": "-----BEGIN PRIVATE KEY-----...",
    "query_type": "table", "table_names": ["ORDER_FACTS", "CUSTOMERS"] } }

// BigQuery
{ "plugin": "database", "config": {
    "db_type": "bigquery", "project_id": "my-gcp-project", "dataset": "analytics",
    "location": "US", "service_account_json": { "...": "..." },
    "query_type": "table", "table_names": ["orders"] } }

⏱ ~20 minutes

Discover, then bind — and label your tables honestly

Every warehouse table must bind live_read; the platform validates this on connector create and on any update touching the table set, so a table cannot silently drift into an unsupported mode later. One consequence worth planning for: a stream you class as event is addressed by dimension and time range, not by uuid — get-by-uuid on it is refused. Drill-downs work on dimension tables; on a fact table, drill through to a dimension instead.

Set stream_class correctly while you are here: it distinguishes bounded dimension tables from time-series fact tables, and it changes how reads are planned. The stored binding key is field_map, always {schema_field: source_column}. Discovery only needs the columns you rename — every other discovered column gets an identity entry added automatically at confirm time, so the saved map covers the whole table rather than being sparse.

⏱ ~30 minutes

Layer the access model the warehouse does not have

This is the step that turns a warehouse into a portal. Roles, tenant isolation and field hiding apply to warehouse-backed schemas exactly as they do to native ones.

python
POLICIES = [
    PolicyDef(role="tenant_admin", default_access="full", rules=[]),

    # Managers: facts and customers, but never the margin columns.
    PolicyDef(role="tenant_user", default_access="none", rules=[
        PolicyRule(entity="order_facts", can_read=True,
                   hidden_fields=["unit_cost", "gross_margin"]),
        PolicyRule(entity="customers",   can_read=True,
                   hidden_fields=["lifetime_discount"]),
    ]),

    PolicyDef(role="viewer", default_access="none", rules=[
        PolicyRule(entity="order_facts", can_read=True,
                   hidden_fields=["unit_cost", "gross_margin"]),
    ]),
]

Then test it as a user, not as an admin: sign in as viewer, try to sort by gross_margin, and confirm the refusal happens before the warehouse is ever billed for the query.

⏱ ~1 hour

Build the portal and switch on the agent

Generate the dashboards and detail views from the bound schemas. The in-app AI agent needs no wiring: it derives search_/get_ tools per bound entity plus shared analytics tools — db_stats for sums and averages, db_count_by for grouped counts, db_distinct for the value list behind a filter, db_aggregate for multi-stage grouping — and every one goes through the same access engine as the UI.

⏱ ~1–2 hours

Open the SQL console to the few who need it

Expose run_query behind an admin-only route. Always pass params rather than interpolating values, always set row_limit, and surface the rejection code verbatim — not_a_select, multiple_statements, forbidden_keyword — so an analyst can fix their own query.

bash
curl -X POST https://api.supero.dev/api/v1/connectors/CONNECTOR_ID/run_query \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sql": "SELECT region, COUNT(*) AS orders, SUM(amount) AS total FROM order_facts WHERE order_date >= ? GROUP BY region ORDER BY total DESC",
    "params": ["2026-01-01"],
    "row_limit": 200
  }'

⏱ ~30 minutes

Know the edges

What a warehouse connector will not do

  • No sync mode — there is no whole-warehouse copy into Supero.
  • No live_readwrite, and no reverse-ETL back into the warehouse.
  • Writes fail (403/405/409/400 depending on the path). If you need write access to the same system, point a regular database connector at it instead.
  • Latency is your warehouse's latency, and repeated reads mean repeated warehouse compute. There is no staleness window — that is the trade.
  • run_query returns raw columns with no identity envelope, so it does not feed drill-downs, references or the agent. Use structured reads for those.
  • Microsoft Fabric ships for structured live reads, but it speaks T-SQL over TDS and is routed through the SQL Server reader rather than the shared warehouse driver — so the SQL console (run_query) is not available on Fabric.
The economics

Time to deliver

PhaseOn SuperoConventional BI build
Read-only role + connectivity~15 min~15 min (same either way)
Semantic layer over the tables~30 min (bind)1–3 weeks
Column-level access per role~1 h of policy2–4 weeks, and again for each BI tool
Cost guardrails0 — on by default1 week, typically after the first surprising bill
Dashboards + drill-downs~1–2 h2–4 weeks
Natural-language querying0 — the agent ships with itA separate product and a separate budget
Total~½ day~6 weeks
The takeaway

When you should just buy a BI tool instead

Be honest about the alternative, because for a lot of teams it is the right one. If what you need is exploration by analysts who already write SQL, dashboards that change weekly, and a semantic layer several teams share, buy the BI tool. It is better at that than this is, and it always will be.

Reach for this instead when the consumers are not analysts. A portal your customers log into, a regional manager who will never open a BI seat, an app that needs warehouse facts alongside its own operational data, an AI agent answering in the asker's own permissions — those are application problems that happen to read from a warehouse, and a BI tool is the wrong shape for all four.

Your warehouse already knows the answers. What it does not have is those answers expressed in your application's roles, a spend ceiling on a query a user typed, an identity per row so a drill-down works, or a UI a regional manager will open.

Delivery estimates in this guide are for the scope described, built by one experienced developer with any vendor sandbox credentials already in hand. Treat them as a realistic starting point, not a commitment — and check the documentation for the authoritative behaviour of any API named here.

Build this on Supero

Describe it and get schemas, APIs, SDKs and a governed multi-tenant backend — or point your own AI editor at Supero over MCP and build it there.