# Single-Tenant Mode

Single-tenant mode disables the Better Auth organization plugin, removing the `organizationId` requirement from your API. This is ideal for personal sites, blogs, admin panels, or any app with a single tenant.

## Enabling Single-Tenant Mode

Set `features.organizations` to `false` in your config:

```typescript
import { defineConfig, defineRuntime, defineDatabase, defineAuth } from "@quickback/compiler";

export default defineConfig({
  name: "my-site",
  template: "hono",
  features: {
    organizations: false,
  },
  providers: {
    runtime: defineRuntime("cloudflare"),
    database: defineDatabase("cloudflare-d1"),
    auth: defineAuth("better-auth"),
  },
});
```

## What Changes

| Aspect | Multi-Tenant (default) | Single-Tenant |
|--------|----------------------|---------------|
| Organization plugin | Enabled | Disabled |
| `organizationId` columns | Required on resources | Not used |
| Role source | Org membership table | `user.role` field |
| Firewall scopes | `owner`, `organization`, `team`, `exception` | `owner`, `exception` only |
| Admin org endpoints | Use Better Auth's `/auth/v1/organization/list` (caller's orgs) and `/auth/v1/admin/list-organizations` when `cms.sysadmin: true` (cross-tenant) | N/A (no org plugin) |
| `ctx.activeOrgId` | From session/JWT | Always `undefined` |
| `ctx.roles` | From org member role | From `user.role` |

## Firewall Configuration

In single-tenant mode, resources use either **owner** scope (user-scoped data) or **exception** (public/system data):

```typescript
// User-scoped data (requires authentication)
export default defineTable(posts, {
  firewall: [{ field: 'ownerId', equals: 'ctx.userId' }],
  read: {
    access: { roles: ['user', 'admin'] },
  },
  create: { access: { roles: ['admin'] } },
});

// Public data (no ownership filtering)
export default defineTable(pages, {
  firewall: [{ exception: true }],
  read: {
    access: { roles: ['PUBLIC'] },
  },
  create: { access: { roles: ['admin'] } },
  update: { access: { roles: ['admin'] } },
  delete: { access: { roles: ['admin'] } },
});
```

> Using `q.scope('organization')` / `q.scope('team')` (or any firewall predicate referencing `ctx.activeOrgId` / `ctx.activeTeamId`) with `organizations: false` will cause a compile error. These scopes require the organization plugin.


## Public Routes

Use the `PUBLIC` role to expose routes without authentication. This works the same as in multi-tenant mode:

```typescript
read: {
  access: { roles: ['PUBLIC'] },  // GET / and GET /:id — no auth required
},
create: { access: { roles: ['admin'] } }, // Admin only
```

## Role-Based Access

Roles come from the `user.role` field (managed by Better Auth's [admin plugin](https://www.better-auth.com/docs/plugins/admin)).

> In single-tenant mode `ctx.roles` mirrors `user.role`, so `roles: ["admin"]` and `userRole: ["admin"]` behave the same. Prefer [`userRole`](/compiler/definitions/access#userrole-vs-roles) — it documents intent and keeps the gate correct if you later enable multi-tenant.



| Role | Description |
|------|-------------|
| `admin` | Full access, set via Better Auth admin plugin |
| `user` | Default role for authenticated users |
| `PUBLIC` | Unauthenticated access (special keyword) |

```typescript
// Admin-only action
export default defineActions({
  publish: {
    access: { roles: ['admin'] },
    execute: async ({ ctx, db }) => {
      // ctx.userRole === 'admin'
      // ctx.roles === ['admin']
    },
  },
});
```

## Consuming the API

Since single-tenant mode produces 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', content: '...' }),
});
```
