Skip to content
Report library
Purpose / Other

Organization Best Practices Skill Security Audit

What the author says it does (original text)

Configure multi-tenant organizations, manage members and invitations, define custom roles and permissions, set up teams, and implement RBAC using Better Auth's organization plugin. Use when users need org setup, team management, member roles, access control, or the Better Auth organization plugin.

Independent security check

Do not install or run it yet

Files checked
1
Risks found
5
Could it run dangerous commands?Looks for programs run straight after downloading, remote control of your computer, and hidden commands.Risks found: 1
Medium risk

Setup runs an unpinned remote CLI that can directly migrate the database

Source references: 3
What we found

`npx auth@latest migrate` selects the newest package code available at execution time and then makes persistent database-schema changes. The guide does not require a pinned, reviewed version, migration preview, or backup first.

Why this matters

If the latest package is compromised, changes behavior, or generates an incompatible migration, the CLI can execute with the developer's privileges and may cause downtime, schema damage, or unavailable data.

The guide tells the user to run an unpinned `npx` CLI for a database migration and then verify that new tables exist, showing that the step changes schema state. It provides no migration preview, backup, or rollback guidance. Before running it, the user can request a pinned version, generated SQL, backup and rollback steps, and test it against a non-production database.

SKILL.md:8In the instructionsOpen original file
1. Add `organization()` plugin to server config2. Add `organizationClient()` plugin to client config3. Run `npx auth@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma4. Verify: check that organization, member, invitation tables exist in your database
Show 2 other places
SKILL.md:10In the instructionsOpen original file
2. Add `organizationClient()` plugin to client config3. Run `npx auth@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma4. Verify: check that organization, member, invitation tables exist in your database
SKILL.md:11In the instructionsOpen original file
3. Run `npx auth@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma4. Verify: check that organization, member, invitation tables exist in your database
Could it expose your files or keys?Looks for uploads of files containing passwords or keys, and keys written directly in the code.Risks found: 1
Medium risk

Invitation templates interpolate account and organization names into raw HTML

Source references: 4
What we found

The template inserts `inviter.user.name` and `organization.name` directly into an `html` string without HTML escaping. Organization names can be supplied during creation; if these values contain attacker-provided markup, the email body may be altered.

Why this matters

An attacker could place misleading links, images, or text inside an invitation sent by the trusted application, potentially directing recipients to phishing pages or inducing sensitive disclosure. The result depends on whether the mailer or email client performs additional sanitization.

The organization name comes from a creation request, while the invitation email inserts that name and the inviter's name directly into an HTML string without showing escaping or safe template encoding. If those fields can contain HTML, an attacker could alter or spoof the rendered email content. The source does not establish that an email client would execute scripts, so the supported risk is HTML-content manipulation. The user can ask for documented field constraints and a default-escaping template or context-aware encoding for every interpolation.

SKILL.md:43In the instructionsOpen original file
```tsconst createOrg = async () => {  const { data, error } = await authClient.organization.create({    name: "My Company",    slug: "my-company",    logo: "https://example.com/logo.png",    metadata: { plan: "pro" },  });};
Show 3 other places
SKILL.md:164In the instructionsOpen original file
    organization({      sendInvitationEmail: async (data) => {        const { email, organization, inviter, invitation } = data;        await sendEmail({          to: email,          subject: `Join ${organization.name}`,          html: `            <p>${inviter.user.name} invited you to join ${organization.name}</p>            <a href="https://yourapp.com/accept-invite?id=${invitation.id}">              Accept Invitation            </a>          `,        });      },
SKILL.md:45In the instructionsOpen original file
const createOrg = async () => {  const { data, error } = await authClient.organization.create({    name: "My Company",    slug: "my-company",    logo: "https://example.com/logo.png",    metadata: { plan: "pro" },  });
SKILL.md:165In the instructionsOpen original file
      sendInvitationEmail: async (data) => {        const { email, organization, inviter, invitation } = data;        await sendEmail({          to: email,          subject: `Join ${organization.name}`,          html: `            <p>${inviter.user.name} invited you to join ${organization.name}</p>            <a href="https://yourapp.com/accept-invite?id=${invitation.id}">              Accept Invitation            </a>          `,        });
Could it delete files or keep running?Looks for broad file deletion, disk overwrites, and programs set to start automatically.No risks found
Could it bypass safety checks?Looks for skipped website security checks, excessive file access, or actions that skip your approval.Risks found: 3
High risk

Server-side administration examples omit caller authorization checks

Source references: 5
What we found

The examples use server APIs to create an organization for an arbitrary `userId` and directly add an arbitrary user to a specified organization, but show no check that the caller is a system administrator or may manage the target organization. Calling the actor an administrator in prose does not enforce that permission.

Why this matters

If these calls are wrapped in an endpoint ordinary users can trigger, an attacker could create organizations for accounts, bypass the invitation flow, or add accounts to tenants they should not access.

What this evidence establishes

The guide says delegated organization creation is server-side and for administrators, but the snippet does not show how administrator status is enforced; the add-member example also accepts user, role, and organization IDs directly. If exposed through an unprotected route, callers could create organizations, choose an owner, or alter membership. The source does not show the surrounding route or establish that Better Auth lacks internal checks, so arbitrary access is not proven. The user should request explicit server-side caller and target-organization authorization checks.

This assessment concerns the code and conditions shown, not proof that harm has occurred.
SKILL.md:70In the instructionsOpen original file
### Creating Organizations on Behalf of UsersAdministrators can create organizations for other users (server-side only):```tsawait auth.api.createOrganization({  body: {    name: "Client Organization",    slug: "client-org",    userId: "user-id-who-will-be-owner", // `userId` is required  },});```**Note**: The `userId` parameter cannot be used alongside session headers.
Show 4 other places
SKILL.md:105In the instructionsOpen original file
### Adding Members (Server-Side)```tsawait auth.api.addMember({  body: {    userId: "user-id",    role: "member",    organizationId: "org-id",  },});```For client-side member additions, use the invitation system instead.
SKILL.md:72In the instructionsOpen original file
Administrators can create organizations for other users (server-side only):
SKILL.md:75In the instructionsOpen original file
```tsawait auth.api.createOrganization({  body: {    name: "Client Organization",    slug: "client-org",    userId: "user-id-who-will-be-owner", // `userId` is required  },});
SKILL.md:108In the instructionsOpen original file
```tsawait auth.api.addMember({  body: {    userId: "user-id",    role: "member",    organizationId: "org-id",  },});
Medium risk

Session-scoped active organization can direct actions to the wrong tenant

Source references: 5
What we found

The active organization is stored in the session, and endpoints such as member invitations use it when `organizationId` is omitted. The invitation example omits that identifier; after organization switching, or across tabs sharing a session, code may use an unexpected or stale tenant context.

Why this matters

A user who administers multiple organizations could send invitations or other management actions to the wrong tenant, causing unintended membership, privacy exposure, or configuration changes. A permission check may confirm authority over that tenant without establishing that it was the intended tenant.

The guide says the active organization is stored in the session and that endpoints including member invitations use it when `organizationId` is omitted; its invitation example does omit that ID. If session state differs from the tenant shown or intended by the UI, the invitation could target the wrong organization. The source does not prove that multiple tabs necessarily cause this, but the implicit scope is a plausible risk. The user can require sensitive writes to include an explicit organization ID and verify it server-side.

SKILL.md:87In the instructionsOpen original file
## Active OrganizationsStored in the session and scopes subsequent API calls. Set after user selects one.```tsconst setActive = async (organizationId: string) => {  const { data, error } = await authClient.organization.setActive({    organizationId,  });};```Many endpoints use the active organization when `organizationId` is not provided (`listMembers`, `listInvitations`, `inviteMember`, etc.).
Show 4 other places
SKILL.md:183In the instructionsOpen original file
### Sending Invitations```tsawait authClient.organization.inviteMember({  email: "newuser@example.com",  role: "member",});```
SKILL.md:89In the instructionsOpen original file
Stored in the session and scopes subsequent API calls. Set after user selects one.
SKILL.md:99In the instructionsOpen original file
Many endpoints use the active organization when `organizationId` is not provided (`listMembers`, `listInvitations`, `inviteMember`, etc.).
SKILL.md:186In the instructionsOpen original file
```tsawait authClient.organization.inviteMember({  email: "newuser@example.com",  role: "member",});```
Low risk

The install command does not pin a dependency version

Source references: 1
What we found

The installation command does not specify dependency versions. The same command may download different code later, so what you install can differ from what was checked.

Why this matters

A later install may download different code even though the command and this report have not changed.

The setup explicitly uses `@latest`, so separate runs may fetch different remote package versions. A breaking or compromised upstream release could therefore run without the user pinning and reviewing the exact version. The user can ask for a tested fixed version and inspect the package and migration in a restricted environment first.

SKILL.md:10In the instructionsOpen original file
2. Add `organizationClient()` plugin to client config3. Run `npx auth@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma4. Verify: check that organization, member, invitation tables exist in your database
Could it mislead the AI or hide text?Checks the skill instructions for requests to ignore you, influence the report, or hide text in invisible characters.No risks found
Could it change links or payment recipients without asking?Looks for forced referral or payment changes combined with instructions to hide the change.No risks found

Inside this skill

8 instruction sections

This is a configuration guide for Better Auth's organization plugin; its contents are examples to be copied into an application, not an implementation that runs automatically in the supplied source.

View source
SKILL.md:2In the instructionsOpen original file
---name: organization-best-practicesdescription: Configure multi-tenant organizations, manage members and invitations, define custom roles and permissions, set up teams, and implement RBAC using Better Auth's organization plugin. Use when users need org setup, team management, member roles, access control, or the Better Auth organization plugin.---

The guide covers organization creation, direct member addition, invitations, role permissions, teams, lifecycle hooks, and organization deletion; these operations can change tenant membership and persistent data.

View source
SKILL.md:103In the instructionsOpen original file
## Members### Adding Members (Server-Side)```tsawait auth.api.addMember({  body: {    userId: "user-id",    role: "member",    organizationId: "org-id",  },});```
SKILL.md:406In the instructionsOpen original file
### Organization DeletionDeleting an organization removes all associated data (members, invitations, teams). Prevent accidental deletion:

The guide correctly distinguishes static role checks used for UI rendering from the dynamic permission endpoint, and documents an option to disable organization deletion.

View source
SKILL.md:232In the instructionsOpen original file
Use `checkRolePermission({ role, permissions })` for client-side UI rendering (static only). For dynamic access control, use the `hasPermission` endpoint.
SKILL.md:410In the instructionsOpen original file
```tsorganization({  disableOrganizationDeletion: true, // Disable via config});```
Start here · InstructionsSKILL.md
organization-best-practices
Lines connect the instruction file to its sections, not an observed execution order. Select a section to read the source. 4 more sections are available in the original file.
Files and check records1 files

Coverage and gaps

Content covered in each file

These are the source ranges included in this check, not a guarantee that every issue has been resolved.

  • SKILL.mdFull text included

This report is for the version above. We read the available code and instructions without running the skill or checking extra packages it installs. This is not a promise of safety: a different version or setup may behave differently.

  • SKILL.mdInstructions

Operations mentioned in code and instructions

Install extra software packages
SKILL.md:10In the instructionsOpen original file
2. Add `organizationClient()` plugin to client config3. Run `npx auth@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma4. Verify: check that organization, member, invitation tables exist in your database
Connect to websites
SKILL.md:48In the instructionsOpen original file
    slug: "my-company",    logo: "https://example.com/logo.png",    metadata: { plan: "pro" },
SKILL.md:172In the instructionsOpen original file
            <p>${inviter.user.name} invited you to join ${organization.name}</p>            <a href="https://yourapp.com/accept-invite?id=${invitation.id}">              Accept Invitation
SKILL.md:198In the instructionsOpen original file
  role: "member",  callbackURL: "https://yourapp.com/dashboard",});
Lines read
480
File checksum (to compare versions)
1fa18fa9554155e1b1b2c866f7c2a1cbc0d363425bdaa98afd05ada1fcf9a2e0