Quickback Docs

Connecting

Run the CMS in demo mode with mock data or connect it to a live Quickback API.

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.

.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:

.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:

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

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

List Result

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.

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

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:

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.

You can also restrict CMS access to admin users only:

cms: { domain: "cms.example.com", access: "admin" }

When access: "admin" is set, the CMS is gated on user.role === "admin" 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 CRUD is 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 renders an "Access Denied" screen.

Your resource CRUD endpoints (the ones generated for your features) are unaffected — they continue to use the per-resource crud.access.roles you defined.

See Multi-Domain Architecture 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 — Browse and Data Table view modes
  • Security — How roles, guards, and masking work in the CMS

On this page