npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@four-leaves/better-auth-multi-tenancy

v1.0.0

Published

A Better Auth plugin for multi-tenancy with hierarchical tenant support

Downloads

93

Readme

better-auth-multi-tenancy

A Better Auth plugin that adds multi-tenancy support with hierarchical tenant structures, member management, and invitation flows.

Features

  • Tenant management — create, read, update, delete, and transfer ownership of tenants
  • Hierarchical tenants — nest tenants under parent tenants to model org structures
  • Member management — add and remove users from tenants directly or via invitation
  • Invitation system — invite users by email; accept, decline, or revoke invitations
  • Lifecycle hooks — granular callbacks for every create, update, delete, and membership event
  • Configurable limits — cap tenants per user, hierarchy depth, and child tenant count
  • Full type safety — server and client plugins with TypeScript inference
  • Database agnostic — works with any Better Auth adapter (SQLite, PostgreSQL, MySQL, …)

Installation

npm install @four-leaves/better-auth-multi-tenancy

Setup

1. Server

Add the plugin to your Better Auth configuration:

import { betterAuth } from 'better-auth'
import { multiTenancy } from '@four-leaves/better-auth-multi-tenancy'

export const auth = betterAuth({
  // ... your existing config
  plugins: [
    multiTenancy({
      // Optional: send invitation emails
      onInvitationCreated: async (invitation, tenantName) => {
        await sendEmail({
          to: invitation.email,
          subject: `You've been invited to ${tenantName}`,
          body: `Accept your invitation at https://yourapp.com/invitations/${invitation.id}/accept`,
        })
      },
    }),
  ],
})

2. Database migration

Run the Better Auth migration to create the tenant, tenantMember, and tenantInvitation tables:

npx better-auth migrate

Or generate the SQL without applying it:

npx better-auth generate

3. Client

Add the client plugin to your auth client:

import { createAuthClient } from 'better-auth/client'
import { multiTenancyClient } from '@four-leaves/better-auth-multi-tenancy/client'

export const authClient = createAuthClient({
  // ... your existing config
  plugins: [multiTenancyClient()],
})

All methods are available under authClient.multiTenancy.*.

API Reference

Tenants

| Method | HTTP | Path | Description | | ------------------- | ---- | --------------------------------- | ------------------------------------------------- | | createTenant | POST | /tenants | Create a new root tenant | | listTenants | GET | /tenants | List tenants the current user is a member of | | getTenant | GET | /tenants/:idOrSlug | Get a tenant by id or slug | | updateTenant | POST | /tenants/:id | Update a tenant's name or slug (owner only) | | transferOwnership | POST | /tenants/:id/transfer-ownership | Transfer ownership to another member (owner only) | | deleteTenant | POST | /tenants/:id/delete | Delete a tenant (owner only) |

Create a tenant

const { data } = await authClient.multiTenancy.createTenant({
  name: 'Acme Corp',
  slug: 'acme-corp',
})

Get a tenant by slug

const { data } = await authClient.multiTenancy.getTenant({
  params: { idOrSlug: 'acme-corp' },
})

Transfer ownership

The new owner must already be a member of the tenant.

await authClient.multiTenancy.transferOwnership({
  params: { id: 'tenant-id' },
  body: { userId: 'new-owner-user-id' },
})

Members

There are two ways to add someone to a tenant:

  • addMember — directly grants membership without any invitation step. Use this when the user is already known (e.g. same company domain). Requires the caller to be the tenant owner.
  • inviteMember — creates a pending invitation that the invitee must explicitly accept. Any existing tenant member can invite. This is the standard onboarding path for external users.

| Method | HTTP | Path | Description | | -------------- | ---- | --------------------------------------------- | -------------------------------------- | | listMembers | GET | /tenants/:tenantId/members | List all members of a tenant | | getMember | GET | /tenants/:tenantId/members/:memberId | Get a single member | | addMember | POST | /tenants/:tenantId/members | Directly add a user by id (owner only) | | inviteMember | POST | /tenants/:tenantId/members/invite | Invite a user by email | | removeMember | POST | /tenants/:tenantId/members/:memberId/remove | Remove a member (owner only) |

Add a member directly (no invitation)

await authClient.multiTenancy.addMember({
  params: { tenantId: 'tenant-id' },
  body: { userId: 'user-id' },
})

Invite a member by email

await authClient.multiTenancy.inviteMember({
  params: { tenantId: 'tenant-id' },
  body: { email: '[email protected]' },
})

Invitations

Invitations are created via inviteMember (see above). The endpoints below manage the lifecycle of existing invitations.

| Method | HTTP | Path | Description | | ------------------- | ---- | ------------------------------------------------------ | --------------------------------- | | listInvitations | GET | /tenants/:tenantId/invitations | List all invitations for a tenant | | getInvitation | GET | /tenants/:tenantId/invitations/:invitationId | Get an invitation by id | | revokeInvitation | POST | /tenants/:tenantId/invitations/:invitationId/revoke | Cancel a pending invitation | | acceptInvitation | POST | /tenants/:tenantId/invitations/:invitationId/accept | Accept an invitation | | declineInvitation | POST | /tenants/:tenantId/invitations/:invitationId/decline | Decline an invitation |

Accept an invitation

await authClient.multiTenancy.acceptInvitation({
  params: {
    tenantId: 'tenant-id',
    invitationId: 'invitation-id',
  },
})

The caller's email must match the invited email. On success, a tenantMember record is created automatically.

Child Tenants

Child tenants use the parentTenantId field to model a hierarchy. Only the parent tenant's owner can create, update, or delete child tenants.

| Method | HTTP | Path | Description | | ------------------- | ---- | --------------------------------------------- | ----------------------------------------- | | listChildTenants | GET | /tenants/:tenantId/children | List direct children of a tenant | | getChildTenant | GET | /tenants/:tenantId/children/:childId | Get a single child tenant | | createChildTenant | POST | /tenants/:tenantId/children | Create a child tenant (parent owner only) | | updateChildTenant | POST | /tenants/:tenantId/children/:childId | Update a child tenant (parent owner only) | | deleteChildTenant | POST | /tenants/:tenantId/children/:childId/delete | Delete a child tenant (parent owner only) |

Create a child tenant

const { data } = await authClient.multiTenancy.createChildTenant({
  params: { tenantId: 'parent-tenant-id' },
  body: {
    name: 'Acme Engineering',
    slug: 'acme-engineering',
  },
})

Plugin Options

All options are optional. The plugin works with zero configuration.

multiTenancy({
  // ── Limits ──────────────────────────────────────────────────────────────────

  /**
   * Maximum number of tenants a single user can own simultaneously.
   * Counts only tenants where the user is the ownerId.
   */
  maxTenantsPerUser: 5,

  /**
   * Maximum nesting depth for the tenant hierarchy.
   * 1 = children only (no grandchildren). 2 = children + grandchildren. Etc.
   */
  maxHierarchyDepth: 3,

  /**
   * Maximum total descendants allowed under a root tenant tree.
   * Enforced at the root ancestor level — adding a child anywhere in the tree
   * counts against the root's quota. Suitable for plan/billing caps.
   */
  maxChildTenantsPerTenant: 10,

  // ── Generic tenant hooks ─────────────────────────────────────────────────────

  /** Called after any tenant is created (root or child). */
  onTenantCreated: async (tenant, userId) => { ... },

  /** Called after any tenant's name or slug is updated (root or child). */
  onTenantUpdated: async (tenant) => { ... },

  /** Called after any tenant is deleted (root or child). */
  onTenantDeleted: async (tenant) => { ... },

  /** Called after ownership is transferred. `previousOwnerId` is the old owner. */
  onOwnershipTransferred: async (tenant, previousOwnerId) => { ... },

  // ── Root-tenant-specific hooks ───────────────────────────────────────────────
  // Fire in addition to the generic hooks above, only for root tenants.

  onRootTenantCreated: async (tenant, userId) => { ... },
  onRootTenantUpdated: async (tenant) => { ... },
  onRootTenantDeleted: async (tenant) => { ... },

  // ── Child-tenant-specific hooks ──────────────────────────────────────────────
  // Fire in addition to the generic hooks above, only for child tenants.

  onChildTenantCreated: async (tenant, userId) => { ... },
  onChildTenantUpdated: async (tenant) => { ... },
  onChildTenantDeleted: async (tenant) => { ... },

  // ── Member hooks ─────────────────────────────────────────────────────────────

  /**
   * Called after a user is added to a tenant via addMember or acceptInvitation.
   * Does not fire for the creator being auto-added during tenant creation.
   */
  onMemberAdded: async (member) => { ... },

  /** Called after a member is removed from a tenant. */
  onMemberRemoved: async (member) => { ... },

  // ── Invitation hooks ─────────────────────────────────────────────────────────

  /**
   * Called after an invitation is created. Implement this to send invitation emails.
   * Without this hook the invitation is persisted but the invitee receives no notification.
   */
  onInvitationCreated: async (invitation, tenantName) => { ... },

  /** Called after a pending invitation is revoked by a tenant member. */
  onInvitationRevoked: async (invitation) => { ... },

  /**
   * Called after an invitation is accepted. Fires in addition to onMemberAdded.
   * Both the updated invitation and the new member record are provided.
   */
  onInvitationAccepted: async (invitation, member) => { ... },

  /** Called after an invitation is declined by the invitee. */
  onInvitationDeclined: async (invitation) => { ... },
})

Sending Invitation Emails

The plugin does not send emails itself. Wire in your email provider via onInvitationCreated:

import { Resend } from 'resend'

const resend = new Resend(process.env.RESEND_API_KEY)

multiTenancy({
  onInvitationCreated: async (invitation, tenantName) => {
    await resend.emails.send({
      from: '[email protected]',
      to: invitation.email,
      subject: `You've been invited to join ${tenantName}`,
      html: `
        <p>You've been invited to join <strong>${tenantName}</strong>.</p>
        <p>
          <a href="https://yourapp.com/invitations/${invitation.id}/accept">
            Accept invitation
          </a>
        </p>
      `,
    })
  },
})

Hierarchical Tenants

A tenant can be nested under a parent via parentTenantId. There is no limit on depth by default; use maxHierarchyDepth to cap it, and maxChildTenantsPerTenant to cap the total number of descendants under a root tenant.

Acme Corp (root)
├── Engineering
│   ├── Frontend
│   └── Backend
└── Sales

Creating this structure:

const { data: acme } = await authClient.multiTenancy.createTenant({
  name: 'Acme Corp',
  slug: 'acme',
})

const { data: eng } = await authClient.multiTenancy.createChildTenant({
  params: { tenantId: acme.tenant.id },
  body: { name: 'Engineering', slug: 'acme-engineering' },
})

await authClient.multiTenancy.createChildTenant({
  params: { tenantId: eng.tenant.id },
  body: { name: 'Frontend', slug: 'acme-frontend' },
})

Deleting a parent tenant sets parentTenantId to null on its direct children — they become root tenants. Grandchildren and deeper descendants are unaffected.

Database Schema

The plugin creates three tables:

| Table | Description | | ------------------ | ----------------------------------------------------------- | | tenant | Stores tenant metadata (name, slug, creator, owner, parent) | | tenantMember | Links users to tenants | | tenantInvitation | Tracks pending, accepted, and declined invitations |

License

MIT — see LICENSE.