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

Use a Real Organization ID

pinnedOrganizationId must point at an organization that actually exists in your Better Auth data. The compiler does not create the organization for you.

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.).
export default defineActions({
  publish: {
    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: "<real-org-id>".
  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