Quickback Docs
Quickback for Hono API

D1 Database

Cloudflare D1 is managed, serverless SQLite on Cloudflare's network. It is Quickback's zero-configuration default for Cloudflare deployments and a strong fit for CRUD-heavy SaaS products, content catalogs, internal tools, and workflows that benefit from simple operations.

What is D1?

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

  • Cloudflare-native — queried directly from your Worker with no public connection string
  • SQLite SQL — including D1's supported FTS5, JSON, and math extensions
  • Zero connection management — no pool or database server to operate
  • Managed durability — built-in disaster recovery, with global read replication available through D1 Sessions when a workload needs it

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

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:

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

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

# 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

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

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

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

Choose by workload, not by a limitations checklist

D1 is more capable than a simple LIKE-only store. Cloudflare documents FTS5 full-text search and the JSON extension, and D1 supports JSON path extraction, mutation, -> / ->> operators, and array expansion through json_each. SQLite also supports CTEs and window functions. The important distinction is whether your application wants SQLite's model or PostgreSQL's ecosystem.

NeedD1 approachChoose Neon/Postgres when…
Generated Quickback searchThe generated ?search API uses allowlisted LIKE semantics; custom actions can use FTS5Search is built around PostgreSQL tsvector, language dictionaries, GIN indexes, or an existing Postgres search design
Structured JSONJSON is stored as text with D1's JSON functions and path operatorsYou need native jsonb, GIN indexing, or extensive JSONB query/update operators
Booleans and arraysDrizzle maps booleans to SQLite integers; arrays can be modeled relationally or stored/queryable as JSONNative PostgreSQL booleans/arrays are part of the schema contract
Reporting and aggregationSQLite CTEs, window functions, joins, and aggregates cover conventional reportsYou need Postgres extensions, materialized views, specialized indexes, or a Postgres analytics ecosystem
IDsq.id() uses Quickback's provider-driven generation; SQLite INTEGER PRIMARY KEY is also available through Drizzle interopNative UUID/identity types and database-side defaults are a design requirement
Write profileA D1 database processes queries serially and is excellent for modest, short writesThe app needs sustained concurrent writes or interactive multi-step transactions

The practical rule is simple:

  • Start with D1 for conventional application data, low operational overhead, and Cloudflare-native deployment.
  • Start with Neon/Postgres when advanced SQL and data types are part of the product architecture—not as a workaround after launch. Neon HTTP remains the lightweight Cloudflare path; add Hyperdrive only when the application needs interactive transactions or connection pooling.

Keep Cloudflare's current D1 platform limits in capacity planning. Quickback supports both providers, so choosing Postgres for an advanced application is a first-class architecture choice.

On this page