Use casesData teamsFiles as an API08 / 12
How it fits together

A bucket of files, served as an API, with no copy in between

A DuckDB connection lists the folders you allow and makes each one a view, with partitions as a column. Every read is checked twice: by DuckDB against the paths you listed, and by your storage against the credentials you gave it. Apps run saved queries, analysts write SQL over the views, and nobody, not even the admin key, reads outside those paths.

0copies of the data
1view per folder, partitions as a column
2locks on every read
Who asks
The governed core
DuckDB, in the server's process
Your bucket
↗ Finance dashboard queries: daily_events 200 · X-Cache: HIT
↗ An analyst allowed_tables: events 200 · application/vnd.apache.parquet
Even the admin key admin 403 path_not_allowed
The governed core↗
GET /q/daily_events · POST /execute_sql

01 · Grant

A saved query, or SQL over named views. Never a file function.

admin ✓ 403 table_not_allowed

02 · Cache

Masking as for any database. Cached, the files are read once per TTL.

not cached X-Cache: HIT

03 · Format

JSON, CSV, Parquet and five more. Streamed Parquet in steady memory.

json parquet · stream
The connection ↗ "db": "duckdb", "database": ":memory:", "auto_views": true, "allowed_paths": ["s3://acme-lake/events/", "s3://acme-lake/dim/"], "user": "${LAKE_ACCESS_KEY}", "password": "${LAKE_SECRET}"
↗

DuckDBStable

lake · no tables of its own Views, made as it opens events ← events/date=*/*.parquet + date customers ← dim/customers.parquet regions ← dim/regions.csv Lock 1 · allowed_paths s3://acme-lake/events/
s3://acme-lake/dim/
Enforced by DuckDB, locked as the connection opens: a query can't change it back. 403 path_not_allowed

Your bucket

s3://acme-lake/
events/ date=2026-10-08/part-0.parquet date=2026-10-09/part-0.parquet date=2026-10-10/part-0.parquet
dim/ customers.parquet regions.csv
hr/salaries.parquet Not in allowed_paths, nor in the credentials' policy. Lock 2 · your bucket policy credentials read events/ and dim/ only
What can be readchecked by grant, then by DuckDB
analyst SELECT date, count(*) FROM events GROUP BY 1200 · 7 rows analyst SELECT * FROM read_parquet('s3://acme-lake/events/…')403 table_not_allowed admin SELECT * FROM read_parquet('s3://acme-lake/hr/salaries.parquet')403 path_not_allowed
Stablecompatibility promise Each read is requests and egress on your bucket: cache what's polled.
Who asksAn app, an analyst, even the adminApps run saved queries; analysts write SQL over the views, never over file paths.
The governed coreGrant → cache → formatGrants and masking as for any database. Cached, the files are read once per TTL.
DuckDBFolders become viewsListed as the connection opens; date=…/ partitions become one view with a date column.
Lock 1, allowed_pathsNothing else can be readEnforced by DuckDB, even for the admin key: 403 path_not_allowed.
Your bucket, behind lock 2Read where the files are, no copyGive the connection credentials that read only those prefixes.

The same governed core as the full architecture.

For data teams

Turn a bucket of Parquet into a queryable API

The lake has the data; the people who need it have a browser and curl. A DuckDB connection pointed at a prefix makes each file a view, locked to the paths you list, with no copy and no warehouse.

✓Parquet, CSV and JSON on S3, GCS, R2, HTTPS or disk. Hive-style key=value/ partitions become one view.
✓Nothing outside allowed_paths can be read, enforced by DuckDB itself as the connection opens.
✓Saved queries over the views get grants, masking and caching like any other.
✓Results back out as Parquet too, in steady memory at any size.
Files as an API
# a connection of nothing but files
curl -X POST https://data.example.com/api/v1/connections \
  -H 'X-API-Key: <admin>' -H 'Content-Type: application/json' -d '{
  "name": "lake",
  "db": "duckdb",
  "database": ":memory:",
  "active": true,
  "allowed_paths": ["s3://acme-lake/events/", "s3://acme-lake/dim/"],
  "auto_views": true,
  "user": "${LAKE_ACCESS_KEY}", "password": "${LAKE_SECRET}", "region": "eu-west-1"
}'

# events/date=2026-10-10/*.parquet is now a table called events
SELECT date, count(*) FROM events GROUP BY 1 ORDER BY 1 DESC LIMIT 7
More use casesAll twelve →

Not sure which fits?

The example APIs ship with a generated database and one scoped key per scenario, so you can try each of these in a few minutes without connecting anything of your own.

Run the examplesCompare alternatives