# Blog Template

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](/compiler/config/single-tenant) — no organizations, no `organizationId` columns. Roles come from `user.role` instead of org membership.

## Create the Project

```bash
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

```typescript
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](/compiler/config/single-tenant) for details.

### Posts Feature (posts.ts)

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

```typescript
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: [
    { field: 'ownerId', equals: 'ctx.userId' },
    { field: 'deletedAt', isNull: true },
  ],
  guards: {
    createable: ["title", "slug", "content", "excerpt", "status", "publishedAt"],
    updatable: ["title", "slug", "content", "excerpt", "status", "publishedAt"],
    immutable: ["id", "ownerId"],
  },
  read: {
    access: { roles: ["PUBLIC"] },
  },
  create: { access: { roles: ["admin"] } },
  update: { access: { roles: ["admin"] } },
  delete: { access: { roles: ["admin"] }, mode: "soft" },
});
```

- **`PUBLIC`** role on `read.access` means no authentication required for `GET /` and `GET /:id`
- **`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:

```typescript
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

```bash
npm install
```

### 2. Log In to the Compiler

```bash
quickback login
```

### 3. Compile Your Definitions

```bash
quickback compile
```

### 4. Create D1 Database

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

```bash
npx wrangler d1 create my-site
```

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

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

### 5. Run Migrations (Local)

```bash
npm run db:migrate:local
```

### 6. Start Development Server

```bash
npm run dev
```

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

## Using the API

### Public Endpoints (No Auth Required)

```bash
# 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)

```bash
# 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:

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

```typescript
// 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: [{ field: 'ownerId', equals: 'ctx.userId' }],
  read: {
    access: { roles: ["PUBLIC"] },
  },
  create: { access: { roles: ["admin"] } },
  update: { access: { roles: ["admin"] } },
  delete: { access: { roles: ["admin"] } },
});
```

Then recompile:

```bash
quickback compile
```

## Deploying to Production

```bash
# Apply remote migrations
npm run db:migrate:remote

# Deploy to Cloudflare
npm run deploy
```

## Next Steps

- [Single-Tenant Mode](/compiler/config/single-tenant) — How single-tenant mode works
- [Access Control](/compiler/definitions/access) — PUBLIC role and role-based permissions
- [Custom Actions](/compiler/definitions/actions) — Add business logic beyond CRUD
- [Cloudflare Template](/compiler/getting-started/template-cloudflare) — Multi-tenant alternative
