# Empty Scaffolding Templates

The `empty` template creates a project with only the configuration file — no example features. Use this when you know exactly what you want to build and don't need starter code.

## Available Empty Templates

| Template | Runtime | Database | Command |
|----------|---------|----------|---------|
| `empty` | Cloudflare Workers | D1 | `quickback create empty my-app` |

Alias: `quickback create scaffold my-app` (same as `empty`)

## Create the Project

```bash
quickback create empty my-app
```

This scaffolds a minimal project:

```
my-app/
├── quickback/
│   ├── quickback.config.ts       # Compiler configuration
│   └── features/                 # Empty — add your own
├── package.json
└── tsconfig.json
```

## Generated Configuration

```typescript
export default {
  name: "my-app",
  template: "hono",
  auth: {
    // Role hierarchy: lowest → highest privilege.
    // Use "role+" in access rules to mean "this role and above".
    // e.g. roles: ["member+"] expands to ["member", "admin", "owner"].
    roleHierarchy: ["member", "admin", "owner"],
  },
  providers: {
    runtime: { name: "cloudflare", config: {} },
    database: {
      name: "cloudflare-d1",
      config: { binding: "DB" },
    },
    auth: { name: "better-auth", config: {} },
  },
};
```

Organization-backed auth is the default. If you want a fixed-org deployment with no org switching UI, add `features.pinnedOrganizationId` as described in [Pinned Organization Mode](/configure/single-tenant).

## Adding Your First Feature

Create a feature directory with a schema file:

```bash
mkdir -p quickback/features/products
```

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

export const products = sqliteTable("products", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  description: text("description"),
  price: integer("price").notNull(),
  organizationId: text("organization_id").notNull(),
  // `ownerId` is the owner column the firewall auto-detects — `userId` is not
  // auto-detected on feature tables.
  ownerId: text("owner_id").notNull(),

  // Compiler-managed columns — the audit quartet on every table, plus the
  // soft-delete pair because `delete.mode` is "soft".
  createdAt: text("created_at").notNull().default('1970-01-01T00:00:00.000Z').$defaultFn(() => new Date().toISOString()),
  modifiedAt: text("modified_at").notNull().default('1970-01-01T00:00:00.000Z').$defaultFn(() => new Date().toISOString()).$onUpdate(() => new Date().toISOString()),
  createdBy: text("created_by"),
  modifiedBy: text("modified_by"),
  deletedAt: text("deleted_at"),
  deletedBy: text("deleted_by"),
});

export default defineTable(products, {
  firewall: {
    organization: {},
    owner: {},
    softDelete: {},
  },
  guards: {
    createable: ["name", "description", "price"],
    updatable: ["name", "description", "price"],
  },
  read: {
    access: { roles: ["member+"] },
  },
  create: { access: { roles: ["member+"] } },
  update: { access: { roles: ["member+"] } },
  delete: { access: { roles: ["admin+"] }, mode: "soft" },
});
```

Then compile:

```bash
quickback login    # First time only
quickback compile
```

## Setup Steps

Follow the same setup steps as the [Cloudflare template](/start/template-cloudflare#setup-steps). Compiling with zero features works (you get an auth-only API); add feature files as you go and recompile.

## Next Steps

- [Schema Definitions](/define/schema) — Define tables with Drizzle ORM
- [Firewall](/define/firewall) — Data isolation and tenant scoping
- [Access Control](/define/access) — Role-based permissions
- [Guards](/define/guards) — Field-level write protection
- [Full Example](/start/first-api) — Complete feature walkthrough
