# Connecting

The CMS supports two modes: **demo mode** for development and testing, and **live mode** for connecting to a real Quickback API.

## Demo Mode

When no `VITE_QUICKBACK_API_URL` is set, the CMS runs in demo mode:

- Uses a **mock API client** backed by `localStorage`
- Loads seed data from JSON files in `data/mock/`
- Provides a **role switcher** in the header to test owner/admin/member access
- Simulates authentication sessions without a real backend

Demo mode is useful for prototyping your schema, testing guard behavior across roles, and verifying masking rules before deploying.

```bash title=".env.development"
# No VITE_QUICKBACK_API_URL — CMS runs in demo mode
```

### Mock Data

Place JSON files in `data/mock/` matching your table names:

```
data/mock/
  contact.json       # Array of contact records
  project.json       # Array of project records
  invoice.json       # Array of invoice records
  _meta.json         # Mock session and org metadata
```

Each file contains an array of records. The mock client loads them on startup and persists changes to `localStorage`.

### Role Switcher

In demo mode, the header displays a role switcher dropdown allowing you to switch between `owner`, `admin`, and `member` roles in real time. This lets you verify:

- Which CRUD buttons appear per role
- Which form fields are editable
- Which masking rules apply
- Which actions are available
- Which views are accessible

## Live Mode

Set `VITE_QUICKBACK_API_URL` to connect to a real Quickback API:

```bash title=".env.production"
VITE_QUICKBACK_API_URL=https://api.example.com
```

In live mode, the CMS:

- Reads the **Better Auth session** from cookies
- Fetches the user's **organization membership** and role
- Makes real API calls to the Quickback backend for all CRUD operations
- Applies server-side security (firewall, guards, masking) in addition to client-side UI filtering

## API Client Interface

The CMS communicates with the backend through a standard interface:

```typescript
interface IApiClient {
  list(table: string, params?: ListParams): Promise<ListResult>;
  get(table: string, id: string): Promise<Record | null>;
  create(table: string, data: Record): Promise<Record>;
  update(table: string, id: string, data: Partial<Record>): Promise<Record>;
  delete(table: string, id: string): Promise<void>;
  executeAction(
    table: string,
    actionName: string,
    recordId: string | null,
    input: Record
  ): Promise<Record>;
  listView(
    table: string,
    viewName: string,
    params?: ListParams
  ): Promise<ListResult>;
}
```

### List Parameters

```typescript
interface ListParams {
  page?: number;
  pageSize?: number;
  sort?: string;
  order?: "asc" | "desc";
  search?: string;
  filters?: Record<string, unknown>;
}
```

### List Result

```typescript
interface ListResult {
  data: Record[];
  total: number;
  page: number;
  pageSize: number;
}
```

Both the mock client and the live client implement this same interface. The CMS code is identical in both modes — only the underlying transport changes.

> **Server-Side Security**
>
> In live mode, all four Quickback security layers (firewall, CRUD access, guards, masking) are enforced server-side. The CMS hides unauthorized UI elements as a convenience, but the API rejects unauthorized requests regardless.


## How the Client is Created

The CMS auto-creates the API client based on configuration:

1. **No `VITE_QUICKBACK_API_URL`** — Instantiates the mock client, loads seed data from `data/mock/`, and uses `localStorage` for persistence.
2. **`VITE_QUICKBACK_API_URL` is set** — Instantiates the live client, reads the auth session cookie, and makes fetch requests to the Quickback API using the standard REST endpoints (`/api/v1/{table}`).

The client is provided via React context (`ApiClientContext`) and available throughout the component tree.

## Embedded Mode (Recommended)

Set `cms: true` in your config to embed the CMS directly in your compiled Worker:

```typescript title="quickback/quickback.config.ts"
export default defineConfig({
  name: "my-app",
  cms: true,
  // ...providers
});
```

When you run `quickback compile`, the CLI:
1. Builds the CMS SPA from source and places assets in `src/apps/cms/`
2. Adds `[assets]` config to `wrangler.toml` with `run_worker_first = true`
3. Generates Worker routing to serve the CMS SPA

Run `wrangler dev` and the CMS is served at `/cms/`. API routes (`/api/*`, `/auth/*`, etc.) pass through to your Hono app. Same origin — no CORS, auth cookies work naturally. On a custom domain, the CMS is served at root (`/`).

### Custom CMS Domain

Optionally serve the CMS on a separate subdomain:

```typescript
cms: { domain: "cms.example.com" }
```

This adds a custom domain route to `wrangler.toml`. Both `api.example.com` and `cms.example.com` point to the same Worker. The compiler auto-configures hostname-based routing and cross-subdomain cookies.

CMS access defaults to `access: "admin"` — only `user.role === "admin"` (the Better Auth platform-admin tier) can reach `/cms/*`. Non-admin authenticated users are redirected to `/account/profile`. The gate is enforced at multiple layers:

- **Account UI** hides the "Go to CMS" button for non-admins.
- **CMS SPA shell** — the Worker returns `403` (or redirects unauthenticated requests to login) before serving `index.html` to a non-admin. A non-admin who types the CMS URL directly cannot load the app.
- **`/api/v1/schema`** returns `403 Admin access required` to non-admin callers, so schema metadata can't be scraped by hitting the API directly.
- **`custom_view` routes** are gated on `userRole: ["admin"]` rather than org membership, so saved views can't be listed or modified without admin rights.
- **In-SPA gate** — if any earlier layer is bypassed, the SPA still redirects the user to `/account/profile`.

Your resource API endpoints (the ones generated for your features) are unaffected — they continue to use the per-resource `read`, `create`, `update`, `delete`, and `upsert` access rules you defined.

To allow any authenticated user to access the CMS, opt out explicitly:

```typescript
cms: { access: "user" }
```

See [Multi-Domain Architecture](/compiler/config/domains) for details on multi-domain routing.

## Multi-Tenant vs Single-Tenant

The CMS supports two authentication modes depending on your project's organization configuration.

### Multi-Tenant (Default)

In multi-tenant mode, the CMS gates access behind organization membership:

1. User logs in via Better Auth session
2. CMS fetches the user's organization memberships
3. If the user belongs to multiple organizations, an **org selector** is displayed
4. Once an org is selected, the user's role within that org determines their CMS permissions

This is the default behavior for all Quickback projects.

### Single-Tenant Mode

When your project disables organizations (`features.organizations: false` in config), the compiler sets `singleTenant: true` in the schema registry. In this mode:

- No org gate or org selector is shown
- The user's role is read directly from `user.role` on the session
- All data is unscoped (no `organizationId` filtering)

Single-tenant mode is useful for internal tools, personal projects, or apps that don't need multi-org isolation.

## Next Steps

- **[Table Views](/stack/cms/table-views)** — Browse and Data Table view modes
- **[Security](/stack/cms/security)** — How roles, guards, and masking work in the CMS
