Row- and column-level security, enforced adversarially.
Rowfence is a policy-driven proxy that sits in front of any VGI worker and decides, on every single query, which rows and columns a caller is allowed to see — without asking the worker to police itself. Part of the VGI suite; a Query Farm product, hosted or on your own infrastructure.
Not a config flag
A dedicated hop between DuckDB and the worker
A VGI worker doesn't have to implement access control itself to get it. Point
ATTACH at Rowfence instead of the worker directly, and
every query — every row, every column, every function call — passes through Cedar policy
evaluation before the worker ever sees it. There's no passthrough mode: enforcement is
always on.
Hosted
Query Farm runs the proxy for you, in front of a worker you already run yourself or one hosted on Orchard.
On-prem
Run it inside your own network, in front of a worker that never has to leave your infrastructure either.
Rowfence is a Query Farm product, sold as part of the VGI suite — it is not open source, and there's no repository to point at. What follows describes how it behaves once it's running, not how to build one yourself; for pricing framing and the pitch, see the Rowfence product page.
The policy language
Cedar policies, not a bespoke DSL
Access control is expressed in Cedar
— the same policy language AWS built for Verified Permissions — against an entity model
that mirrors a catalog exactly: a server contains catalogs, a catalog contains schemas, a
schema contains tables, and a table contains columns, views, functions, and macros. Users
belong to groups, and every policy is a plain permit or
forbid statement over that hierarchy.
permit(
principal in Vgi::Group::"analysts",
action == Vgi::Action::"Read",
resource in Vgi::Table::"main.customers"
);
forbid(
principal,
action == Vgi::Action::"Read",
resource
)
when { resource.pii == true }
unless { principal in Vgi::Group::"compliance" };
forbid(principal, action, resource)
when { context.schema == "finance" }
unless { principal in Vgi::Group::"finance-managers" };
Nothing is allowed by default. A caller with no matching permit
sees nothing — the policy set has to grant access explicitly, not merely fail to deny it.
Same table, different rows
Row-level filtering
Row rules are ordered branches, each with an optional Cedar condition and a scope pattern
that matches table names — exact, a prefix, a suffix, or everything. A matching branch can
grant every row, none of them, or inject a WHERE expression built
from the caller's own identity: ${principal} and
${claim.*} render as escaped SQL literals — never raw string
interpolation — and ${entitlement.*} resolves to a semi-join
against a key set fetched from outside the query itself.
# row-filters.yaml
branches:
- scope: "earthquakes"
when: 'context.claim.region == "us-west"'
expr: "region = ${claim.region}"
- scope: "earthquakes"
when: "principal in Vgi::Group::\"internal\""
grant: all
- scope: "*"
grant: none # fail closed — no matching branch, zero rows
The earthquakes-by-region example from
Data as a Service
is exactly this pattern: one earthquakes table, one policy set,
a different WHERE region = ... injected per subscriber. Fail
closed is the default, not an opt-in — a query against a table with no matching branch
returns zero rows rather than everything.
Beyond static values
Composing dynamic filters
${entitlement.*} isn't limited to a static lookup table —
an entitlement is anything resolved from outside the query, and "outside the query" can
mean another VGI worker just as easily as a database. Say you sell property data by state
and want a caller scoped to California limited to rows that actually fall inside its
border — not a bounding box, its real irregular outline. Overture Maps publishes exactly
that boundary as a worker-queryable polygon:
-- How the proxy resolves ${entitlement.ca_boundary} — a normal VGI query,
-- run by the proxy itself, against the same worker any caller could attach to
ATTACH 'overture' (TYPE vgi, LOCATION 'https://vgi-overture.rusty-bb6.workers.dev');
SELECT geometry FROM overture.divisions.division_area
WHERE names.primary = 'California' AND subtype = 'region';
That's an ordinary ATTACH and an ordinary query — the proxy is
itself a VGI client here, reaching Overture the same way any caller's session would. A
real administrative polygon is expensive enough to fetch that doing it on every request
would be wasteful, so the proxy caches the resolved geometry and reuses it until the
entitlement is next refreshed — the same caching discipline VGI already applies to worker
results generally. The row rule itself just references the cached value:
# row-filters.yaml — filtered against a real polygon, not a bounding box
branches:
- scope: "properties"
when: 'context.claim.region == "california"'
expr: "ST_Within(location, ${entitlement.ca_boundary})"
- scope: "*"
grant: none
Nothing about this is a special case in the proxy — it's the same
${entitlement.*} mechanism as a subscriber allow-list, just
pointed at a geometry instead of a set of IDs. Any VGI worker can sit on either side of
that composition: the one being filtered, and the one supplying what to filter by.
Same table, different columns
Column-level enforcement
A forbidden column doesn't just get filtered out of the results — it's removed from the
schema DuckDB is told about in the first place, at bind time, so there's no column to
select in the first place, not just an empty one. Columns are marked sensitive either
through Vgi::Column policy entities or through Arrow field
metadata tags the worker itself sets (pii,
sensitive, classification,
confidential, restricted,
secret). If every column in a table ends up forbidden for a
given caller, the whole table is denied rather than returned empty — and column-level
statistics, which can leak distributional information even without returning rows, are
denied outright for any table under a column policy.
Not just tables
Function-level access
A VGI worker isn't only a catalog of tables — it can expose scalar, table, and aggregate
functions too, and some of those are worth gating on their own: an LLM-backed function, a
geocoder, a scoring model, anything with a real cost or a licensing meter behind each call.
The same entity model that secures a table secures a function — Vgi::Function
is a resource, Execute is the action, and
permit / forbid work exactly as they do
everywhere else on this page.
forbid(
principal,
action == Vgi::Action::"Execute",
resource in Vgi::Function::"geocode"
)
unless { principal in Vgi::Group::"premium-tier" };
This is the enforcement side of compute as a product: a function call is a discrete, priceable unit precisely because it's also a thing you can say no to.
Not a thin passthrough
It doesn't trust the worker
The proxy never takes a worker's word for its own schema — it discovers the real schema itself, using an embedded DuckDB engine, and enforces policy against that discovery rather than whatever the worker advertises. For scans that go through a function rather than a native table, results are strictly whitelist-filtered after the fact: if a worker leaks an extra column it wasn't supposed to, the proxy drops it anyway rather than trusting the worker to have behaved. Every error path in the enforcement logic denies by default; none of them fall through to “allow.”
That work isn't free, and it isn't pretended to be — but it's tuned, not brute-forced: peak memory during result post-filtering runs at roughly a quarter of the raw payload size, and sixteen concurrent clients hold around 140MB of resident memory in the proxy itself.
Who's asking
Identity
The proxy accepts a caller's identity as a static bearer token, a verified JWT, or by
forwarding whatever the upstream connection already carries — and when it verifies OAuth
tokens, it does so using the same underlying library the core VGI extension's own
OAuth support
is built on, not a second, independent implementation. Verified claims are exposed to
policies as context.claim.<name> — the same claims a
$${claim.*} row rule reads from.
Running it
Hosted or on-prem
Query Farm sells this two ways: hosted, running alongside a worker on Orchard or in front of one you run yourself, or on-prem, deployed entirely inside your own network so the proxy — and the identity claims and query plans that pass through it — never leave your infrastructure. Both shapes support a single worker behind stateless, load-balanced replicas, and a multi-tenant gateway mode that routes by tenant and worker to many different backends behind one proxy — the shape for a vendor brokering access to more than one downstream source.
Every decision is logged — JSONL, one line per event, covering authentication, every row filter injected, every column projection denied, every entitlement resolved (as counts and hashes, never the underlying values) — and an optional decision journal makes those decisions replayable offline, so “why did this query see these rows” has an answer that doesn't depend on remembering.
That log is also the foundation for something not built yet: a compliance bundle — canned access-review reports, entitlement-change history, decision-replay summaries — aimed at the audits that currently mean someone hand-assembling spreadsheets. It's on the roadmap, not shipped; if it'd help your audit cycle, tell us what it needs to cover.
Where it draws the line
What it's honest about
Not everything a worker can do has an obvious securable anchor. A transform function — one that takes a table in and returns a table out — has no single resource the proxy can check a row or column policy against, so those calls are forwarded uninspected rather than silently broken; they're still fully audited, tagged distinctly from an enforced scan, so it's visible in the log that enforcement didn't apply rather than pretending it did. Native-source interception (the fast path that rewrites a scan before it reaches the worker) only covers reads, and only for the native scan types it knows about.
In context
How this compares
Row- and column-level security itself isn't the differentiator — Immuta, Satori, and others in that category already do it well. What's different is what it's enforced in front of, and how it's deployed.
Warehouse-native governance platforms
Immuta, Satori, and similar tools connect to a fixed list of warehouses and databases — Snowflake, Databricks, BigQuery, and the like — through native integrations, and are primarily sold as SaaS.
This proxy
Sits in front of any VGI worker — a live API, a custom service, a warehouse, anything the protocol reaches — because enforcement happens at the protocol layer, not against a connector list. Hosted or fully on-prem, your choice.
A proprietary policy builder
Most platforms in this category express rules through their own UI-driven builder or a vendor-specific rule format, portable only within that product.
Cedar
The same open policy language AWS built for Verified Permissions — not invented for this page. permit/forbid over principals, actions, and resources, not a bespoke syntax.
Questions
FAQ
Is this open source?
No — unlike the VGI extension and its worker SDKs, Rowfence is a Query Farm product we sell hosted or on-prem. There's no repository to self-host from.
Does the worker behind it need to change?
No — it's a standard VGI worker, unaware it's being proxied. Column-sensitivity tags are the one optional exception: a worker can flag its own columns via Arrow field metadata, but policy entities alone are enough without any worker changes at all.
Can it sit in front of a worker on Orchard?
Yes — the proxy doesn't care whether the worker behind it is self-hosted or running on Orchard; either way, it's the same ATTACH pointed at the proxy instead of the worker directly.
How does this relate to VGI's own OAuth support?
It builds on it rather than replacing it — the proxy's JWT verification shares the same underlying library the core extension uses for OAuth, so a caller authenticated any of the ways described on the Authentication page carries an identity the proxy can already read.
Keep going
Where to next
Rowfence, the product
Pricing framing, deployment options, and how it compares — the product page, not the mechanism docs.
Authentication
Bearer tokens and OAuth — how a caller’s identity reaches Rowfence in the first place.
Data-as-a-Service
The broader case for full SQL access instead of a copy of your data.