# 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 (PostgreSQL over Hyperdrive) |

```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

| 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 |
| `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` | — | Explicit D1 database ID for audit events |
| `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 |

#### Audit Database

Cross-tenant `unsafe` actions (see [Actions → Unsafe mode](/compiler/definitions/actions#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 action. 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 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 relationship-authorized resource scopes, use the object form:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", {
    realtime: {
      wsTicket: {
        role: "attendee",
        requestField: "eventId",
        scopeTable: "events",
      },
    },
  }),
}
```

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

- `role`: an authz role declared under `authz.roles` that uses a direct relationship (`{ via: "..." }`)
- `requestField`: the JSON body field the client posts to `/broadcast/v1/ws-ticket`
- `scopeTable`: the parent resource table that defines the broadcast scope key

In other words: the relationship table 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`.

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](/stack/realtime) for the client-side connection flow and helper examples.

<Callout>
`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.
</Callout>

### 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.


## 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 |
| `socialProviders` | `object` | Social login providers (`google`, `github`, `discord`) |
| `debugLogs` | `boolean` | Enable auth debug logging |

<Callout>
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.
</Callout>

### 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 |

## Storage Providers

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

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

<Callout>
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.
</Callout>

### Storage Types

| Type | Description |
|------|-------------|
| `kv` | Key-value storage (Cloudflare KV, Redis) |
| `r2` | Object storage (Cloudflare R2) |
| `memory` | In-memory storage (development only) |
| `redis` | Redis storage |

### 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` | `boolean` | Scope files by user |
