A live ops console on the PostgreSQL you already run
Point Supero at a database you already run, and get a governed application over it — without copying a single row.
Most internal tools die on the same question: who gets a copy of the data? Supero's live mode removes the question — queries are compiled with your access rules and run on your database, and no rows are stored on the Supero side.
The tool nobody is allowed to build
Operations needs a console over the production order database: search, filter, drill into a record, see counts by status, and hand a read-only view to the support team. Everyone agrees it should exist. It never gets built, because the first design review asks where the data will live, and the answer — "we ETL it into a second store" — turns a two-day tool into a quarter-long data project with its own security review, its own staleness bugs and its own copy of your customers' personal data.
Supero's live mode sidesteps the argument. A connector is a governed link, not a pipeline: your query is compiled with the caller's tenant and role rules applied, then executed against your database. Rows come back, get rendered, and are never stored on the Supero side.
Three modes, chosen per table
Mode is a per-table decision, not a per-connector one. One database connector can sync a small reference table into Supero for speed while reading your 400-million-row events table live.
| Mode | Storage in Supero | Reads | Writes |
|---|---|---|---|
sync (default) | A copy, refreshed on a schedule, webhook or manual trigger | Served from the local copy | Full CRUD against the copy, synced back on the next run |
live_read | None | Real time, from the source, on every request | Blocked — writes return 403 |
live_readwrite | None | Real time, from the source | Passed through to the source in real time |
live_readis available for PostgreSQL, MySQL and SQL Server. Write-through (live_readwrite) is implemented for PostgreSQL and MySQL only — a SQL Server write returns 501.- Warehouses (Snowflake, Databricks, ClickHouse, BigQuery, Redshift, Microsoft Fabric) are always
live_read— there is no write-back path for an analytical source. - HTTP-API sources support
live_read(served by a REST planner rather than SQL), but not write-through. File and webhook sources are sync-only. - Oracle and MongoDB connect in
syncmode.
What happens when someone opens the list
Two properties in that diagram are worth naming, because they are what makes this safe enough to hand to a support team:
The predicate is not optional
Tenant and role scoping is compiled into the query before it is sent. It is not a filter your app remembers to add — there is no code path that omits it.
It fails closed
A request that cannot resolve a tenant scope is refused. The failure mode of a missing rule is "no data", never "all data".
Hidden fields are rejected, not filtered
A query that references a field the caller may not see is refused at the boundary, rather than running and stripping the column afterwards.
It behaves like native data
Same CRUD and query API, same pagination shape, same SDKs — so your screens, your typed clients and the agent all work unchanged. The differences that remain are honest ones your UI can read: an access_mode stamp, and a flag when a scan was truncated.
How to build it on Supero
Create a read-only database user
Do this first and the rest of the review goes quietly. Give Supero a user that can do exactly what the console needs and nothing more.
CREATE ROLE supero_reader LOGIN PASSWORD '••••••••';
GRANT CONNECT ON DATABASE prod TO supero_reader;
GRANT USAGE ON SCHEMA public TO supero_reader;
GRANT SELECT ON public.orders, public.order_items, public.customers TO supero_reader;
-- no INSERT, no UPDATE, no DELETE, no other tables⏱ ~10 minutes
Connect the source
A database connector needs the reachability details and the credential. Credentials are stored as secrets and returned masked — they never enter your app bundle.
{
"plugin": "database",
"config": {
"db_type": "postgresql",
"host": "orders-db.internal.example.com",
"port": 5432,
"database": "prod",
"schema": "public",
"username": "supero_reader",
"password": "REDACTED",
"ssl_mode": "require",
"query_type": "table",
"table_names": ["orders"],
"batch_size": 1000
}
}Table selection is always a list under table_names; there is no singular table_name. To expose a joined, pre-narrowed slice of a messy schema, create a database VIEW and bind that — the live planner addresses a named relation. (query_type: "query", a hand-written SQL source, applies to sync mode only.)
⏱ ~10 minutes
Discover and bind
Discovery introspects the source and proposes schemas. Binding maps a source table onto a Supero schema, which is the moment the data becomes ordinary records: typed fields, references, pagination, aggregates, SDKs and the AI agent all start working.
From an AI editor over MCP the same path is six tools: build_list_data_sources → build_connect_data_source → build_discover_source → build_bind_data_source → build_run_data_source → build_list_bound_schemas.
⏱ ~20 minutes
Set each table's mode
Set orders, order_items and customers to live_read. Supero stamps a connector-level access_mode summarising the result — live-ro here — which your UI can read to decide whether to render write controls at all. When a connector mixes modes, the most permissive one wins, so check what it reports rather than assuming.
| access_mode | Means |
|---|---|
sync | Full CRUD against Supero's own copy |
live-ro | Read-only, live from the source; writes rejected |
live-rw | Read and write, live, straight through |
warehouse | Read-only analytical queries against a warehouse |
⏱ ~5 minutes
Apply roles and hide the columns that should not travel
This is the step that makes it a product rather than a database viewer. Live data honours the same policy model as native data — roles, tenant isolation and field hiding all apply.
POLICIES = [
PolicyDef(role="tenant_admin", default_access="full", rules=[]),
# Support: read orders and customers, never payment identifiers.
PolicyDef(role="viewer", default_access="none", rules=[
PolicyRule(entity="orders", can_read=True),
PolicyRule(entity="order_items", can_read=True),
PolicyRule(entity="customers", can_read=True,
hidden_fields=["payment_token", "card_last4", "tax_id"]),
]),
]Then verify it the only way that counts: sign in as viewer and try to sort or filter by card_last4. A refusal is the correct answer — the query never reaches your database. Hidden columns are also stripped from responses, but the refusal is the part that matters: a filter on a hidden column would otherwise leak its value through the result count.
⏱ ~30 minutes
Build the console on top
From here it is an ordinary Supero app. Generate the screens from the bound schemas, or hand-build the UI against the same CRUD API. Aggregates, group-by counts and distinct-value lookups run on your source, so a status breakdown across a very large table is a query your database is already good at.
The in-app AI agent comes along for free: it derives search_/get_ tools per bound entity plus shared analytics tools (db_stats, db_count_by, db_distinct, db_aggregate), all going through the same access engine. "How many held orders per region this week?" is answered by your database, under the asker's own permissions.
⏱ ~45 minutes
When live is the wrong choice
Live mode is not universally better than sync, and an article that pretended otherwise would be selling you something. Choose deliberately:
Choose live_read when | Choose sync when |
|---|---|
| Data residency or policy forbids a copy | The source is slow, flaky, or occasionally offline |
| Freshness matters — the console must show current state | You need full CRUD, history, or write-back through Supero |
| The table is too large to replicate sensibly | You want the app to keep working when the source does not |
| The source is the system of record and must stay so | The source is a warehouse export you own anyway |
Time to deliver
| Phase | On Supero | Conventional internal tool |
|---|---|---|
| Read-only DB user + network path | ~10 min | ~10 min (same either way) |
| Getting the data somewhere queryable | 0 — it stays where it is | 2–6 weeks of pipeline + a second store |
| Typed API over the tables | ~20 min (bind) | 1–2 weeks |
| Auth, roles, column-level hiding | ~30 min of policy | 2–3 weeks |
| List/detail/search/aggregate UI | ~45 min | 2–3 weeks |
| Security review | Short — nothing was copied | Usually the long pole |
| Total | ~2 hours | ~1–2 weeks with an internal-tool builder over a copy; ~2 months if you build the pipeline too |
The fastest data migration is the one you skip
The reason internal tools take a quarter is rarely the tool. It is the copy — the pipeline, the second store, the staleness, the review, the extra place your customer data now lives. Removing the copy removes most of the project.
What you keep is the part you wanted: a typed API, real access control, a working UI, and an AI agent over data that never left your database. The next article does the same thing to a warehouse, where "do not copy this" is not a preference but a cost model.
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.