Skip to content

Organization Plugin

Multi-tenant organizations with teams, members, roles, and invitations.

Installation

Add the organization plugin to your config:

typescript
// nevr.config.ts
import { defineConfig } from "nevr"
import { organization } from "nevr/plugins/organization"

export const config = defineConfig({
  database: "postgresql",
  entities: [],
  plugins: [
    organization({
      allowUserToCreate: true,
      creatorRole: "owner",
      teams: { enabled: true },
    }),
  ],
})

export default config

Then in your server:

typescript
// src/server.ts
import { nevr } from "nevr"
import { prisma } from "nevr/drivers/prisma"
import { PrismaClient } from "@prisma/client"
import { config } from "./nevr.config.js"

const api = nevr({ ...config, driver: prisma(new PrismaClient()) })

Generate and push or migrate the database

bash
npx nevr generate    # Generates user + session tables
npx nevr db:push     # Push to database
# or
npx nevr db:migrate  # Create migration files

Client Setup

typescript
import { createClient } from "nevr/client"
import { organizationClient } from "nevr/plugins/organization/client"
import type { API } from "./api"

const client = createClient<API>()({
  baseURL: "/api",
  plugins: [organizationClient()],
})

Configuration

typescript
organization({
  // Allow users to create organizations
  allowUserToCreate: true,

  // Maximum organizations per user
  maxOrgsPerUser: 10,

  // Maximum members per organization
  maxMembersPerOrg: 100,

  // Role assigned to organization creator
  creatorRole: "owner",

  // Custom roles with permissions
  roles: {
    owner: {
      name: "Owner",
      permissions: {
        organization: ["create", "read", "update", "delete"],
        member: ["create", "read", "update", "delete"],
        invitation: ["create", "read", "update", "delete"],
        team: ["create", "read", "update", "delete"],
      },
    },
    admin: {
      name: "Admin",
      permissions: {
        organization: ["read", "update"],
        member: ["create", "read", "update", "delete"],
        invitation: ["create", "read", "update", "delete"],
        team: ["create", "read", "update", "delete"],
      },
    },
    member: {
      name: "Member",
      permissions: {
        organization: ["read"],
        member: ["read"],
        invitation: ["read"],
        team: ["read"],
      },
    },
  },

  // Enable teams feature
  teams: {
    enabled: true,
    maxTeams: 10,
    maxMembersPerTeam: 50,
  },

  // Invitation settings
  invitation: {
    expiresIn: 7 * 24 * 60 * 60, // 7 days
    maxPending: 50,
    sendEmail: async ({ invitation, organization, inviter, acceptUrl }) => {
      // Send invitation email
    },
  },

  // Lifecycle hooks
  hooks: {
    afterCreate: async ({ organization, member }) => {
      console.log(`Created ${organization.name}`)
    },
  },
})

Endpoints

Organization CRUD (7 endpoints)

MethodPathDescription
POST/organization/create-organizationCreate organization
POST/organization/update-organizationUpdate organization
POST/organization/delete-organizationDelete organization
POST/organization/set-active-organizationSet active org
GET/organization/get-full-organizationGet org with members
GET/organization/list-organizationsList user's orgs
GET/organization/check-organization-slugCheck slug availability

Member Management (7 endpoints)

MethodPathDescription
POST/organization/add-memberAdd member
POST/organization/remove-memberRemove member
POST/organization/update-member-roleUpdate role
GET/organization/get-active-memberGet current membership
GET/organization/get-active-member-roleGet role with permissions
GET/organization/list-membersList members
POST/organization/leave-organizationLeave organization

Invitations (7 endpoints)

MethodPathDescription
POST/organization/create-invitationCreate invitation
POST/organization/accept-invitationAccept invitation
POST/organization/reject-invitationReject invitation
POST/organization/cancel-invitationCancel invitation
GET/organization/get-invitationGet invitation
GET/organization/list-invitationsList org invitations
GET/organization/list-user-invitationsList user's invitations

Teams (9 endpoints) - when enabled

MethodPathDescription
POST/organization/create-teamCreate team
POST/organization/remove-teamDelete team
POST/organization/update-teamUpdate team
GET/organization/list-organization-teamsList teams
POST/organization/set-active-teamSet active team
GET/organization/list-user-teamsList user's teams
GET/organization/list-team-membersList team members
POST/organization/add-team-memberAdd to team
POST/organization/remove-team-memberRemove from team

Access Control (6 endpoints)

MethodPathDescription
POST/organization/create-org-roleCreate custom role
POST/organization/delete-org-roleDelete role
GET/organization/list-org-rolesList roles
GET/organization/get-org-roleGet role
POST/organization/update-org-roleUpdate role
POST/organization/has-permissionCheck permission

Client SDK

The organization plugin uses the unified client pattern with createClient:

typescript
import { createClient } from "nevr/client"
import { authClient } from "nevr/plugins/auth/client"
import { organizationClient } from "nevr/plugins/organization/client"
import type { API } from "./api"

// Use curried pattern for full type inference
export const client = createClient<API>()({
  baseURL: "http://localhost:3000",
  plugins: [
    authClient(),
    organizationClient(),
  ],
})

// Create organization (namespaced under `org`)
const { data } = await client.org.create({
  name: "Acme Inc",
  slug: "acme",
})

// Reactive state
client.$store.atoms.activeOrganization.subscribe(({ organization, member }) => {
  console.log("Active:", organization?.name)
})

// Set active organization
await client.org.setActive(data.organization.id)

// Invite member
await client.org.invite({
  organizationId: orgId,
  email: "teammate@example.com",
  role: "admin",
})

// Create team
await client.org.createTeam({
  organizationId: orgId,
  name: "Engineering",
})

// Check permission
const { data: result } = await client.org.hasPermission({
  permission: { member: ["create"] },
})

Reactive State

The client plugin provides reactive atoms:

AtomDescription
$activeOrganizationCurrent org + member + loading state
$organizationsList of user's organizations
typescript
// React usage with nanostores
import { useStore } from "@nanostores/react"

function OrgSwitcher() {
  const { organization, isPending } = useStore(client.$store.atoms.activeOrganization)
  const { data: orgs } = useStore(client.$store.atoms.organizations)

  if (isPending) return <Loading />

  return (
    <select 
      value={organization?.id} 
      onChange={(e) => client.org.setActive(e.target.value)}
    >
      {orgs.map(org => <option key={org.id} value={org.id}>{org.name}</option>)}
    </select>
  )
}

Type Inference

The plugin exports types for SDK inference:

typescript
import type { InferPlugin } from "nevr"
import { organization } from "nevr/plugins/organization"

type OrgPlugin = InferPlugin<typeof organization>

// Available types:
// OrgPlugin["Organization"]
// OrgPlugin["Member"]
// OrgPlugin["Team"]
// OrgPlugin["Invitation"]
// OrgPlugin["RoleDefinition"]

Schema

Organization

FieldTypeDescription
idstringUnique identifier
namestringOrganization name
slugstringURL-friendly identifier
logostring?Logo URL
metadatajson?Custom metadata
createdAtdatetimeCreation timestamp

Member

FieldTypeDescription
idstringUnique identifier
userIdstringUser reference
organizationIdstringOrganization reference
rolestringMember role
createdAtdatetimeJoin timestamp

Invitation

FieldTypeDescription
idstringUnique identifier
emailstringInvitee email
organizationIdstringOrganization reference
rolestringAssigned role
statusstringpending, accepted, rejected, canceled
expiresAtdatetimeExpiration
inviterIdstringInviter user ID

Team

FieldTypeDescription
idstringUnique identifier
namestringTeam name
organizationIdstringOrganization reference
createdAtdatetimeCreation timestamp

Released under the MIT License.