# Authenticating a client to DuckDB over Flight SQL

> How a Flight SQL client authenticates to the DuckDB gateway: password or bearer token, how the tenant is resolved, and the TLS settings to use.

The FlightSQL edge authenticates every connection independently of the admin UI / REST session. This page covers the two credential paths a client can use, how the tenant is resolved, and TLS. For the operator side (configuring providers), see [Authentication](/qod/operating/authentication) and [Authentication providers](/qod/operating/auth-providers).

## Two credential paths

The edge accepts either Basic credentials or a bearer JWT.

### Basic (username + password)

The client sends a username and password (the `user` / `password` JDBC params, or the ADBC `username` / `password` kwargs). They are validated by the tenant's configured provider, the database bcrypt backend or an OIDC provider's resource-owner-password flow. The `tenant` and `pool` must be supplied as headers / URL params; both are required.

### Bearer JWT

The client presents a validated JWT as the bearer token. The user is taken from the token's `sub` claim. The `tenant` and `pool` headers are always required - the edge does NOT read a tenant claim from the JWT; the URL is authoritative.

In both paths the owning database (tenant-db) is resolved server-side from `(tenant, pool)`; the client never names it. A disabled tenant or pool causes the handshake to fail as unauthenticated.

## Auth realm: tenant vs. system superuser

Two URL params pick which realm validates your credentials. The realm choice is independent of the routing target - a system superuser still addresses a specific `(tenant, pool)` for the query.

- `tenant=X&pool=Y` (no `superuser` flag) - **tenant realm**. Credentials are checked against tenant `X`'s configured auth provider (`qodstate_tenant.authConfig`); the matching `qodstate_user` row must have `tenant = X`.
- `tenant=X&pool=Y&superuser=true` - **system realm**. Credentials are checked against the manager's global auth providers (`quack-flightsql.auth.*` in `application.conf`); the matching `qodstate_user` row must have `tenant IS NULL`. The tenant/pool params still drive query routing.

There is no fallback between the two: a system credential cannot authenticate a tenant-scoped login and vice versa. Pre-existing JDBC URLs that used the bootstrap admin (`admin@localhost.local`) against tenants must add `?superuser=true` after upgrading.

```
# Tenant user
jdbc:arrow-flight-sql://host:31338/?tenant=tpch&pool=sales

# System superuser targeting the same tenant
jdbc:arrow-flight-sql://host:31338/?tenant=tpch&pool=sales&superuser=true
```

## No-auth mode

When the server has no auth providers configured, the edge runs in a v1 "trust the client" mode: it accepts the Basic username as-is (with a loud startup warning) and the admin UI skips its login screen. This is for local development only; never expose the edge without a provider configured.

## TLS

Edge TLS is on by default. On first boot the manager generates a self-signed certificate under `certs/`, so clients must either skip certificate verification or trust a CA-signed certificate you supply:

- JDBC: `useEncryption=true&disableCertificateVerification=true`.
- ADBC: `DatabaseOptions.TLS_SKIP_VERIFY = "true"`.

To remove the skip-verify flag, install a CA-signed certificate and point the edge at it; see [TLS](/qod/operating/tls). When edge TLS is disabled, connect with `grpc://` (ADBC) or `useEncryption=false` (JDBC) instead.

## Relationship to the REST session

The username/password you use here is the same credential set the admin UI accepts, but the mechanisms are separate: the UI mints a session token for `/api/*` calls, while the edge authenticates each Flight connection with Basic or JWT. A REST `X-API-Key` or UI session token is not a Flight credential.

Personal access tokens (`qod_pat_...`) are a third credential: they authenticate the [MCP server](/qod/connecting/mcp) for AI agents, and on `/api/*` a PAT in the `X-API-Key` header is accepted wherever its owner's session would be - though a token minted with a narrower scope carries only that scope, not the owner's full reach (see [Scoped tokens and delegation](/qod/connecting/mcp#scoped-tokens-and-delegation)). The Flight edge does not accept PATs.

The REST endpoints that run SQL on a node for you (catalog preview and data diff, undrop, restore, and the Iceberg preview and diff) run each statement as the credential's principal: a UI or CLI session as its user, a PAT as its owner with the token's own restriction (pools, `maxRows`, branch-only), so the ACL, column masking and row policies apply exactly as on a query over the edge, and a denied table answers `403 acl_denied`. Only the static `X-API-Key` runs as the system caller. A token that resolves to no live session, PAT or the static key is refused with `401 unauthorized` before any statement runs; the branch endpoints apply the same rule. Two internal steps still run as the system caller and return aggregate change counts only: the restore dry run and the branch change counter.
