S
supero.docs
Documentation/Application Architectures/Warehouse-Backed Reporting
Solution Architecture

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.

Warehouse tables registered as read-only object types behind the platform CRUD surface
Publish this elsewhere:

The Project

You already have the numbers. They live in Snowflake or Databricks, they are already modelled, and nobody wants a second copy. What you lack is a way to put them in front of people with the right access controls, alongside an application’s operational records, without writing a query service and an auth layer first.
This shape registers warehouse tables as read-only object types in your namespace, served through the ordinary CRUD surface — the same roles, tenancy and query syntax as anything you built.

⚠️ This is not a BI product

Supero ships no analytics or BI application archetype. There is no dashboard builder and no semantic layer. Metric cards and charts are a feature of every generated application, not a product of their own. What this page gives you is governed access to warehouse data from inside an application — not a replacement for your BI tool.
The exemplar is 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

Nothing here extends a service base type. A connector-backed schema is defined by what the connector stamped on it, and the bundle lists them in a module constant so nothing is inferred:
python
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"}]}
The fields that matter are the ones the connector added, not the ones you wrote:
Field on the schemaWhat it means
schema_source: "connector"This type came from a registered source, not your app model. App-authored schemas carry "app" instead.
read_only: trueWrites 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_streamThe table on the far side. The schema is access_role; the Databricks table is roles.
source_details.db_typeWhere the database type lives. Not a top-level db_type, not connector_type.
The two schemas that are not connector-backed are 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. 1

    Register the source

    The discriminator is source.plugin — http_api, csv_file, database or webhook. A warehouse is database, and the engine goes at source.config.db_type: databricks or snowflake.

  2. 2

    Name the tables

    table_names is always a list. There is no singular table_name, even for one table.

  3. 3

    Choose a mode per table

    sync copies rows in; live_read and live_readwrite leave them where they are. Live reads work on the database and http_api plugins; live writes are database only.

  4. 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. 5

    Read them like anything else

    Through /query, /list-bulk and /aggregate on the normal CRUD path /api/v1/crud/{domain}/{type}. There is no live-specific endpoint to learn.

  6. 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.

Pagination is cursor-based: 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

There is no Salesforce exemplar anywhere in the test corpus — the bundle named 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

Two kinds of work. First you register the warehouse tables and put a surface over them; then you decide what each caller may see and how fresh it must be.

Model it

  1. 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, /query and /aggregate API (see Connect Your Database). Then generate a UI over them in App Studio, or consume the API from your own front-end.

  2. 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

The trap that costs people an afternoon. When a live connector read returns HTTP 200 with an empty result set, the likely explanation is that the read fell back to the empty local store — not that the warehouse has no matching rows. A healthy-looking 200 is not evidence the source was reached. Check 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

The control plane exposes 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.
DecisionWhy it is yours
Which tables to registerThe whole warehouse gives you hundreds of object types and a navigation problem. Pick the tables a person will open.
sync versus live_readLive is current and costs a round trip per read. Sync is fast and stale. Neither is the default answer.
Freshness expectationsOn 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 dataThe two live in different stores. Combining them for a screen is application work.
Field-level exposureWarehouse 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

The same read path turns a warehouse into a customer-facing analytics portal: each customer is a tenant, so their reports are their rows and no query of theirs can reach another customer's. Layer a role with hidden fields on top and cost or PII columns disappear for the external audience — including through aggregates, per the field-exposure note above. Two things carry this and both fail quietly if wrong, so make them the thing you test: the per-customer row scoping (assert a customer sees only their own rows, under their own role, not an admin token), and the role you mint the customer's API key against (a key resolves to a role from its address, and a tenant-scoped key defaults to full read — attach an explicit read-only access policy rather than assuming the key is limited). See Access Policies and Building Multi-Tenant Apps.
Source credentials belong in the keystore, not in a schema or a config file — see Connector Security. For the complete set, see Connectors Overview.

Deployment

The path from this shape to a running reporting app or customer portal at a public URL, end to end:
  1. 1

    Preview while you iterate

    A preview deploy answers at https://preview-{domain}-{project}.supero.live for a short lease while you build and test. Publish Your App covers how preview and live differ.

  2. 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 a 202 and a poll URL, then the live address. The full contract is in Publish Your App.

  3. 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/logs is a filtered read, and a single request traces across services at /api/v1/logs/trace/{correlation_id} when you set your own X-Correlation-ID. See Operations & Observability.

Final Thoughts

This shape fits governed access to warehouse data from inside an application — your numbers in front of the right people, under the same roles and tenancy as everything else. It is not a BI product: there is no dashboard builder and no semantic layer, so if what you need is analytics tooling rather than governed reads, reach for that instead.
The one trap to carry into the build: a 200 with zero rows is not an empty source. A live read that falls back to the empty local store looks exactly like a warehouse with no matching rows — so check 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.
What you own is the choice of tables, the 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.