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:
| 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 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 ownershipMigrations
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 --remoteMigration 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-filesAccessing 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/:idFiltering 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=descWhy 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
| 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
- No external database access - D1 can only be queried through your Worker. There's no connection string or external endpoint.
- Generated code enforces rules - All CRUD and Action endpoints automatically apply firewall, access, guards, and masking.
- 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:
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
- Use generated endpoints - Prefer CRUD and Actions over manual routes
- Always apply firewall - When writing manual routes, always use
withFirewall - Avoid raw SQL - Raw SQL bypasses application security; use Drizzle ORM
- 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.
| Need | D1 approach | Choose Neon/Postgres when… |
|---|---|---|
| Generated Quickback search | The generated ?search API uses allowlisted LIKE semantics; custom actions can use FTS5 | Search is built around PostgreSQL tsvector, language dictionaries, GIN indexes, or an existing Postgres search design |
| Structured JSON | JSON is stored as text with D1's JSON functions and path operators | You need native jsonb, GIN indexing, or extensive JSONB query/update operators |
| Booleans and arrays | Drizzle maps booleans to SQLite integers; arrays can be modeled relationally or stored/queryable as JSON | Native PostgreSQL booleans/arrays are part of the schema contract |
| Reporting and aggregation | SQLite CTEs, window functions, joins, and aggregates cover conventional reports | You need Postgres extensions, materialized views, specialized indexes, or a Postgres analytics ecosystem |
| IDs | q.id() uses Quickback's provider-driven generation; SQLite INTEGER PRIMARY KEY is also available through Drizzle interop | Native UUID/identity types and database-side defaults are a design requirement |
| Write profile | A D1 database processes queries serially and is excellent for modest, short writes | The 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.