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: falseproviders.database.config.organizations: falseproviders.auth.config.plugins.organization = false
Replace them with features.pinnedOrganizationId.
What Changes
| Aspect | Default Org Mode | Pinned Organization Mode |
|---|---|---|
| Organization plugin | Enabled | Enabled |
| Role source | Org membership table | Org membership table in the pinned org |
| Firewall scopes | owner, organization, team, exception | Same |
ctx.activeOrgId | From session / JWT / API key context | Always the configured org id |
| CMS org selector | Visible when caller can switch orgs | Hidden |
| CMS org gate | Requires an active org selection | Bypassed; 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 onuser.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
- Remove
features.organizations: false. - Add
features.pinnedOrganizationId: "<real-org-id>". - Remove any assumptions that
ctx.rolesmirrorsuser.role. - If you need platform-wide roles, keep those checks on
userRole, notroles.
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: "..." }),
});