Leave Localhost logoLeave LocalhostDocs
Multi-tenancy

Multi-Tenancy Overview

Leave Localhost is designed as a B2B multi-tenant SaaS. It uses the concept of Organizations (often called Workspaces or Teams) to group users, data, and billing subscriptions.

Leave Localhost is designed as a B2B multi-tenant SaaS. It uses the concept of Organizations (often called Workspaces or Teams) to group users, data, and billing subscriptions.

Authentication and organization management are powered by Better Auth's Organization Plugin.

The Data Model

In the multi-tenant model:

  • Users belong to one or more Organizations as Members.
  • Data (records, settings, files) belongs to an Organization, not a User.
  • Billing is tied to an Organization, not a User.
  • A user acts on behalf of their Active Organization (workspace switcher).

Personal vs Team Mode

The starter can operate in two modes, controlled by packages/backend/convex/organizations/config.ts:

Team Mode (Default)

  • Users get a "Personal Workspace" on signup.
  • Users can create new shared "Team Workspaces".
  • Users can be invited to other workspaces.
  • Best for B2B SaaS (Slack, Linear, Vercel).

Personal Mode

  • Users get a single "Personal Workspace" on signup.
  • Creation of additional workspaces is disabled.
  • Invitations are disabled.
  • Member management is disabled.
  • Best for B2C SaaS or prosumer tools where users don't collaborate.

See Switching to Personal Mode for how to toggle this.

Organization Lifecycle

  1. Creation: When a user signs up, the ensureDefaultOrganization mutation automatically creates a Personal Workspace for them and sets it as active.
  2. Team Creation: If team mode is on, users can create additional workspaces via the createWorkspace mutation.
  3. Invitation: Users can invite others to join their workspaces via email.
  4. Deletion: Owners can delete an organization, which cascades to delete its data, subscriptions, and memberships.

The Active Organization

At any given time, a user is acting within their "Active Organization". This is stored in two places to keep the frontend and backend in sync:

  1. Better Auth Session: session.activeOrganizationId
  2. Convex User: users.activeOrganizationId

The setActiveOrganization mutation safely switches the context, ensuring the user is a valid member of the target organization.

Security and Scoping

All backend queries and mutations must filter data by the active organization. The requireAppPermission helper makes this safe by returning the verified organizationId:

const actor = await requireAppPermission(ctx, {
  permission: "workspace.records.read",
});

const records = await ctx.db
  .query("workspace_records")
  // ALWAYS scope by the actor's organization
  .withIndex("by_organizationId", (q) =>
    q.eq("organizationId", actor.organizationId),
  )
  .collect();

Next Reads

On this page