Direct Data API or Governed Business Query API? Supabase and TeaQL Solve Different Boundaries
Supabase gives applications a direct Data API. TeaQL gives applications and AI agents a governed business query API.
The two ideas can produce a similar developer experience: a frontend composes a query instead of waiting for a backend team to add one more endpoint. But they place the public boundary at different layers of the system.
Supabase reflects a PostgreSQL schema into an API. TeaQL exposes capabilities from a shared business model and lets a trusted runtime decide how those capabilities are executed. That difference becomes important when an application has complex authorization, several backend languages, multiple data sources, or AI agents that need composition without database authority.
First, Supabase solves a real problem well
Supabase's official documentation describes its Data API as an API generated directly from the database schema. It can be called from a browser, uses PostgREST, and works with PostgreSQL grants and Row Level Security. Supabase supports both a two-tier architecture, where the browser accesses the Data API, and a three-tier architecture with an application server.
That is a compelling model for many products:
const {data, error} = await supabase
.from('orders')
.select('id,status,total_amount')
.eq('status', 'NEW')
.order('id', {ascending: false})
.limit(20)
The application gets a useful query surface quickly. PostgreSQL remains the execution engine, while grants and RLS determine which objects and rows a role may access. Supabase explicitly recommends RLS and least-privilege grants for frontend access and warns that service-role credentials must never be exposed in the browser.
Those are strengths, not shortcomings. TeaQL is not trying to reproduce a PostgreSQL platform, authentication product, storage service, or hosted backend. It starts from a different question:
What should the query boundary look like when the public contract is the business model rather than the database schema?
Similar experience, different authority
A TeaQL TypeScript application normally composes the query through the
model-generated Q API:
const orders: SmartList<CustomerOrder> = await Q.customerOrders()
.withOrderNumberStartingWith('WEB-')
.withTotalAmountGreaterThanOrEqualTo(1000)
.selectOrderNumber()
.selectTotalAmount()
.orderByIdDescending()
.limit(20)
.comment('what: load high-value active orders')
.purpose('why: render the operations queue')
.executeForList(context)
customerOrders, withOrderNumberStartingWith, selectTotalAmount, and the
returned CustomerOrder are generated from the model. They are not strings
invented by the application. Rename or remove a modeled field and this code
stops compiling. Editor completion also exposes only the operations generated
for that field type.
The generated request builds TeaQL's internal typed query model. At a federal boundary it is serialized into a canonical TeaQL Federal Protocol request; the application does not need to construct that string-keyed wire envelope itself. The browser transmits neither SQL nor a runtime-native object. The receiving application maps the public entity and field vocabulary to capabilities that its trusted policy permits.
TypeScript query
│
▼
TeaQL Federal Protocol
│
▼
entity / field / operator / limit policy
│
▼
tenant + actor + purpose context
│
▼
Java or Rust TeaQL runtime
│
▼
configured data services
The same TypeScript query can be executed by a Java backend or a Rust backend. The backend may use PostgreSQL, SQLite, another supported provider, or a composed data-service topology. The frontend contract does not become a raw view of any one physical schema.
A business model is not just a nicer table name
Database tables are an implementation model. A governed application needs additional concepts:
- which entities and fields this caller may query;
- which operators are allowed for each exposed field;
- tenant and regional boundaries injected by the server;
- required query limits and stable ordering;
- loaded, null, and not-loaded object state;
- comments describing what the query does;
- purpose describing why it is being executed;
- audit and telemetry that remain independent from business payloads;
- consistent semantics when a service moves between runtime languages.
Some of these rules can be implemented with PostgreSQL RLS, views, functions, grants, and API-server code. TeaQL's proposition is not that they are impossible elsewhere. It is that they should form one generated, testable runtime contract rather than being reconstructed independently in every application layer.
This is especially relevant to AI agents. An agent can productively combine approved predicates, projections, relations, and facets. It should not receive the authority to invent SQL against an internal schema or to omit the tenant condition because it did not appear in the prompt.
One model generates both sides; only the trusted side owns policy
TeaQL starts with one KSML business model. From that model, the generator can produce both sides of the interaction:
one KSML model
│
┌──────────────┴──────────────┐
▼ ▼
generated client API generated server runtime
Q.customerOrders() entities and metadata
typed predicates query execution
typed projections checker and fix lifecycle
typed result objects mutation and audit lifecycle
│ │
│ application customization
│ tenant and actor policy
│ workflow authorization
└──────── TFP ────────────────┘
The client is intentionally close to the generated model. A frontend developer
normally consumes the generated Q API directly. The client does not need a
copy of the organization's authorization implementation, and it must not
receive server secrets or trusted policy state.
The server is generated from the same model but is designed to be customized. An application installs its runtime module, checkers, fixes, behaviors, trusted field mappings, and authorization rules there. The server may expose only a subset of the generated model through TFP. It may also add tenant conditions or reject an operation even when the client constructed a perfectly valid typed query.
This separation is important: type-safe means structurally valid; it does not mean authorized. Generated client code prevents accidental field and operator mistakes. The trusted server decides whether this actor may perform this query or mutation in the current business context.
Because both sides originate from the same model, they share entity, field, relationship, nullability, and query semantics without sharing authority. The client can remain mostly generated and immediately usable, while the server can be deeply adapted to the organization.
Tenant isolation is only the beginning
Consider a procurement application used by many companies. Every contract belongs to a tenant. A user from tenant A must never read or mutate a contract from tenant B—even if a request explicitly supplies tenant B's identifier.
TeaQL treats the tenant as trusted context rather than ordinary client input:
browser request: contracts over 1,000,000
authenticated context: tenant=A, actor=alice, roles=[buyer]
effective server query:
requested business predicates
AND tenant = context.tenant
AND fields/operators allowed by server policy
AND runtime execution limits
The frontend can compose the useful part of the question, but it cannot remove or replace the server-injected tenant boundary. The same rule applies whether the query is executed by the Java runtime today or a Rust service after a later migration.
Now add a common enterprise authorization rule:
A contract up to 1,000,000 may follow the normal approval path. A contract above 1,000,000 must be approved by a second authorized person, and the second approver cannot be the person who submitted it.
This is not merely “can this role update this row?” The decision depends on:
- the tenant of the contract and the actor;
- the contract amount and currency;
- the current workflow state;
- the actor's approval authority;
- prior mutation history;
- separation of duties between submitter and approver;
- possibly regional or legal-entity rules.
In TeaQL, the generated mutation enters the server runtime with an authenticated
context. Server-side checker/behavior policy can reject the transition, or move
the contract into a PENDING_SECOND_APPROVAL state and record the decision in
the mutation and audit lifecycle. The client still uses the generated contract
API; it does not duplicate the threshold rule or decide that its own request is
authorized.
PostgreSQL and Supabase can implement sophisticated rules using RLS, grants, views, triggers, database functions, or a custom application server. The point is not that a million-unit approval is impossible there. The architectural question is where that evolving business workflow lives. Once an application needs value-dependent approval, separation of duties, audit reasons, several services, and multiple runtime languages, a table-reflected API alone is no longer the complete business boundary.
TeaQL keeps the public query composable while placing these decisions in a model-aware server runtime. That is what “governed” adds to “direct.”
Facets show why this boundary matters
Suppose an order page needs both filtered orders and status counts. The client can request a relation facet:
const statusValues = Q.orderStatuses()
.selectCode()
.selectLabel()
.countAs('orderCount')
.comment('what: load status facet values')
.purpose('why: render order filters')
const orders = await Q.customerOrders()
.withTotalAmountGreaterThanOrEqualTo(1000)
.selectOrderNumber()
.limit(20)
.comment('what: load orders with their status facet')
.purpose('why: render the governed order list')
.facetByStatusAs('statusFacet', statusValues, true)
.executeForList(context)
const statusFacet = orders.facet('statusFacet')
The server retains the outer filter when calculating membership counts. It validates the relation, nested fields, aggregate, page limit, comment, and purpose. It can return every allowed status—including a zero-count value—or only matched values according to the explicit option.
This is still composable frontend querying, but the expensive operation is not defined solely by whatever expression arrives from the browser. Its shape is a bounded protocol capability.
TeaQL currently retains executable conformance for this path with a TypeScript client against both Java and Rust endpoints. The same fixture verifies the result and negative policy cases instead of treating a successful HTTP response as sufficient evidence.
Where each approach fits
The useful comparison is not “which product has more features?” It is “where should authority live for this application?”
| Requirement | Direct Data API is attractive | Governed business query API is attractive |
|---|---|---|
| Deliver CRUD over PostgreSQL quickly | Yes | Possible, but not its main differentiation |
| Browser queries closely follow tables/views | Yes | Usually intentionally abstracted |
| PostgreSQL RLS is the primary policy layer | Yes | Can coexist, but runtime policy is also explicit |
| Java and Rust services share one query contract | Requires an application-level design | A core TeaQL objective |
| Backend may migrate between languages | Public contract may need coordination | TFP keeps the client query stable |
| AI composes new business questions | Requires carefully designed tools and policy | The query model is designed as the governed tool boundary |
| Cross-data-source relations | Application-specific | Part of the runtime model |
| Query purpose and diagnostic comment are required | Application-specific | Part of the query contract |
For a small PostgreSQL application that wants a backend immediately, a direct Data API may be exactly the right abstraction. For a domain with substantial business policy, several runtimes, gradual Java-to-Rust migration, or agentic query composition, binding the public query contract directly to storage can become limiting.
The goal is not unrestricted frontend querying
“Let the frontend query” can sound like moving backend authority into the browser. That is not the TeaQL design.
The client controls composition only within a capability vocabulary. The server owns identity, tenant scope, allowed entities, field mappings, operators, limits, execution, and error classification. Unknown fields and unsupported operators fail closed. Internal database errors and mappings do not become the public protocol.
That produces a useful middle ground:
fixed endpoints governed composition raw SQL
low flexibility <------------------------------------> high authority
TeaQL aims here
Applications and agents gain enough freedom to answer questions that were not pre-packaged as dedicated endpoints. The backend keeps enough authority to make those questions safe, explainable, portable, and testable.
One sentence to remember
Supabase gives applications a direct Data API. TeaQL gives applications and AI agents a governed business query API.
They address neighboring needs. The distinction is the layer being exposed: the database as an API, or the governed business model as an API.
Further reading:
