> ## Documentation Index
> Fetch the complete documentation index at: https://oxy.tech/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> How Airhouse works: components, the life of a query, multi-tenancy, consistency, where your data lives, and deployment topologies.

Airhouse is a server layer on top of two open technologies: [DuckDB](https://duckdb.org) runs your queries, and [DuckLake](https://ducklake.select) stores your tables. Airhouse adds the Postgres wire protocol, authentication, roles, multi-tenancy, resource limits, and an Admin API. It doesn't have its own query engine, SQL parser, or storage format.

## Key concepts

<Accordion title="Database">
  An isolated Airhouse workspace with its own catalog and its own storage location. You pick the database with the standard Postgres `database` connection parameter (for example `psql -d demo`). Users are created per database, and a user can only connect to the database it belongs to.
</Accordion>

<Accordion title="The lake catalog">
  Inside every session, your persistent tables live in the DuckLake catalog named `lake`. Each session starts in `lake` with the default schema `main`, so `orders` and the fully qualified `lake.main.orders` refer to the same table. Objects created outside `lake` (for example in DuckDB's in-memory `memory` or `temp` catalogs) are session-local and disappear when you disconnect.
</Accordion>

<Accordion title="Roles">
  Every user has exactly one role: `reader` (queries only), `writer` (queries and data changes), or `admin` (everything, including creating and dropping tables). See [Users, roles, and security](../guides/users-and-roles).
</Accordion>

<Accordion title="Service accounts">
  For applications that serve many end users, a service account mints short-lived database credentials on demand, so your platform never stores a long-lived password per user. See [Service accounts](../guides/service-accounts).
</Accordion>

## Components

```mermaid theme={null}
flowchart TB
    clients["Postgres clients<br/>psql, JDBC, psycopg2, BI tools"]
    backend["Your backend<br/>(provisioning, service accounts)"]

    subgraph airhouse["Airhouse"]
      dp["Data plane<br/>Postgres wire protocol :5445<br/>auth, roles, limits<br/>DuckDB + DuckLake sessions"]
      cp["Control plane<br/>Admin API :8080<br/>databases, users, service accounts"]
    end

    registry[("Registry<br/>(Postgres)")]
    catalog[("DuckLake catalogs<br/>(Postgres, one per database)")]
    s3[("Object storage<br/>(Parquet files)")]

    clients --> dp
    backend --> cp
    cp --> registry
    cp -->|creates catalogs| catalog
    dp -->|looks up users and databases| registry
    dp --> catalog
    dp --> s3
```

| Component | What it does |
| - | - |
| **Data plane** | Accepts Postgres wire-protocol connections on port `5445`. It authenticates each connection, checks the user's role on every statement, enforces resource limits, and runs queries in DuckDB with the database's DuckLake catalog attached. |
| **Control plane** | Serves the [Admin API](../reference/admin-api) on port `8080`. It creates and deletes databases, users, and service accounts, and mints short-lived credentials. |
| **Registry** | A Postgres database that records databases, users (as SCRAM verifiers), service accounts, and minted credentials. |
| **DuckLake catalogs** | One Postgres database per Airhouse database. It holds the metadata for schemas, tables, columns, data files, and snapshots. |
| **Object storage** | An S3-compatible bucket that holds the table data as Parquet files, under a separate prefix for each Airhouse database. |

The data plane and control plane are the same binary. On a single host they run in one process. On Kubernetes they run as separate Deployments so each can scale on its own.

## The life of a query

```mermaid theme={null}
sequenceDiagram
    participant C as Client
    participant DP as Airhouse data plane
    participant R as Registry
    participant K as DuckLake catalog
    participant S as Object storage

    C->>DP: Connect (database = "acme", user, SCRAM-SHA-256)
    DP->>R: Look up database and user credentials
    DP-->>C: Authenticated
    C->>DP: SELECT ... FROM lake.main.orders
    DP->>DP: Check role and limits
    DP->>K: Read table metadata and file list
    DP->>S: Read only the Parquet files and columns needed
    DP-->>C: Stream rows in Postgres wire format
```

1. **Connect.** The client opens a Postgres connection. The `database` parameter names the Airhouse database. If it doesn't exist, the connection fails with SQLSTATE `3D000`.
2. **Authenticate.** Airhouse verifies the password with SCRAM-SHA-256 against the registry. Expired or revoked credentials are rejected.
3. **Open a session.** The connection gets a DuckDB session with the database's DuckLake catalog attached as `lake`. Each connection's work is isolated from other connections.
4. **Authorize.** Every statement is checked against the user's role (`reader`, `writer`, or `admin`) and, for schema-scoped writers, against their allowed schemas. It's also checked against rate and concurrency limits. Refused statements never reach the engine.
5. **Execute.** DuckDB plans and runs the query. DuckLake uses the catalog to find which Parquet files hold the data, and DuckDB reads only the files and columns it needs.
6. **Return results.** Results stream back in the Postgres wire format, with standard command tags such as `SELECT 42` and `INSERT 0 3`.
7. **Write, if the statement changes data.** DuckLake writes new Parquet files to object storage, then commits the change to the catalog as a new snapshot.

## Multi-tenancy

An Airhouse **database** (a **tenant** in the Admin API) is the unit of isolation:

* **Separate catalog.** Each database has its own DuckLake catalog in its own Postgres database, accessed with its own Postgres role.
* **Separate storage prefix.** Each database's Parquet files live under their own prefix in the bucket.
* **Separate users.** Users and minted credentials belong to exactly one database. Connecting to another database with them fails.
* **Separate limits.** Operators can cap concurrent queries and queries per minute per database, as well as per user. See [Query limits](../reference/configuration#query-limits).

Clients choose the database with the standard Postgres `database` parameter, so every Postgres tool works without changes.

## Transactions and consistency

* **ACID transactions.** DuckLake makes every commit atomic. A statement, or a `BEGIN` … `COMMIT` block, either applies completely or not at all.
* **Snapshots.** Each commit creates a new catalog snapshot. Queries read a consistent snapshot, and you can query earlier ones with [time travel](../guides/tables-and-storage#snapshots-and-time-travel).
* **Serialized writes per database.** Airhouse commits writes to the same database one at a time. Reads never wait for writes, and writes to different databases don't affect each other.
* **Visibility.** Other connections see a change once it's committed. Each server process can also cache `SELECT` results (60 seconds by default) and clears its cache when it runs a write to that database. With several server replicas, a replica that didn't handle the write can return a cached result until the cache entry expires. If you need read-after-write across connections, lower or disable the cache with `[cache] ttl_secs` or `enabled`.

## Where your data lives

Airhouse runs in your environment, and your data stays in infrastructure you choose:

| Data | Location |
| - | - |
| Table data | Parquet files in your S3-compatible bucket |
| Table metadata and snapshots | DuckLake catalogs in your Postgres |
| Databases, users, service accounts | The registry database in your Postgres |
| Query results in flight | Memory of the Airhouse server, spilling to local disk for large queries |
| Cached query results | Memory of the Airhouse server, for the cache TTL |

Because the data is standard Parquet and the catalog is standard DuckLake, it stays readable with DuckDB and DuckLake tooling. For encryption at rest, use your object storage's and Postgres' encryption features. See [Security](./security).

## Deployment topologies

### Single node

```mermaid theme={null}
flowchart LR
    c[Clients] -->|:5445| a["airhouse server<br/>(data + control plane)"]
    b[Your backend] -->|":8080 Admin API"| a
    a --> pg[(Postgres)]
    a --> s3[(Object storage)]
```

One container runs both planes. It's the simplest setup, suited to evaluation, development, and small workloads. See [Installation](../installation#option-1-docker-compose).

### Kubernetes

```mermaid theme={null}
flowchart LR
    c[Clients] -->|:5445| lb[HAProxy]
    lb --> dp1[Data plane pod]
    lb --> dp2[Data plane pod]
    lb --> dpn[Data plane pod ...]
    b[Your backend] -->|":8080 Admin API"| cp["Control plane<br/>(2 replicas)"]
    dp1 & dp2 & dpn --> pg[(Postgres)]
    dp1 & dp2 & dpn --> s3[(Object storage)]
    cp --> pg
```

The Helm chart runs the data plane as an autoscaled Deployment behind HAProxy, and the control plane as its own Deployment. Data-plane pods are stateless: all durable state is in Postgres and object storage, so pods can be added, removed, or restarted freely. See [Installation](../installation#option-2-kubernetes-with-helm).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.