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