# D1 Database

Cloudflare D1 is SQLite at the edge. Quickback uses D1 as the primary database for Cloudflare deployments, with a multi-database pattern for separation of concerns.

## What is D1?

D1 is Cloudflare's serverless SQL database built on SQLite:

- **Edge-native** - Runs in Cloudflare's global network
- **SQLite compatible** - Use familiar SQL syntax
- **Zero configuration** - No connection strings or pooling
- **Automatic replication** - Read replicas at every edge location

## Multi-Database Pattern

Quickback generates separate D1 databases for different concerns:

| Database | Binding | Purpose |
|----------|---------|---------|
| `AUTH_DB` | `AUTH_DB` | Better Auth tables (user, session, account) |
| `DB` | `DB` | Your application data |
| `FILES_DB` | `FILES_DB` | File metadata for R2 uploads |
| `WEBHOOKS_DB` | `WEBHOOKS_DB` | Webhook delivery tracking |

This separation provides:
- **Independent scaling** - Auth traffic doesn't affect app queries
- **Isolation** - Auth schema changes don't touch your data
- **Clarity** - Clear ownership of each database

## Drizzle ORM Integration

Quickback uses [Drizzle ORM](https://orm.drizzle.team/) for type-safe database access:

```ts
// schema/tables.ts - Your schema definition
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const posts = sqliteTable("posts", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  content: text("content"),
  authorId: text("author_id").notNull(),
  createdAt: integer("created_at", { mode: "timestamp" }),
});
```

The compiler generates Drizzle queries based on your security rules:

```ts
// Generated query with firewall applied
const result = await db
  .select()
  .from(posts)
  .where(eq(posts.authorId, userId)); // Firewall injects ownership
```

## Migrations

Quickback generates migrations automatically at compile time based on your schema changes:

```bash
# Compile your project (generates migrations)
quickback compile

# Apply migrations locally
wrangler d1 migrations apply DB --local

# Apply to production D1
wrangler d1 migrations apply DB --remote
```

Migration files are generated in `quickback/drizzle/` and version-controlled with your code. You never need to manually generate migrations—just define your schema and compile.

## wrangler.toml Bindings

The compiler generates D1 bindings in `wrangler.toml` automatically. To get production-ready IDs in the output, set them in your `quickback.config.ts`:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", {
    databaseId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",       // Features DB → binding "DB"
    authDatabaseId: "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",   // Auth DB → binding "AUTH_DB"
    filesDatabaseId: "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", // Files DB → binding "FILES_DB"
  }),
}
```

This generates:

```toml
[[d1_databases]]
binding = "AUTH_DB"
database_name = "my-app-auth"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

[[d1_databases]]
binding = "DB"
database_name = "my-app-features"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

[[d1_databases]]
binding = "FILES_DB"
database_name = "my-app-files"
database_id = "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"
```

If database IDs are omitted, the compiler uses local-dev placeholders — you'll need to replace them manually before deploying.

Create databases via Wrangler:

```bash
wrangler d1 create my-app-features
wrangler d1 create my-app-auth
wrangler d1 create my-app-files
```

## Accessing Data via API

All data access in Quickback goes through the generated API endpoints—never direct database queries. This ensures security rules (firewall, access, guards, masking) are always enforced.

### CRUD Operations

```bash
# List posts (firewall automatically filters by ownership)
GET /api/v1/posts

# Get a single post
GET /api/v1/posts/:id

# Create a post (guards validate allowed fields)
POST /api/v1/posts
{ "title": "Hello World", "content": "..." }

# Update a post (guards validate updatable fields)
PATCH /api/v1/posts/:id
{ "title": "Updated Title" }

# Delete a post
DELETE /api/v1/posts/:id
```

### Filtering and Pagination

```bash
# Filter by field
GET /api/v1/posts?status=published

# Pagination
GET /api/v1/posts?limit=10&offset=20

# Sort
GET /api/v1/posts?sort=createdAt&order=desc
```

### Why No Direct Database Access?

Direct database queries bypass Quickback's security layers:
- **Firewall** - Data isolation by user/org/team
- **Access** - Role-based permissions
- **Guards** - Field-level create/update restrictions
- **Masking** - Sensitive data redaction

Always use the API endpoints. For custom business logic, use [Actions](/compiler/definitions/actions).

## Local Development

D1 works locally with Wrangler:

```bash
# Start local dev server with D1
wrangler dev

# D1 data persists in .wrangler/state/
```

Local D1 uses SQLite files in `.wrangler/state/v3/d1/`, which you can inspect with any SQLite client.

## Security Architecture

For all Quickback-generated code, D1's application-layer security provides equivalent protection to Supabase RLS. The key difference is where enforcement happens — and this matters if you write custom routes.

### How Security Works

| Component | Enforcement | Notes |
|-----------|-------------|-------|
| CRUD endpoints | ✅ Firewall auto-applied | All generated routes enforce security |
| Actions | ✅ Firewall auto-applied | Both standalone and record-based |
| Manual routes | ⚠️ Must apply firewall | Use `withFirewall` helper |

### Why D1 is Secure

1. **No external database access** - D1 can only be queried through your Worker. There's no connection string or external endpoint.
2. **Generated code enforces rules** - All CRUD and Action endpoints automatically apply firewall, access, guards, and masking.
3. **Single entry point** - Every request flows through your API where security is enforced.

Unlike Supabase where PostgreSQL RLS provides database-level enforcement (protecting data even if application code has bugs), D1's security comes from architecture: the database is inaccessible except through your Worker, and all generated routes apply the four security pillars. The trade-off is that **custom routes you write outside Quickback must manually apply security** — there is no database-level safety net.

### Comparison with Supabase RLS

| Scenario | Supabase | D1 |
|----------|----------|-----|
| CRUD endpoints | ✅ Secure (RLS + App) | ✅ Secure (App) |
| Actions | ✅ Secure (RLS + App) | ✅ Secure (App) |
| Manual routes | ✅ RLS still protects | ⚠️ Must apply firewall |
| External DB access | ⚠️ Possible with credentials | ✅ Not possible |
| Dashboard queries | Via Supabase Studio | ⚠️ Admin only (audit logged) |

### Writing Manual Routes

If you write custom routes outside of Quickback compilation (e.g., custom reports, integrations), use the generated `withFirewall` helper to ensure security:

```ts
import { withFirewall } from '../features/invoices/resource';

app.get('/reports/monthly', async (c) => {
  return withFirewall(c, async (ctx, firewall) => {
    const results = await db.select()
      .from(invoices)
      .where(firewall);
    return c.json(results);
  });
});
```

The `withFirewall` helper:
- Validates authentication
- Builds the correct WHERE conditions for the current user/org
- Returns 401 if not authenticated

### Best Practices

1. **Use generated endpoints** - Prefer CRUD and Actions over manual routes
2. **Always apply firewall** - When writing manual routes, always use `withFirewall`
3. **Avoid raw SQL** - Raw SQL bypasses application security; use Drizzle ORM
4. **Review custom code** - Manual routes should be code-reviewed for security

## Limitations

D1 is built on SQLite, which means:

- **No stored procedures** - Business logic lives in your Worker
- **Single-writer** - One write connection at a time (reads scale horizontally)
- **Size limits** - 10GB per database (free tier: 500MB)
- **No PostgreSQL extensions** - Use Supabase if you need PostGIS, etc.

For most applications, these limits are non-issues. D1's edge distribution and zero-config setup outweigh the constraints.
