Warehouse-Backed Reporting
Register tables from Snowflake or Databricks as read-only object types, then read them through the same governed CRUD surface as the rest of your application. From a real 18-schema Databricks bundle.
The Project
⚠️ This is not a BI product
bundle_qa-databricks in ~/supero-test/e2e-tests/apps/bundle_qa-databricks, namespace acme_lakehouse — eighteen schemas, sixteen of them read-only mirrors of Databricks tables.Architecture & Design
READ_ONLY_SCHEMAS = ["access_role", "activity", "address", "category",
"company", "contact", "customer", "invoice",
"member", "note", "order", "order_item",
"organization", "payment", "product", "task"]
{"name": "access_role",
"namespace": "acme_lakehouse",
"schema_type": "object",
"parent_type": "tenant",
"source_stream": "roles",
"schema_source": "connector",
"access_mode": "warehouse",
"read_only": True,
"read_only_source": "derived",
"source_details": {"type": "database",
"source_stream": "roles",
"db_type": "databricks",
"connector_uuid": "..."},
"end_points": [{"type": "database", "name": "mongodb",
"priority": 1, "operations": ["read"]}],
"attributes": [
{"name": "role_id", "type": "integer"},
{"name": "role_name", "type": "string"},
{"name": "is_active", "type": "boolean"}]}| Field on the schema | What it means |
|---|---|
schema_source: "connector" | This type came from a registered source, not your app model. App-authored schemas carry "app" instead. |
read_only: true | Writes are refused. Sixteen of the eighteen schemas here carry it. |
read_only_source: "derived" | The flag was derived from the source, not set by hand. |
access_mode: "warehouse" | Reads are served from the warehouse. An OLTP live source stamps live-ro. |
operations: ["read"] | The endpoint exposes read and nothing else — no create, update or delete to call. |
source_stream | The table on the far side. The schema is access_role; the Databricks table is roles. |
source_details.db_type | Where the database type lives. Not a top-level db_type, not connector_type. |
generic_status, an enum, and landing_override, a platform system schema for the public landing page. Everything else in this app is warehouse data.The Flow
- 1
Register the source
The discriminator is
source.plugin—http_api,csv_file,databaseorwebhook. A warehouse isdatabase, and the engine goes atsource.config.db_type:databricksorsnowflake. - 2
Name the tables
table_namesis always a list. There is no singulartable_name, even for one table. - 3
Choose a mode per table
synccopies rows in;live_readandlive_readwriteleave them where they are. Live reads work on thedatabaseandhttp_apiplugins; live writes aredatabaseonly. - 4
The schemas appear
Registered tables become object types in your namespace, stamped as above. Databricks and Snowflake come back read-only, with
access_mode: "warehouse". - 5
Read them like anything else
Through
/query,/list-bulkand/aggregateon the normal CRUD path/api/v1/crud/{domain}/{type}. There is no live-specific endpoint to learn. - 6
Put a surface on it
They are ordinary object types, so the generated application lists, filters and charts them as it does app-owned records.
limit plus cursor. page and page_size are silently ignored — on a warehouse table, a quiet way to get the same first page forever. See Querying & Pagination.What You Get Without Building It
- •Warehouse tables as first-class object types, addressable at
/api/v1/crud/{domain}/{type}like every other record. - •Read-only enforced at the schema, with
operations: ["read"]on the endpoint rather than as a UI convention. - •The platform roles applied to warehouse data, with no per-query permission layer of your own.
- •Tenancy over data that has none of its own — records address as
fq_name,[domain, project, tenant, record]. - •Aggregation through
/aggregate, so a summary and a detail list are one kind of call. - •Proven sources: Databricks and Snowflake as warehouses; PostgreSQL and REST as live OLTP reads; MSSQL, MySQL, Postgres and CSV in sync mode.
ℹ️ On Salesforce
bundle_qa-sfcrm is Snowflake despite its name. If Salesforce is on your critical path, treat it as unproven and get in touch before planning around it.How to Implement in Supero
Model it
- 1
Build the schema
Point a connector at your warehouse and register the tables you need — they become object types read through the same CRUD,
/queryand/aggregateAPI (see Connect Your Database). Then generate a UI over them in App Studio, or consume the API from your own front-end. - 2
Configure the services
The warehouse’s credentials go in the keystore, not a schema or config file — see Connector Security. There are no transactional-service keys in this shape; per-customer governance is access policies, not provider credentials.
What you write yourself
⚠️ A 200 with zero rows is not an empty source
access_mode: a real warehouse read stamps warehouse, a real OLTP live read stamps live-ro.ℹ️ A passing connection test is not a passing read
POST /api/v1/connectors/test-config (test a configuration before saving), POST /api/v1/connectors/{id}/test (test a saved connector — this is what the console's detail page calls), POST /api/v1/connectors/{id}/discover (async — poll /discover/{execution_id}/results), POST /api/v1/connectors/{id}/run with {"full_sync": true}, and POST /api/v1/connectors/{id}/run_query for warehouses. Use them. But they answer a narrower question than operators expect: they establish credentials and reachability, not that a given caller gets correctly scoped rows back. A read stamped with the right access_mode is the check to put in a runbook.| Decision | Why it is yours |
|---|---|
| Which tables to register | The whole warehouse gives you hundreds of object types and a navigation problem. Pick the tables a person will open. |
sync versus live_read | Live is current and costs a round trip per read. Sync is fast and stale. Neither is the default answer. |
| Freshness expectations | On sync, how out of date the copy may be is a promise you make to your users, not one the platform makes. |
| Joining warehouse and app data | The two live in different stores. Combining them for a screen is application work. |
| Field-level exposure | Warehouse tables often carry more columns than an app user should see. Trim at registration, or mark them hidden for a role with an access policy. A hidden field is more than stripped from rows: a query that groups, filters, or sorts by it is refused, so it cannot be recovered through the aggregate endpoint either — the redaction holds where the data would otherwise leak. |
💡 Serving reports to your own customers
Deployment
- 1
Preview while you iterate
A preview deploy answers at
https://preview-{domain}-{project}.supero.livefor a short lease while you build and test. Publish Your App covers how preview and live differ. - 2
Go Live
Go Live publishes to a permanent, Supero-hosted URL —
https://live-{domain}-{project}.supero.live— with no cloud account to connect and nothing to provision. It is asynchronous: you get a202and a poll URL, then the live address. The full contract is in Publish Your App. - 3
Watch the logs
When a deploy fails or the live app misbehaves, the **Cloud Deploy Logs** page in the dashboard shows the published service’s container logs — pick the Live lane to read the published app rather than the preview build (up to ~24 hours back, ~500 lines per request). For platform activity — requests, auth, errors across your domain —
GET /api/v1/logsis a filtered read, and a single request traces across services at/api/v1/logs/trace/{correlation_id}when you set your ownX-Correlation-ID. See Operations & Observability.
Final Thoughts
access_mode, where a real warehouse read stamps warehouse and a real OLTP live read stamps live-ro. A healthy-looking 200 is not evidence the source was reached.sync-versus-live_read tradeoff, the freshness promise, and the field-level exposure each role gets. The governed read surface — the CRUD path, the platform roles, tenancy over data that has none of its own — stays the platform's; the warehouse stays where it is, and what you add on top is policy, not machinery.Where to Go Next
Governing it
Access Control
Applying the platform roles and access policies to connector-backed types. Open
On this page