# Providers

Providers configure which services your compiled backend targets.

## Runtime Providers

The Quickback Stack is **Cloudflare-only**. `cloudflare` is the single supported runtime.

```typescript
import { defineRuntime } from "@quickback/compiler";

providers: {
  runtime: defineRuntime("cloudflare"),
}
```

### Runtime Options

| Option | Type | Description |
|--------|------|-------------|
| `compatibilityDate` | `string` | Emits Cloudflare `compatibility_date` in `wrangler.toml`. |
| `placement.mode` | `"smart" \| "standard"` | Emits Cloudflare's `[placement]` block. |
| `observability.enabled` | `boolean` | Emits top-level Workers Logs persistence under `[observability]`. |
| `observability.headSamplingRate` | `number` | Top-level Workers Logs sample rate from `0` to `1`. |
| `observability.logs.enabled` | `boolean` | Emits `[observability.logs]`. |
| `observability.logs.invocationLogs` | `boolean` | Enables or suppresses invocation logs. |
| `observability.logs.destinations` | `string[]` | Named log export destinations configured in Cloudflare. |
| `observability.logs.headSamplingRate` | `number` | Workers Logs sample rate from `0` to `1`. |
| `observability.logs.persist` | `boolean` | Keeps exported logs in the Cloudflare dashboard. |
| `observability.traces.enabled` | `boolean` | Emits `[observability.traces]`. |
| `observability.traces.destinations` | `string[]` | Named trace export destinations configured in Cloudflare. |
| `observability.traces.headSamplingRate` | `number` | Workers Traces sample rate from `0` to `1`. |
| `observability.traces.persist` | `boolean` | Keeps exported traces in the Cloudflare dashboard. |
| `routes` | `{ pattern, custom_domain? }[]` | Custom domain routes for the generated Worker. |

```typescript
providers: {
  runtime: defineRuntime("cloudflare", {
    compatibilityDate: "2026-05-13",
    placement: { mode: "smart" },
    observability: {
      enabled: true,
      logs: {
        enabled: true,
        invocationLogs: false,
        destinations: ["logs-prod"],
        headSamplingRate: 0.6,
      },
      traces: {
        enabled: true,
        destinations: ["otel-prod"],
        headSamplingRate: 0.05,
        persist: false,
      },
    },
  }),
}
```

## Database Providers

Two databases are supported, both Cloudflare-native:

| Provider | Description |
|----------|-------------|
| `cloudflare-d1` | Cloudflare D1 (SQLite at the edge) |
| `neon` | Neon serverless PostgreSQL (`@neondatabase/serverless` over HTTP) |

One database provider per app: auth tables and feature tables always share the
selected database. On `neon`, Better Auth uses its PostgreSQL/Drizzle adapter
in the same Postgres database as your features; on `cloudflare-d1`, both use
D1. A per-service map (`database: { auth: ..., features: ... }`) is rejected at
compile time — mixed database providers are not supported.

```typescript
import { defineDatabase } from "@quickback/compiler";

providers: {
  database: defineDatabase("cloudflare-d1", {
    generateId: "prefixed",          // "uuid" | "cuid" | "nanoid" | "prefixed" | "serial" | false
    namingConvention: "snake_case",   // "camelCase" | "snake_case"
    usePlurals: false,                // Auth table names: "users" vs "user"
  }),
}
```

### Database Options

Options from `splitDatabases` down apply to `cloudflare-d1` only — Neon uses a
single Postgres database with no bindings or split mode (see
[Neon options](#neon-options) below).

| Option | Type | Default (D1) | Description |
|--------|------|---------|-------------|
| `generateId` | `string \| false` | `"prefixed"` | ID generation strategy |
| `namingConvention` | `string` | `"snake_case"` | Column naming convention |
| `usePlurals` | `boolean` | `false` | Pluralize auth table names (e.g. `user` vs `users`) |
| `splitDatabases` | `boolean` | `true` | Separate auth and features databases (D1 only) |
| `authBinding` | `string` | `"AUTH_DB"` | Binding name for auth database |
| `featuresBinding` | `string` | `"DB"` | Binding name for features database |
| `databaseId` | `string` | — | D1 database ID for the features database (used in generated wrangler.toml) |
| `authDatabaseId` | `string` | — | D1 database ID for the auth database |
| `featuresDatabaseId` | `string` | — | Explicit features DB ID (falls back to `databaseId`) |
| `filesDatabaseId` | `string` | — | D1 database ID for the files metadata database |
| `webhooksDatabaseId` | `string` | — | D1 database ID for the webhooks database |
| `auditBinding` | `string` | `"AUDIT_DB"` | Binding name for the security audit database |
| `auditDatabaseName` | `string` | `"{app}-audit"` | D1 database name for audit events |
| `auditDatabaseId` | `string` | — | D1 database ID for audit events. Optional at compile time — a placeholder is emitted and `quickback deploy` / the Start broker create the database and splice the real id back |
| `experimentalRemote` | `{ auth?, features?, audit?, webhooks?, files? }` | `{}` | Per-binding `experimental_remote = true` flag for `wrangler dev` (see [below](#sharing-deployed-d1-in-dev-experimental_remote)) |
| `realtime` | `boolean \| { enabled?, wsTicket? }` | `false` | Enables the Broadcaster Durable Object and optional resource-scoped ws-ticket auth (see [below](#realtime-broadcast-config)) |
| `webhookAdminRoles` | `string[]` | `["owner", "admin"]` | Org roles allowed to manage webhook endpoints |

#### Neon Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `connectionMode` | `"auto" \| "http" \| "websocket" \| "hyperdrive"` | `"auto"` | How the generated runtime connects (`hyperdrive` enables interactive Postgres transactions on Workers) |
| `hyperdrive.id` | `string` | — | Required Cloudflare Hyperdrive configuration ID when `connectionMode: "hyperdrive"` |
| `hyperdrive.localConnectionString` | `string` | — | Optional local-only Postgres connection used by Wrangler dev |
| `pooled` | `boolean` | `true` | Use the pooled Neon connection string (recommended for Workers) |

The Worker connects through a single `DATABASE_URL` secret — auth and feature
queries share it. See [Neon](/platform/database/neon) for
connection modes, RLS, and the migration workflow.

#### Audit Database

Cross-tenant `unsafe` actions (see [Actions → Unsafe mode](/define/actions/scoped-db#unsafe-mode)) write mandatory audit events. Those events land in a dedicated D1 database — separate from your features DB — so audit history stays isolated from application data and can't be tampered with through a normal CRUD route.

You don't need to do anything to enable it — the compiler wires up the binding automatically if your project declares any unsafe, `PUBLIC`, or hard-delete action. Leave `auditDatabaseId` unset until deploy: compile emits a placeholder, and `quickback deploy` (or the Start Cloudflare card) creates the D1 and writes the id back. Do not put an empty string in the config.

To override defaults:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", {
    auditBinding: "MY_AUDIT_DB",            // default: AUDIT_DB
    auditDatabaseName: "my-app-audit-prod", // default: {app}-audit
    auditDatabaseId: "c3b8866b-cc4f-4e51-8382-0af244584d93",
  }),
}
```

The binding name must match what your Worker reads at runtime. If you don't declare any unsafe, `PUBLIC`, or hard-delete actions, no audit DB is provisioned and these options have no effect.

#### Setting Database IDs for Deployment

When deploying to production, set your D1 database IDs so the compiler generates a ready-to-deploy `wrangler.toml`:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", {
    authDatabaseId: "fe6c77c4-2131-40be-a8fc-3649eef2a38c",
    databaseId: "bba22981-cb6e-43d6-bf32-4193513c60b1",
    filesDatabaseId: "c3b8866b-cc4f-4e51-8382-0af244584d93",
  }),
}
```

If these are omitted, the compiler uses local-dev placeholders and you'll need to manually edit `wrangler.toml` before deploying.

#### Sharing deployed D1 in dev (`experimental_remote`)

Wrangler can read or write the **deployed** D1 for a specific binding while `wrangler dev` runs locally. A common workflow: share the production auth DB in dev so real sessions and users work against the running local Worker, while feature writes stay sandboxed in the local SQLite shadow.

Quickback regenerates `wrangler.toml` on every compile, so flags like `experimental_remote = true` need to live in `quickback.config.ts`:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", {
    authDatabaseId: "fe6c77c4-2131-40be-a8fc-3649eef2a38c",
    experimentalRemote: { auth: true },
  }),
}
```

Keys map to logical bindings, not the runtime binding string:

| Key | Binding affected |
|-----|------------------|
| `auth` | `AUTH_DB` (also applied to the files worker's auth binding) |
| `features` | `DB` (or the legacy single-DB binding when `splitDatabases: false`) |
| `audit` | `AUDIT_DB` |
| `webhooks` | `WEBHOOKS_DB` |
| `files` | `FILES_DB` (also applied to the files worker) |

Only `true` flips the flag on; `false` or omitted leaves the binding local. Off by default — only enable against databases you intend to share.

> `experimental_remote` is a Wrangler dev feature. A flagged binding hits the deployed D1 from `wrangler dev`, so any write goes to production. Only enable for bindings you've decided are safe to read/write from local dev.


#### Realtime Broadcast Config

Set `realtime: true` to enable the generated Broadcaster Durable Object with the default organization-scoped routing model:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", {
    realtime: true,
  }),
}
```

For resource-scoped tickets, use the object form. The mint gate is a discriminated `access` union — exactly one arm:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", {
    realtime: {
      wsTicket: {
        access: { authzRole: "attendee" },   // or { roles: ["member+"] } or { fga: { relation, object } }
        requestField: "eventId",
        scopeTable: "events",
      },
    },
  }),
}
```

> The broadcast surface mounts at `/broadcast/v1` by default and `/broadcast/v2` under `contract.routes: "v2"`. The path is the only thing `routes` moves — frame format is unaffected.

`wsTicket` tells the compiler how to authorize `/broadcast/v1/ws-ticket` and what scope to stamp into the short-lived websocket ticket:

- `access`: the mint gate — one of `{ roles: ["member+"] }` (org-membership roles with hierarchy expansion; also verifies the scope row is in the caller's active org), `{ authzRole: "attendee" }` (an authz role declared under `authz.roles` with a direct relationship `{ via: "..." }`), or `{ fga: { relation: "viewer", object: "event:{id}" } }` (an FGA relation on the object derived from the scope row). The legacy `role: "attendee"` string is the shorthand for `{ authzRole }`.
- `requestField`: the JSON body field the client posts to `/broadcast/v1/ws-ticket`
- `scopeTable`: the parent resource table that defines the broadcast scope key

Instead of `access`, `wsTicket` may name a **namespace** (`namespace: "eventScope"`) to
authorize through that namespace's full multi-lane `via` — including a namespace declared by
a [feature area](/define/areas)'s `_area.ts` (the one root-config
surface allowed to reference an area-declared name; it attaches the area's complete gate and
is listed in the authz manifest under the owning area). See
[Namespace-backed tickets](/platform/realtime/durable-objects#namespace-backed-tickets-per-matched-lane).

In other words: the declared gate proves access, and the `scopeTable` row defines the channel. For an attendee flow, `guests` (or RSVP) would be the relationship evidence and `events` would be the `scopeTable`; see [Realtime](/platform/realtime#enabling-realtime) for the full per-arm semantics.

Quickback keeps Better Auth as the front door, then mints its own 60-second websocket ticket after the authz check succeeds. The generated `createRealtime()` helper and Broadcaster share the same targeting model:

- string target: historical `organizationId` shorthand
- object target: `{ scopeKey?, organizationId?, userId? }`

See [Realtime](/platform/realtime) for the client-side connection flow and helper examples.

> `usePlurals` only affects **auth tables** generated by Better Auth (e.g. `user` vs `users`, `session` vs `sessions`). Feature table names come directly from your Drizzle schema definitions — whatever you pass to `sqliteTable()` or `pgTable()` is used as-is. The `namingConvention` setting applies to both auth and feature column names.


### ID Generation Options

| Value | Description | Example |
|-------|-------------|---------|
| `"uuid"` | Server generates UUID | `550e8400-e29b-41d4-a716-446655440000` |
| `"cuid"` | Server generates CUID | `clh2v8k9g0000l508h5gx8j1a` |
| `"nanoid"` | Server generates nanoid (21-char, URL-safe) | `V1StGXR8_Z5jdHi6B-myT` |
| `"short"` | 6-char alphanumeric (nanoid `customAlphabet`) | `a3F9xK` |
| `"prefixed"` | Prefixed ID from table name | `room_abc123` |
| `"serial"` | Database auto-increments | `1`, `2`, `3` |
| `false` | Client provides ID (enables PUT/upsert) | Any string |

> `"short"` produces IDs with ~56.8 billion possible values (62^6). That's fine for small/scoped tables like rooms, invites, or share codes — but at roughly 238,000 rows you hit a 50% chance of collision (birthday bound). Don't use it for high-volume records. `"nanoid"` and `"short"` both require the `nanoid` package in your project.


> `generateId: "cuid"` is not byte-for-byte uniform across every compiler path. Import-capable runtime paths emit real `createId()` usage, but compiler-managed schema auto-injection may preserve the `createId()` call site with a file-local `crypto.randomUUID()` polyfill so schema-generation sandboxes stay dependency-free. If you require true cuid2 strings everywhere, explicitly import `createId` from `@paralleldrive/cuid2` in the authored schema file.


## Auth Providers

| Provider | Description |
|----------|-------------|
| `better-auth` | Better Auth with plugins (default) |
| `external` | External auth via Cloudflare service binding |

```typescript
import { defineAuth } from "@quickback/compiler";

providers: {
  auth: defineAuth("better-auth", {
    session: {
      expiresInDays: 7,
      updateAgeInDays: 1,
    },
    rateLimit: {
      enabled: true,
      window: 60,
      max: 100,
    },
  }),
}
```

### Better Auth Options

| Option | Type | Description |
|--------|------|-------------|
| `session.expiresInDays` | `number` | Session expiration in days |
| `session.updateAgeInDays` | `number` | Session refresh interval in days |
| `rateLimit.enabled` | `boolean` | Enable rate limiting |
| `rateLimit.window` | `number` | Rate limit window in seconds |
| `rateLimit.max` | `number` | Max requests per window |
| `atomicAuthRoutes` | `("emailOtpSignIn")[]` | Runs the exact email-OTP sign-in handler and `before-email-otp-activate.ts` admission hook in one Neon Hyperdrive transaction. Contract v2 only. |
| `socialProviders` | `object` | Social login providers (`google`, `github`, `discord`) |
| `debugLogs` | `boolean` | Enable auth debug logging |

> Auth table naming (`usePlurals`, `namingConvention`) is inherited from your database provider config — you don't need to set it separately on the auth provider. `usePlurals` controls whether auth tables are named `user`/`session` or `users`/`sessions`. It does not affect feature tables, which use whatever name you define in your schema.


### Better Auth Plugins

When `features: ["organizations"]` is set in your config, the compiler automatically enables organization-related plugins. Additional plugins can be configured:

| Plugin | Description |
|--------|-------------|
| `organization` | Multi-tenant organizations |
| `admin` | Admin panel access |
| `apiKey` | API key authentication |
| `anonymous` | Anonymous sessions |
| `upgradeAnonymous` | Convert anonymous to full accounts |
| `twoFactor` | Two-factor authentication |
| `passkey` | WebAuthn passkey login |
| `magicLink` | Email magic link login |
| `emailOtp` | Email one-time password |
| `deviceAuthorization` | Device auth flow (CLI tools) |
| `jwt` | JWT token support |
| `openAPI` | OpenAPI spec generation |

`emailOtp` also accepts an options object. Set `expiresIn` to a positive integer
number of seconds to override Better Auth's OTP lifetime. The setting is
transport-neutral and is emitted identically for Cloudflare Email and AWS SES:

```typescript
plugins: { emailOtp: { expiresIn: 1200 } }
```

## Storage Providers

```typescript
import { defineStorage, defineFileStorage } from "@quickback/compiler";

providers: {
  storage: defineStorage("cloudflare-kv", {
    binding: "KV_STORE",
    namespaceId: "7ed0f824a8c54c388713d9fdf63cd004",
  }),
  fileStorage: defineFileStorage("cloudflare-r2", {
    binding: "FILES",
    maxFileSize: "10mb",
    allowedTypes: ["image/png", "image/jpeg", "application/pdf"],
    publicDomain: "files.example.com",
  }),
}
```

> The KV `namespaceId` can be set on the storage provider config, or as `kvId` on the auth provider config (since KV is primarily used for auth sessions). The compiler checks both locations.


### Storage Types

| Name | Description |
|------|-------------|
| `cloudflare-kv` | Key-value storage (Cloudflare KV) |
| `cloudflare-r2` | Object storage (Cloudflare R2) |
| `memory` | In-memory storage (development only) |

These three names are the complete registry. Any other name — `kv`, `r2`, `redis` — fails the compile with `Unknown storage provider`.

### File Storage Options

| Option | Type | Description |
|--------|------|-------------|
| `binding` | `string` | R2 bucket binding name |
| `maxFileSize` | `string` | Maximum file size (e.g. `"10mb"`) |
| `allowedTypes` | `string[]` | Allowed MIME types |
| `publicDomain` | `string` | Public domain for file URLs |
| `userScopedBuckets` | `string[]` | Buckets stored under `users/{userId}/{bucket}/{path}` instead of being org-scoped (default: `["avatars"]`) |
