Quickback Docs

Pinned Organization Mode

Pin every authenticated request to one organization id while keeping org-backed auth, roles, and CMS support.

Single-tenant mode has been removed. If you want a one-org deployment without org switching overhead, use features.pinnedOrganizationId instead.

Pinned organization mode keeps Better Auth's organization plugin, organization membership roles, and org-scoped firewalls enabled. The only difference is that Quickback hard-codes one active organization id at runtime and hides the switching UI.

Enabling Pinned Organization Mode

Set a real organization id in your config:

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

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

The organization must exist

pinnedOrganizationId is the Better Auth organization id the compiled Worker hard-codes. The compiler does not insert that row.

A browser deploy from Quickback Start does: the broker writes organization.id as this exact string, seeds user.role = "sysadmin", and seats that user as owner of the org. org_default is a valid id — use it when you do not have a pre-existing org.

A CLI deploy (quickback deploy) does not seed AUTH_DB. Create the organization yourself (or deploy once through Start) so the pin points at a real row.

Removed Single-Tenant Settings

These older settings now fail the compile with a migration error:

  • features.organizations: false
  • providers.database.config.organizations: false
  • providers.auth.config.plugins.organization = false

Replace them with features.pinnedOrganizationId.

What Changes

AspectDefault Org ModePinned Organization Mode
Organization pluginEnabledEnabled
Role sourceOrg membership tableOrg membership table in the pinned org
Firewall scopesowner, organization, team, exceptionSame
ctx.activeOrgIdFrom session / JWT / API key contextAlways the configured org id
CMS org selectorVisible when caller can switch orgsHidden
CMS org gateRequires an active org selectionBypassed; pinned org is injected automatically

Access and Roles

Pinned organization mode does not bring back the old user.role mirror on ctx.roles.

  • Use roles: ["owner" | "admin" | "member"] for data-plane access based on membership in the pinned org.
  • Use userRole: [...] for platform-wide Better Auth roles stored on user.role (appmanager, sysadmin, etc.).
// quickback/features/posts/actions/publish.ts
import { z } from "zod";
import { defineAction } from "../.quickback/define-action";

export default defineAction({
  description: "Publish a post, for either a pinned-org admin or a platform app manager.",
  input: z.object({}),
  access: {
    or: [
      { roles: ["admin"] },           // admin in the pinned org
      { userRole: ["appmanager"] },   // platform control-plane role
    ],
  },
  execute: async () => ({ ok: true }),
});

Firewalls and Resource Scopes

Organization-scoped resources keep working in pinned mode. q.scope("organization"), q.scope("team"), and any firewall predicate that references ctx.activeOrgId still compile and run normally — they just see the pinned organization id instead of a user-selected one.

Owner-scoped and public resources are unchanged.

CMS Behavior

When features.pinnedOrganizationId is set:

  • the CMS hides the org switcher
  • the CMS skips the org-selection gate
  • API calls and realtime connections use the pinned org automatically
  • role checks still come from the caller's membership in that org

This is the intended replacement for the old single-tenant CMS flow.

Migration from Single-Tenant Mode

  1. Remove features.organizations: false.
  2. Add features.pinnedOrganizationId: "<org-id>". On a Start deploy, that string is the org the broker inserts — "org_default" is fine. On a CLI deploy, it must already exist in AUTH_DB.
  3. Remove any assumptions that ctx.roles mirrors user.role.
  4. If you need platform-wide roles, keep those checks on userRole, not roles.

Consuming the API

Pinned organization mode still produces the same JSON API shape. Pair it with any frontend framework:

const posts = await fetch("https://api.mysite.com/api/v1/posts").then((r) => r.json());

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: "..." }),
});

On this page