Just-Use-Convex Docs
Authentication

Organizations

Multi-tenant organization management with Better Auth

Organizations

Organizations enable multi-tenant functionality where users can belong to multiple organizations and switch between them.

Organization Model

Each organization has:

  • Unique ID and name
  • Owner (user who created it)
  • Members with roles
  • Teams (optional sub-groups)

Creating Organizations

On Sign Up (Automatic)

A personal organization is created automatically when a user signs up:

// In auth.ts databaseHooks
user: {
  create: {
    after: async (user) => {
      await createPersonalOrganization(user);
    },
  },
},

Manual Creation

import { authClient } from "@/lib/auth";

async function createOrganization(name: string) {
  const result = await authClient.organization.create({
    name,
    slug: name.toLowerCase().replace(/\s+/g, "-"),
  });

  if (result.error) {
    throw new Error(result.error.message);
  }
  return result.data;
}

Switching Organizations

async function switchOrganization(organizationId: string) {
  await authClient.organization.setActive({
    organizationId,
  });
  // Session now has new activeOrganizationId
  // All Convex queries will use the new org context
}

Organization Switcher Component

function OrganizationSwitcher() {
  const { data: session } = useSession();
  const { data: organizations } = authClient.useListOrganizations();
  const activeOrg = session?.session.activeOrganizationId;

  return (
    <select
      value={activeOrg || ""}
      onChange={(e) => switchOrganization(e.target.value)}
    >
      {organizations?.map((org) => (
        <option key={org.id} value={org.id}>
          {org.name}
        </option>
      ))}
    </select>
  );
}

Data Isolation

All queries automatically scope to the active organization:

export const list = zQuery({
  args: z.object({}),
  handler: async (ctx) => {
    // Only returns todos from active org
    return await ctx
      .table("todos", "organizationId", (q) =>
        q.eq("organizationId", ctx.identity.activeOrganizationId)
      )
      .collect();
  },
});

Creating Org-Scoped Data

export const create = zMutation({
  args: z.object({ title: z.string() }),
  handler: async (ctx, args) => {
    return await ctx.table("todos").insert({
      title: args.title,
      status: "todo",
      organizationId: ctx.identity.activeOrganizationId,
      userId: ctx.identity.userId,
      updatedAt: Date.now(),
    });
  },
});

Inviting Members

async function inviteUser(email: string, role: "member" | "admin") {
  const result = await authClient.organization.inviteMember({
    email,
    role,
  });

  if (result.error) {
    throw new Error(result.error.message);
  }
  // Invitation email sent
}

Listing Members

const { data: members } = await authClient.organization.listMembers();
// [{ userId, role, user: { name, email } }, ...]

Best Practices

1. Always Validate Org Access

const todo = await ctx.table("todos").get(todoId);
if (todo.organizationId !== ctx.identity.activeOrganizationId) {
  throw new Error("Unauthorized");
}

2. Index by Organization

.index("organizationId", ["organizationId"])
.index("organizationId_status", ["organizationId", "status"])

Next, learn about Teams.