Quickback Docs

Blog Template

Step-by-step guide to creating a single-tenant blog/CMS backend with Quickback on Cloudflare Workers.

The blog template creates a single-tenant backend for a blog or CMS, deployed to Cloudflare Workers with D1. Public visitors can read content without authentication, while admins manage posts via Better Auth's admin plugin.

This template uses single-tenant mode — no organizations, no organizationId columns. Roles come from user.role instead of org membership.

Create the Project

quickback create blog my-site
cd my-site

This scaffolds a project with:

my-site/
├── quickback/
│   ├── quickback.config.ts       # Single-tenant config
│   └── features/
│       └── posts/
│           ├── posts.ts          # Schema + security (defineTable)
│           └── actions.ts        # Publish/unpublish actions
├── src/                          # Compiled output (generated)
├── package.json
├── tsconfig.json
├── wrangler.toml
└── drizzle.config.ts

Generated Configuration

quickback.config.ts

export default {
  name: "my-site",
  template: "hono",
  features: {
    organizations: false,
  },
  providers: {
    runtime: { name: "cloudflare", config: {} },
    database: {
      name: "cloudflare-d1",
      config: { binding: "DB" },
    },
    auth: { name: "better-auth", config: {} },
  },
};

Key difference from the multi-tenant templates: organizations: false disables the Better Auth organization plugin. See Single-Tenant Mode for details.

Posts Feature (posts.ts)

The template includes a posts feature with public read access and admin-only write access:

import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { defineTable } from "@quickback/compiler";

export const posts = sqliteTable("posts", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  slug: text("slug").notNull().unique(),
  content: text("content"),
  excerpt: text("excerpt"),
  status: text("status").notNull().default("draft"),
  publishedAt: text("published_at"),
  ownerId: text("owner_id").notNull(),
});

export default defineTable(posts, {
  firewall: {
    owner: {},
    softDelete: {},
  },
  guards: {
    createable: ["title", "slug", "content", "excerpt", "status", "publishedAt"],
    updatable: ["title", "slug", "content", "excerpt", "status", "publishedAt"],
    immutable: ["id", "ownerId"],
  },
  crud: {
    list: { access: { roles: ["PUBLIC"] } },
    get: { access: { roles: ["PUBLIC"] } },
    create: { access: { roles: ["admin"] } },
    update: { access: { roles: ["admin"] } },
    delete: { access: { roles: ["admin"] }, mode: "soft" },
  },
});
  • PUBLIC role on list and get means no authentication required for reading
  • admin role on create, update, and delete requires an authenticated user with role: "admin" (set via Better Auth's admin plugin)
  • owner firewall scope tracks who created each post via ownerId
  • softDelete marks posts as deleted rather than removing them from the database

Post Actions (actions.ts)

The template includes publish/unpublish actions with guard conditions:

import { posts } from './posts';
import { defineActions } from '@quickback/compiler';
import { z } from 'zod';

export default defineActions(posts, {
  publish: {
    description: "Publish a draft post",
    input: z.object({}),
    guard: {
      roles: ["admin"],
      record: { status: { equals: "draft" } },
    },
    sideEffects: "sync",
  },
  unpublish: {
    description: "Revert a published post to draft",
    input: z.object({}),
    guard: {
      roles: ["admin"],
      record: { status: { equals: "published" } },
    },
    sideEffects: "sync",
  },
});

These actions are admin-only and include record-level guards — you can only publish drafts and unpublish published posts.

Setup Steps

1. Install Dependencies

npm install

2. Log In to the Compiler

quickback login

3. Compile Your Definitions

quickback compile

4. Create D1 Database

The blog template uses a single D1 database (no split databases):

npx wrangler d1 create my-site

Copy the database ID from the output and update your wrangler.toml:

[[d1_databases]]
binding = "DB"
database_name = "my-site"
database_id = "paste-id-here"

5. Run Migrations (Local)

npm run db:migrate:local

6. Start Development Server

npm run dev

Your API is running at http://localhost:8787.

Using the API

Public Endpoints (No Auth Required)

# List all posts
curl http://localhost:8787/api/v1/posts

# Get a single post
curl http://localhost:8787/api/v1/posts/post-id-here

Admin Endpoints (Auth Required)

# Create a post (admin only)
curl -X POST http://localhost:8787/api/v1/posts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Hello World", "slug": "hello-world", "content": "..."}'

# Publish a post (admin only)
curl -X POST http://localhost:8787/api/v1/posts/post-id/actions/publish \
  -H "Authorization: Bearer $TOKEN"

Pairing with a Frontend

Since this is a JSON API, pair it with any frontend framework:

// Astro, Next.js, SvelteKit, etc.
const posts = await fetch('https://api.mysite.com/api/v1/posts').then(r => r.json());

// Admin operations require authentication
const res = await fetch('https://api.mysite.com/api/v1/posts', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'New Post', slug: 'new-post', content: '...' }),
});

Adding More Features

Add new content types by creating feature directories. For example, a pages feature for static pages:

// quickback/features/pages/pages.ts
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
import { defineTable } from "@quickback/compiler";

export const pages = sqliteTable("pages", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  slug: text("slug").notNull().unique(),
  content: text("content"),
  ownerId: text("owner_id").notNull(),
});

export default defineTable(pages, {
  firewall: { owner: {} },
  crud: {
    list: { access: { roles: ["PUBLIC"] } },
    get: { access: { roles: ["PUBLIC"] } },
    create: { access: { roles: ["admin"] } },
    update: { access: { roles: ["admin"] } },
    delete: { access: { roles: ["admin"] } },
  },
});

Then recompile:

quickback compile

Deploying to Production

# Apply remote migrations
npm run db:migrate:remote

# Deploy to Cloudflare
npm run deploy

Next Steps

On this page