Quickback Docs

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:

DatabaseBindingPurpose
AUTH_DBAUTH_DBBetter Auth tables (user, session, account)
DBDBYour application data
FILES_DBFILES_DBFile metadata for R2 uploads
WEBHOOKS_DBWEBHOOKS_DBWebhook 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 for type-safe database access:

// 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:

// 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:

# 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 drizzle/migrations/ and version-controlled with your code. You never need to manually generate migrations—just define your schema and compile.

wrangler.toml Bindings

Configure D1 bindings in wrangler.toml:

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

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

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

Create databases via Wrangler:

wrangler d1 create my-app-db
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

# 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

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

Local Development

D1 works locally with Wrangler:

# 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

D1 uses application-layer security that is equally secure to Supabase RLS when using Quickback-generated code. The key difference is where enforcement happens.

How Security Works

ComponentEnforcementNotes
CRUD endpoints✅ Firewall auto-appliedAll generated routes enforce security
Actions✅ Firewall auto-appliedBoth standalone and record-based
Manual routes⚠️ Must apply firewallUse 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, D1's security comes from architecture: the database is inaccessible except through your Worker, and all generated routes apply the four security pillars.

Comparison with Supabase RLS

ScenarioSupabaseD1
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 queriesVia 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:

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.

On this page