We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide30 min read

Kinde

Kinde is a modern auth platform with generous free tier (10,500 MAU), feature flags, organizations, and analytics. Developer-friendly alternative to Auth0.

Kinde - Kompletny Przewodnik po Modern Auth Platform

Czym jest Kinde?

Kinde to nowoczesna platforma do autentykacji i autoryzacji, która wyróżnia się bardzo generous free tier (10,500 MAU) i developer-friendly podejściem. W przeciwieństwie do tradycyjnych rozwiązań auth, Kinde oferuje w jednym pakiecie nie tylko authentication, ale także feature flags, organizations dla multi-tenancy i wbudowaną analitykę.

Założony w 2021 roku w Australii, Kinde szybko zyskał popularność wśród startupów i średnich firm szukających alternatywy dla Auth0 i Clerk. Platforma wyróżnia się prostotą integracji, doskonałym SDK dla Next.js i React, oraz transparentnym pricingiem.

Kluczowe cechy Kinde:

  • 10,500 MAU za darmo - Najbardziej generous free tier na rynku
  • Feature Flags - Wbudowane bez dodatkowych narzędzi
  • Organizations - Native multi-tenancy support
  • Permissions & Roles - Granularny RBAC
  • Wiele metod auth - Social, Email, Phone, Enterprise SSO
  • SDK dla wszystkiego - Next.js, React, React Native, Node.js, Python

Dlaczego Kinde?

Kluczowe zalety

  1. Generous free tier - 10,500 MAU bez karty kredytowej
  2. All-in-one - Auth + feature flags + organizations + analytics
  3. Developer-first - Doskonałe DX i dokumentacja
  4. Fast setup - Działająca auth w minuty
  5. Modern stack - Natywne wsparcie dla App Router
  6. Fair pricing - Przejrzysty, przewidywalny
  7. Privacy-focused - GDPR compliant out of the box

Kinde vs Auth0 vs Clerk

CechaKindeAuth0Clerk
Free tier10,500 MAU7,500 MAU10,000 MAU
Feature flags✅ Wbudowane❌ Brak❌ Brak
Organizations✅ Wbudowane✅ Płatne✅ Płatne
ComplexityNiskaWysokaNiska
Enterprise SSO✅ Od Pro✅ Płatne✅ Płatne
Analytics✅ WbudowaneZewnętrznePodstawowe
Webhooks✅ Tak✅ Actions✅ Tak
PricingOd $25/moOd $35/moOd $25/mo
Setup time5 min30 min10 min

Instalacja i Setup

Next.js App Router

Code
Bash
npm install @kinde-oss/kinde-auth-nextjs

Konfiguracja

.env.local
ENV
# .env.local
KINDE_CLIENT_ID=your_client_id
KINDE_CLIENT_SECRET=your_client_secret
KINDE_ISSUER_URL=https://your_subdomain.kinde.com
KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard

API Route Handler

TSapp/api/auth/[kindeAuth]/route.ts
TypeScript
// app/api/auth/[kindeAuth]/route.ts
import { handleAuth } from '@kinde-oss/kinde-auth-nextjs/server'

export const GET = handleAuth()

To automatycznie tworzy endpointy:

  • /api/auth/login - Rozpoczyna flow logowania
  • /api/auth/register - Rozpoczyna flow rejestracji
  • /api/auth/logout - Wylogowanie
  • /api/auth/kinde_callback - Callback po auth

Authentication

Komponenty Link

TScomponents/AuthButtons.tsx
TypeScript
// components/AuthButtons.tsx
import {
  LoginLink,
  LogoutLink,
  RegisterLink,
} from '@kinde-oss/kinde-auth-nextjs/components'

export function AuthButtons() {
  return (
    <div className="flex gap-4">
      <LoginLink className="btn btn-primary">
        Sign in
      </LoginLink>
      <RegisterLink className="btn btn-secondary">
        Sign up
      </RegisterLink>
      <LogoutLink className="btn btn-ghost">
        Log out
      </LogoutLink>
    </div>
  )
}

Custom Login z parametrami

Code
TypeScript
import { LoginLink, RegisterLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Wymuszony provider
<LoginLink authUrlParams={{ connection_id: 'conn_google' }}>
  Continue with Google
</LoginLink>

// Pre-filled email
<RegisterLink authUrlParams={{ login_hint: 'user@example.com' }}>
  Sign up
</RegisterLink>

// Custom redirect
<LoginLink postLoginRedirectURL="/onboarding">
  Sign in
</LoginLink>

// Organization login
<LoginLink orgCode="org_acme">
  Sign in to Acme Corp
</LoginLink>

Server-Side Auth Check

TSapp/dashboard/page.tsx
TypeScript
// app/dashboard/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const { getUser, isAuthenticated } = getKindeServerSession()

  if (!(await isAuthenticated())) {
    redirect('/api/auth/login?post_login_redirect_url=/dashboard')
  }

  const user = await getUser()

  return (
    <div>
      <h1>Welcome, {user?.given_name || user?.email}</h1>
      <p>Email: {user?.email}</p>
      <p>ID: {user?.id}</p>
      <img
        src={user?.picture || '/default-avatar.png'}
        alt="Avatar"
        className="w-16 h-16 rounded-full"
      />
    </div>
  )
}

Client-Side Auth Hook

Code
TypeScript
'use client'

import { useKindeAuth } from '@kinde-oss/kinde-auth-nextjs'

export function UserProfile() {
  const { user, isLoading, isAuthenticated } = useKindeAuth()

  if (isLoading) {
    return <div>Loading...</div>
  }

  if (!isAuthenticated) {
    return <div>Please sign in</div>
  }

  return (
    <div className="flex items-center gap-4">
      <img
        src={user?.picture || '/default-avatar.png'}
        alt={user?.given_name || 'User'}
        className="w-10 h-10 rounded-full"
      />
      <div>
        <p className="font-medium">{user?.given_name} {user?.family_name}</p>
        <p className="text-sm text-gray-500">{user?.email}</p>
      </div>
    </div>
  )
}

Middleware Protection

TSmiddleware.ts
TypeScript
// middleware.ts
import { withAuth } from '@kinde-oss/kinde-auth-nextjs/middleware'
import { NextRequest } from 'next/server'

export default withAuth(async function middleware(req: NextRequest) {
  // Custom logic po auth check
  console.log('User authenticated, proceeding...')
}, {
  isReturnToCurrentPage: true,
  loginPage: '/api/auth/login',
})

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/api/protected/:path*'],
}

Social Logins

Dostępne providery

Code
TypeScript
// Kinde Dashboard → Authentication → Social Connections

const socialProviders = [
  'google',
  'github',
  'microsoft',
  'apple',
  'facebook',
  'linkedin',
  'twitter',
  'discord',
  'gitlab',
  'bitbucket',
  'slack',
  'spotify',
  'twitch',
]

Konfiguracja konkretnego providera

Code
TypeScript
import { LoginLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Dedykowane przyciski dla każdego providera
export function SocialLogins() {
  return (
    <div className="space-y-3">
      <LoginLink
        authUrlParams={{ connection_id: 'conn_google' }}
        className="btn btn-outline w-full"
      >
        <GoogleIcon className="w-5 h-5 mr-2" />
        Continue with Google
      </LoginLink>

      <LoginLink
        authUrlParams={{ connection_id: 'conn_github' }}
        className="btn btn-outline w-full"
      >
        <GithubIcon className="w-5 h-5 mr-2" />
        Continue with GitHub
      </LoginLink>

      <LoginLink
        authUrlParams={{ connection_id: 'conn_microsoft' }}
        className="btn btn-outline w-full"
      >
        <MicrosoftIcon className="w-5 h-5 mr-2" />
        Continue with Microsoft
      </LoginLink>
    </div>
  )
}

Feature Flags

Kinde ma wbudowane feature flags - bez potrzeby dodatkowych narzędzi jak LaunchDarkly czy Flagsmith.

Konfiguracja flag

Code
TypeScript
// Kinde Dashboard → Feature Flags → Create Flag

const featureFlags = [
  {
    key: 'new_dashboard',
    name: 'New Dashboard UI',
    type: 'boolean',
    defaultValue: false,
  },
  {
    key: 'max_projects',
    name: 'Maximum Projects',
    type: 'integer',
    defaultValue: 5,
  },
  {
    key: 'theme',
    name: 'UI Theme',
    type: 'string',
    defaultValue: 'light',
  },
]

Server-Side Feature Flags

TSapp/dashboard/page.tsx
TypeScript
// app/dashboard/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'

export default async function DashboardPage() {
  const { getFlag, getBooleanFlag, getIntegerFlag, getStringFlag } = getKindeServerSession()

  // Boolean flag
  const showNewDashboard = await getBooleanFlag('new_dashboard', false)

  // Integer flag
  const maxProjects = await getIntegerFlag('max_projects', 5)

  // String flag
  const theme = await getStringFlag('theme', 'light')

  // Generic getFlag (zwraca obiekt z value i type)
  const featureFlag = await getFlag('new_dashboard')
  // { value: true, type: 'boolean' }

  return (
    <div data-theme={theme}>
      {showNewDashboard ? (
        <NewDashboard maxProjects={maxProjects} />
      ) : (
        <LegacyDashboard />
      )}

      <p>You can create up to {maxProjects} projects</p>
    </div>
  )
}

Client-Side Feature Flags

Code
TypeScript
'use client'

import { useKindeAuth } from '@kinde-oss/kinde-auth-nextjs'

export function FeatureGatedComponent() {
  const { getFlag, getBooleanFlag } = useKindeAuth()

  // Boolean flag
  const showBetaFeature = getBooleanFlag('beta_feature', false)

  // Pełny obiekt flag
  const flag = getFlag('new_checkout')

  if (!showBetaFeature) {
    return null
  }

  return (
    <div className="border-2 border-purple-500 p-4 rounded">
      <span className="badge badge-purple">Beta</span>
      <h3>New Feature Preview</h3>
      {/* Beta feature content */}
    </div>
  )
}

Feature Flags z targeting

Code
TypeScript
// Kinde Dashboard → Feature Flags → Targeting Rules

// Możliwe targeting rules:
const targetingRules = [
  // 1. Percentage rollout
  { percentage: 25 }, // 25% użytkowników

  // 2. User-based
  { users: ['user_123', 'user_456'] },

  // 3. Organization-based
  { organizations: ['org_acme', 'org_beta_testers'] },

  // 4. Property-based
  { property: 'plan', value: 'pro' },
  { property: 'country', value: 'PL' },
]

// Użycie w kodzie - Kinde automatycznie zwraca
// odpowiednią wartość na podstawie reguł
const showFeature = await getBooleanFlag('premium_feature', false)

Organizations (Multi-tenancy)

Tworzenie organizacji

Code
TypeScript
// Kinde Dashboard → Organizations → Create

// Lub przez Management API
import { init } from '@kinde/management-api-js'

const client = init()

const org = await client.organizations.createOrganization({
  name: 'Acme Corporation',
  external_id: 'acme-corp',
  handle: 'acme', // URL-friendly identifier
})

Login z organizacją

Code
TypeScript
import { LoginLink, RegisterLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Login do konkretnej organizacji
<LoginLink orgCode="org_acme">
  Sign in to Acme Corp
</LoginLink>

// Rejestracja z przypisaniem do organizacji
<RegisterLink orgCode="org_acme">
  Join Acme Corp
</RegisterLink>

// Login z wyborem organizacji
<LoginLink authUrlParams={{ org_code: 'org_acme' }}>
  Sign in
</LoginLink>

Pobieranie danych organizacji

TSapp/[orgSlug]/dashboard/page.tsx
TypeScript
// app/[orgSlug]/dashboard/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'

export default async function OrgDashboard({
  params
}: {
  params: { orgSlug: string }
}) {
  const { getOrganization, getUser } = getKindeServerSession()

  const org = await getOrganization()
  const user = await getUser()

  return (
    <div>
      <h1>{org?.orgName} Dashboard</h1>
      <p>Organization ID: {org?.orgCode}</p>
      <p>Logged in as: {user?.email}</p>
    </div>
  )
}

Zarządzanie członkami organizacji

Code
TypeScript
import { init } from '@kinde/management-api-js'

const client = init()

// Dodaj użytkownika do organizacji
await client.organizations.addOrganizationUsers({
  orgCode: 'org_acme',
  users: [{ id: 'user_123' }],
})

// Pobierz członków organizacji
const members = await client.organizations.getOrganizationUsers({
  orgCode: 'org_acme',
})

// Usuń użytkownika z organizacji
await client.organizations.removeOrganizationUser({
  orgCode: 'org_acme',
  userId: 'user_123',
})

Multi-organization user

TSapp/org-switcher/page.tsx
TypeScript
// app/org-switcher/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { LoginLink } from '@kinde-oss/kinde-auth-nextjs/components'

export default async function OrgSwitcher() {
  const { getUserOrganizations } = getKindeServerSession()

  const orgs = await getUserOrganizations()

  return (
    <div>
      <h2>Your Organizations</h2>
      <ul className="space-y-2">
        {orgs?.orgCodes?.map((orgCode) => (
          <li key={orgCode}>
            <LoginLink
              orgCode={orgCode}
              className="btn btn-outline w-full"
            >
              Switch to {orgCode}
            </LoginLink>
          </li>
        ))}
      </ul>
    </div>
  )
}

Permissions & Roles

Definiowanie ról i uprawnień

Code
TypeScript
// Kinde Dashboard → Settings → Roles & Permissions

// 1. Zdefiniuj uprawnienia (Permissions)
const permissions = [
  { key: 'read:users', name: 'Read Users' },
  { key: 'create:users', name: 'Create Users' },
  { key: 'update:users', name: 'Update Users' },
  { key: 'delete:users', name: 'Delete Users' },
  { key: 'read:billing', name: 'Read Billing' },
  { key: 'manage:billing', name: 'Manage Billing' },
  { key: 'admin', name: 'Full Admin Access' },
]

// 2. Zdefiniuj role (Roles) i przypisz uprawnienia
const roles = [
  {
    key: 'owner',
    name: 'Owner',
    permissions: ['admin'],
  },
  {
    key: 'admin',
    name: 'Admin',
    permissions: ['read:users', 'create:users', 'update:users', 'delete:users', 'read:billing', 'manage:billing'],
  },
  {
    key: 'member',
    name: 'Member',
    permissions: ['read:users'],
  },
  {
    key: 'billing',
    name: 'Billing Admin',
    permissions: ['read:billing', 'manage:billing'],
  },
]

Sprawdzanie uprawnień (Server)

TSapp/admin/users/page.tsx
TypeScript
// app/admin/users/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { redirect } from 'next/navigation'

export default async function AdminUsersPage() {
  const { getPermission, getPermissions } = getKindeServerSession()

  // Sprawdź pojedyncze uprawnienie
  const canReadUsers = await getPermission('read:users')

  if (!canReadUsers?.isGranted) {
    redirect('/unauthorized')
  }

  // Pobierz wszystkie uprawnienia
  const permissions = await getPermissions()
  // { permissions: ['read:users', 'create:users'], orgCode: 'org_123' }

  const canCreate = permissions?.permissions?.includes('create:users')
  const canDelete = permissions?.permissions?.includes('delete:users')

  return (
    <div>
      <h1>User Management</h1>
      <UserList />

      {canCreate && <CreateUserButton />}
      {canDelete && <DeleteUserButton />}
    </div>
  )
}

Sprawdzanie uprawnień (Client)

Code
TypeScript
'use client'

import { useKindeAuth } from '@kinde-oss/kinde-auth-nextjs'

export function AdminPanel() {
  const { getPermission, permissions } = useKindeAuth()

  const canManageBilling = getPermission('manage:billing')?.isGranted

  return (
    <div>
      <h2>Admin Panel</h2>

      {canManageBilling && (
        <section>
          <h3>Billing Management</h3>
          <BillingDashboard />
        </section>
      )}

      {/* Pokaż wszystkie uprawnienia */}
      <div className="text-sm text-gray-500">
        Your permissions: {permissions?.join(', ')}
      </div>
    </div>
  )
}

Permission-based middleware

TSmiddleware.ts
TypeScript
// middleware.ts
import { withAuth } from '@kinde-oss/kinde-auth-nextjs/middleware'
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { NextRequest, NextResponse } from 'next/server'

// Mapowanie routes do wymaganych uprawnień
const routePermissions: Record<string, string[]> = {
  '/admin/users': ['read:users'],
  '/admin/billing': ['read:billing'],
  '/admin/settings': ['admin'],
}

export default withAuth(async function middleware(req: NextRequest) {
  const { getPermissions } = getKindeServerSession()
  const { permissions } = await getPermissions() || { permissions: [] }

  const pathname = req.nextUrl.pathname
  const requiredPermissions = routePermissions[pathname]

  if (requiredPermissions) {
    const hasPermission = requiredPermissions.some(p =>
      permissions?.includes(p)
    )

    if (!hasPermission) {
      return NextResponse.redirect(new URL('/unauthorized', req.url))
    }
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/admin/:path*'],
}

Enterprise SSO

SAML Configuration

Code
TypeScript
// Kinde Dashboard → Authentication → Enterprise SSO → SAML

// Konfiguracja dla Identity Provider (IdP)
const samlConfig = {
  // Dane od IdP
  idpEntityId: 'https://idp.example.com/saml/metadata',
  idpSsoUrl: 'https://idp.example.com/saml/sso',
  idpCertificate: '-----BEGIN CERTIFICATE-----...',

  // Dane Kinde (Service Provider)
  spEntityId: 'https://yourapp.kinde.com',
  spAcsUrl: 'https://yourapp.kinde.com/saml/acs',
  spMetadataUrl: 'https://yourapp.kinde.com/saml/metadata',
}

OIDC Enterprise Connection

Code
TypeScript
// Dla Azure AD, Google Workspace, Okta

const oidcConfig = {
  // Konfiguracja providera
  issuer: 'https://login.microsoftonline.com/tenant-id/v2.0',
  clientId: 'AZURE_CLIENT_ID',
  clientSecret: 'AZURE_CLIENT_SECRET',
  scopes: ['openid', 'profile', 'email'],
}

Login z Enterprise SSO

Code
TypeScript
import { LoginLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Login z konkretnym enterprise connection
<LoginLink
  authUrlParams={{
    connection_id: 'conn_enterprise_acme',
  }}
>
  Sign in with Acme SSO
</LoginLink>

// Automatyczny routing po domenie email
<LoginLink
  authUrlParams={{
    login_hint: 'user@acmecorp.com', // Kinde wykryje domenę i przekieruje
  }}
>
  Sign in with work email
</LoginLink>

Webhooks

Konfiguracja webhooków

Code
TypeScript
// Kinde Dashboard → Settings → Webhooks

// Dostępne eventy:
const webhookEvents = [
  'user.created',
  'user.updated',
  'user.deleted',
  'user.authenticated',
  'organization.created',
  'organization.updated',
  'organization.deleted',
  'organization.user_added',
  'organization.user_removed',
  'role.created',
  'role.updated',
  'role.deleted',
  'permission.created',
  'permission.updated',
  'permission.deleted',
]

Webhook handler

TSapp/api/webhooks/kinde/route.ts
TypeScript
// app/api/webhooks/kinde/route.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'

const KINDE_WEBHOOK_SECRET = process.env.KINDE_WEBHOOK_SECRET!

function verifySignature(payload: string, signature: string): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', KINDE_WEBHOOK_SECRET)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

export async function POST(request: Request) {
  const payload = await request.text()
  const signature = request.headers.get('x-kinde-signature')

  if (!signature || !verifySignature(payload, signature)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const event = JSON.parse(payload)

  switch (event.type) {
    case 'user.created':
      await handleUserCreated(event.data)
      break

    case 'user.updated':
      await handleUserUpdated(event.data)
      break

    case 'organization.user_added':
      await handleOrgUserAdded(event.data)
      break

    default:
      console.log('Unhandled event type:', event.type)
  }

  return NextResponse.json({ received: true })
}

async function handleUserCreated(data: any) {
  // Sync user to your database
  await db.user.create({
    data: {
      kindeId: data.user.id,
      email: data.user.email,
      name: `${data.user.first_name} ${data.user.last_name}`,
    },
  })

  // Send welcome email
  await sendWelcomeEmail(data.user.email)
}

async function handleUserUpdated(data: any) {
  await db.user.update({
    where: { kindeId: data.user.id },
    data: {
      email: data.user.email,
      name: `${data.user.first_name} ${data.user.last_name}`,
    },
  })
}

async function handleOrgUserAdded(data: any) {
  // Add user to organization in your system
  await db.organizationMember.create({
    data: {
      userId: data.user.id,
      organizationId: data.organization.code,
      role: 'member',
    },
  })
}

Management API

Konfiguracja

TSlib/kinde-management.ts
TypeScript
// lib/kinde-management.ts
import { init } from '@kinde/management-api-js'

const kindeManagement = init({
  kindeDomain: process.env.KINDE_ISSUER_URL!,
  clientId: process.env.KINDE_M2M_CLIENT_ID!,
  clientSecret: process.env.KINDE_M2M_CLIENT_SECRET!,
})

export default kindeManagement

Zarządzanie użytkownikami

Code
TypeScript
import kindeManagement from '@/lib/kinde-management'

// Pobierz użytkowników
export async function getUsers(page = 0, pageSize = 10) {
  return kindeManagement.users.getUsers({
    page_number: page,
    page_size: pageSize,
  })
}

// Pobierz użytkownika po ID
export async function getUser(userId: string) {
  return kindeManagement.users.getUserData({ id: userId })
}

// Utwórz użytkownika
export async function createUser(data: {
  email: string
  firstName?: string
  lastName?: string
}) {
  return kindeManagement.users.createUser({
    profile: {
      given_name: data.firstName,
      family_name: data.lastName,
    },
    identities: [
      {
        type: 'email',
        details: { email: data.email },
      },
    ],
  })
}

// Zaktualizuj użytkownika
export async function updateUser(userId: string, data: {
  firstName?: string
  lastName?: string
}) {
  return kindeManagement.users.updateUser({
    id: userId,
    given_name: data.firstName,
    family_name: data.lastName,
  })
}

// Usuń użytkownika
export async function deleteUser(userId: string) {
  return kindeManagement.users.deleteUser({ id: userId })
}

// Przypisz rolę
export async function assignRole(userId: string, roleId: string) {
  return kindeManagement.roles.addRoleUsers({
    roleId,
    users: [userId],
  })
}

Zarządzanie organizacjami

Code
TypeScript
import kindeManagement from '@/lib/kinde-management'

// Utwórz organizację
export async function createOrganization(data: {
  name: string
  externalId?: string
}) {
  return kindeManagement.organizations.createOrganization({
    name: data.name,
    external_id: data.externalId,
  })
}

// Dodaj użytkownika do organizacji
export async function addUserToOrganization(
  orgCode: string,
  userId: string,
  roles?: string[]
) {
  return kindeManagement.organizations.addOrganizationUsers({
    orgCode,
    users: [{ id: userId, roles }],
  })
}

// Pobierz członków organizacji
export async function getOrganizationMembers(orgCode: string) {
  return kindeManagement.organizations.getOrganizationUsers({
    orgCode,
  })
}

Integracja z Bazami Danych

Prisma + Kinde

prisma/schema.prisma
Prisma
// prisma/schema.prisma
model User {
  id        String   @id @default(cuid())
  kindeId   String   @unique
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  organizations OrganizationMember[]
  posts         Post[]
}

model Organization {
  id        String   @id @default(cuid())
  kindeOrgCode String @unique
  name      String
  createdAt DateTime @default(now())

  members OrganizationMember[]
}

model OrganizationMember {
  id             String       @id @default(cuid())
  userId         String
  organizationId String
  role           String       @default("member")
  createdAt      DateTime     @default(now())

  user         User         @relation(fields: [userId], references: [id])
  organization Organization @relation(fields: [organizationId], references: [id])

  @@unique([userId, organizationId])
}

Sync User Hook

TSlib/sync-user.ts
TypeScript
// lib/sync-user.ts
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { db } from '@/lib/db'

export async function syncUser() {
  const { getUser, getOrganization } = getKindeServerSession()

  const kindeUser = await getUser()
  const kindeOrg = await getOrganization()

  if (!kindeUser?.id) return null

  // Upsert user
  const user = await db.user.upsert({
    where: { kindeId: kindeUser.id },
    update: {
      email: kindeUser.email!,
      name: `${kindeUser.given_name || ''} ${kindeUser.family_name || ''}`.trim(),
    },
    create: {
      kindeId: kindeUser.id,
      email: kindeUser.email!,
      name: `${kindeUser.given_name || ''} ${kindeUser.family_name || ''}`.trim(),
    },
  })

  // Sync organization membership
  if (kindeOrg?.orgCode) {
    const org = await db.organization.upsert({
      where: { kindeOrgCode: kindeOrg.orgCode },
      update: { name: kindeOrg.orgName || kindeOrg.orgCode },
      create: {
        kindeOrgCode: kindeOrg.orgCode,
        name: kindeOrg.orgName || kindeOrg.orgCode,
      },
    })

    await db.organizationMember.upsert({
      where: {
        userId_organizationId: {
          userId: user.id,
          organizationId: org.id,
        },
      },
      update: {},
      create: {
        userId: user.id,
        organizationId: org.id,
        role: 'member',
      },
    })
  }

  return user
}

Cennik

PlanCenaMAUFeatures
Free$0/mo10,500Auth, Feature Flags, 3 Orgs
Pro$25/moUnlimitedEnterprise SSO, Unlimited Orgs
EnterpriseCustomCustomCustom SLA, Dedicated Support

Co zawiera Free tier?

  • 10,500 Monthly Active Users
  • Social connections (Google, GitHub, etc.)
  • Email/Password authentication
  • Feature flags
  • 3 Organizations
  • Basic analytics
  • Community support

Pro tier dodaje:

  • Unlimited MAU
  • Enterprise SSO (SAML, OIDC)
  • Unlimited Organizations
  • Advanced analytics
  • Priority support
  • Custom domains

FAQ - Najczęściej Zadawane Pytania

Czym Kinde różni się od Clerk?

Kinde ma większy free tier (10,500 vs 10,000 MAU), wbudowane feature flags i organizations w każdym planie. Clerk ma lepsze komponenty UI out-of-the-box i jest bardziej opinionated.

Czy mogę migrować z Auth0?

Tak, Kinde ma dokumentację do migracji z Auth0. Możesz eksportować użytkowników i importować ich do Kinde. Hasła wymagają reset flow dla użytkowników.

Jak działają Feature Flags z targeting?

Feature flags mogą być targetowane na podstawie: user ID, organization, custom properties, percentage rollout. Reguły są ewaluowane server-side i client-side.

Czy Kinde wspiera multi-region?

Tak, Kinde ma data centers w US, EU i Australia. Możesz wybrać region przy tworzeniu konta dla compliance GDPR.

Jak wygląda pricing po przekroczeniu limitu?

Na planie Free musisz przejść na Pro po przekroczeniu 10,500 MAU. Na Pro płacisz flat rate bez dodatkowych opłat za MAU.


Kinde - a complete guide to a modern auth platform

What is Kinde?

Kinde is a modern authentication and authorization platform that stands out with a very generous free tier (10,500 MAU) and a developer-friendly approach. Unlike traditional auth solutions, Kinde offers not just authentication in a single package, but also feature flags, organizations for multi-tenancy, and built-in analytics.

Founded in 2021 in Australia, Kinde quickly gained popularity among startups and mid-sized companies looking for an alternative to Auth0 and Clerk. The platform is known for its ease of integration, excellent SDKs for Next.js and React, and transparent pricing.

Key features of Kinde:

  • 10,500 MAU for free - The most generous free tier on the market
  • Feature Flags - Built-in without additional tools
  • Organizations - Native multi-tenancy support
  • Permissions & Roles - Granular RBAC
  • Multiple auth methods - Social, Email, Phone, Enterprise SSO
  • SDKs for everything - Next.js, React, React Native, Node.js, Python

Why Kinde?

Key advantages

  1. Generous free tier - 10,500 MAU without a credit card
  2. All-in-one - Auth + feature flags + organizations + analytics
  3. Developer-first - Excellent DX and documentation
  4. Fast setup - Working auth in minutes
  5. Modern stack - Native support for App Router
  6. Fair pricing - Transparent, predictable
  7. Privacy-focused - GDPR compliant out of the box

Kinde vs Auth0 vs Clerk

FeatureKindeAuth0Clerk
Free tier10,500 MAU7,500 MAU10,000 MAU
Feature flags✅ Built-in❌ None❌ None
Organizations✅ Built-in✅ Paid✅ Paid
ComplexityLowHighLow
Enterprise SSO✅ From Pro✅ Paid✅ Paid
Analytics✅ Built-inExternalBasic
Webhooks✅ Yes✅ Actions✅ Yes
PricingFrom $25/moFrom $35/moFrom $25/mo
Setup time5 min30 min10 min

Installation and setup

Next.js App Router

Code
Bash
npm install @kinde-oss/kinde-auth-nextjs

Configuration

.env.local
ENV
# .env.local
KINDE_CLIENT_ID=your_client_id
KINDE_CLIENT_SECRET=your_client_secret
KINDE_ISSUER_URL=https://your_subdomain.kinde.com
KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard

API route handler

TSapp/api/auth/[kindeAuth]/route.ts
TypeScript
// app/api/auth/[kindeAuth]/route.ts
import { handleAuth } from '@kinde-oss/kinde-auth-nextjs/server'

export const GET = handleAuth()

This automatically creates the following endpoints:

  • /api/auth/login - Starts the login flow
  • /api/auth/register - Starts the registration flow
  • /api/auth/logout - Logs the user out
  • /api/auth/kinde_callback - Callback after auth

Authentication

Link components

TScomponents/AuthButtons.tsx
TypeScript
// components/AuthButtons.tsx
import {
  LoginLink,
  LogoutLink,
  RegisterLink,
} from '@kinde-oss/kinde-auth-nextjs/components'

export function AuthButtons() {
  return (
    <div className="flex gap-4">
      <LoginLink className="btn btn-primary">
        Sign in
      </LoginLink>
      <RegisterLink className="btn btn-secondary">
        Sign up
      </RegisterLink>
      <LogoutLink className="btn btn-ghost">
        Log out
      </LogoutLink>
    </div>
  )
}

Custom login with parameters

Code
TypeScript
import { LoginLink, RegisterLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Forced provider
<LoginLink authUrlParams={{ connection_id: 'conn_google' }}>
  Continue with Google
</LoginLink>

// Pre-filled email
<RegisterLink authUrlParams={{ login_hint: 'user@example.com' }}>
  Sign up
</RegisterLink>

// Custom redirect
<LoginLink postLoginRedirectURL="/onboarding">
  Sign in
</LoginLink>

// Organization login
<LoginLink orgCode="org_acme">
  Sign in to Acme Corp
</LoginLink>

Server-side auth check

TSapp/dashboard/page.tsx
TypeScript
// app/dashboard/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const { getUser, isAuthenticated } = getKindeServerSession()

  if (!(await isAuthenticated())) {
    redirect('/api/auth/login?post_login_redirect_url=/dashboard')
  }

  const user = await getUser()

  return (
    <div>
      <h1>Welcome, {user?.given_name || user?.email}</h1>
      <p>Email: {user?.email}</p>
      <p>ID: {user?.id}</p>
      <img
        src={user?.picture || '/default-avatar.png'}
        alt="Avatar"
        className="w-16 h-16 rounded-full"
      />
    </div>
  )
}

Client-side auth hook

Code
TypeScript
'use client'

import { useKindeAuth } from '@kinde-oss/kinde-auth-nextjs'

export function UserProfile() {
  const { user, isLoading, isAuthenticated } = useKindeAuth()

  if (isLoading) {
    return <div>Loading...</div>
  }

  if (!isAuthenticated) {
    return <div>Please sign in</div>
  }

  return (
    <div className="flex items-center gap-4">
      <img
        src={user?.picture || '/default-avatar.png'}
        alt={user?.given_name || 'User'}
        className="w-10 h-10 rounded-full"
      />
      <div>
        <p className="font-medium">{user?.given_name} {user?.family_name}</p>
        <p className="text-sm text-gray-500">{user?.email}</p>
      </div>
    </div>
  )
}

Middleware protection

TSmiddleware.ts
TypeScript
// middleware.ts
import { withAuth } from '@kinde-oss/kinde-auth-nextjs/middleware'
import { NextRequest } from 'next/server'

export default withAuth(async function middleware(req: NextRequest) {
  // Custom logic after auth check
  console.log('User authenticated, proceeding...')
}, {
  isReturnToCurrentPage: true,
  loginPage: '/api/auth/login',
})

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/api/protected/:path*'],
}

Social logins

Available providers

Code
TypeScript
// Kinde Dashboard → Authentication → Social Connections

const socialProviders = [
  'google',
  'github',
  'microsoft',
  'apple',
  'facebook',
  'linkedin',
  'twitter',
  'discord',
  'gitlab',
  'bitbucket',
  'slack',
  'spotify',
  'twitch',
]

Configuring a specific provider

Code
TypeScript
import { LoginLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Dedicated buttons for each provider
export function SocialLogins() {
  return (
    <div className="space-y-3">
      <LoginLink
        authUrlParams={{ connection_id: 'conn_google' }}
        className="btn btn-outline w-full"
      >
        <GoogleIcon className="w-5 h-5 mr-2" />
        Continue with Google
      </LoginLink>

      <LoginLink
        authUrlParams={{ connection_id: 'conn_github' }}
        className="btn btn-outline w-full"
      >
        <GithubIcon className="w-5 h-5 mr-2" />
        Continue with GitHub
      </LoginLink>

      <LoginLink
        authUrlParams={{ connection_id: 'conn_microsoft' }}
        className="btn btn-outline w-full"
      >
        <MicrosoftIcon className="w-5 h-5 mr-2" />
        Continue with Microsoft
      </LoginLink>
    </div>
  )
}

Feature flags

Kinde has built-in feature flags - no need for additional tools like LaunchDarkly or Flagsmith.

Flag configuration

Code
TypeScript
// Kinde Dashboard → Feature Flags → Create Flag

const featureFlags = [
  {
    key: 'new_dashboard',
    name: 'New Dashboard UI',
    type: 'boolean',
    defaultValue: false,
  },
  {
    key: 'max_projects',
    name: 'Maximum Projects',
    type: 'integer',
    defaultValue: 5,
  },
  {
    key: 'theme',
    name: 'UI Theme',
    type: 'string',
    defaultValue: 'light',
  },
]

Server-side feature flags

TSapp/dashboard/page.tsx
TypeScript
// app/dashboard/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'

export default async function DashboardPage() {
  const { getFlag, getBooleanFlag, getIntegerFlag, getStringFlag } = getKindeServerSession()

  // Boolean flag
  const showNewDashboard = await getBooleanFlag('new_dashboard', false)

  // Integer flag
  const maxProjects = await getIntegerFlag('max_projects', 5)

  // String flag
  const theme = await getStringFlag('theme', 'light')

  // Generic getFlag (returns an object with value and type)
  const featureFlag = await getFlag('new_dashboard')
  // { value: true, type: 'boolean' }

  return (
    <div data-theme={theme}>
      {showNewDashboard ? (
        <NewDashboard maxProjects={maxProjects} />
      ) : (
        <LegacyDashboard />
      )}

      <p>You can create up to {maxProjects} projects</p>
    </div>
  )
}

Client-side feature flags

Code
TypeScript
'use client'

import { useKindeAuth } from '@kinde-oss/kinde-auth-nextjs'

export function FeatureGatedComponent() {
  const { getFlag, getBooleanFlag } = useKindeAuth()

  // Boolean flag
  const showBetaFeature = getBooleanFlag('beta_feature', false)

  // Full flag object
  const flag = getFlag('new_checkout')

  if (!showBetaFeature) {
    return null
  }

  return (
    <div className="border-2 border-purple-500 p-4 rounded">
      <span className="badge badge-purple">Beta</span>
      <h3>New Feature Preview</h3>
      {/* Beta feature content */}
    </div>
  )
}

Feature flags with targeting

Code
TypeScript
// Kinde Dashboard → Feature Flags → Targeting Rules

// Available targeting rules:
const targetingRules = [
  // 1. Percentage rollout
  { percentage: 25 }, // 25% of users

  // 2. User-based
  { users: ['user_123', 'user_456'] },

  // 3. Organization-based
  { organizations: ['org_acme', 'org_beta_testers'] },

  // 4. Property-based
  { property: 'plan', value: 'pro' },
  { property: 'country', value: 'PL' },
]

// Usage in code - Kinde automatically returns
// the appropriate value based on the rules
const showFeature = await getBooleanFlag('premium_feature', false)

Organizations (multi-tenancy)

Creating organizations

Code
TypeScript
// Kinde Dashboard → Organizations → Create

// Or via the Management API
import { init } from '@kinde/management-api-js'

const client = init()

const org = await client.organizations.createOrganization({
  name: 'Acme Corporation',
  external_id: 'acme-corp',
  handle: 'acme', // URL-friendly identifier
})

Login with an organization

Code
TypeScript
import { LoginLink, RegisterLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Login to a specific organization
<LoginLink orgCode="org_acme">
  Sign in to Acme Corp
</LoginLink>

// Registration with organization assignment
<RegisterLink orgCode="org_acme">
  Join Acme Corp
</RegisterLink>

// Login with organization selection
<LoginLink authUrlParams={{ org_code: 'org_acme' }}>
  Sign in
</LoginLink>

Fetching organization data

TSapp/[orgSlug]/dashboard/page.tsx
TypeScript
// app/[orgSlug]/dashboard/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'

export default async function OrgDashboard({
  params
}: {
  params: { orgSlug: string }
}) {
  const { getOrganization, getUser } = getKindeServerSession()

  const org = await getOrganization()
  const user = await getUser()

  return (
    <div>
      <h1>{org?.orgName} Dashboard</h1>
      <p>Organization ID: {org?.orgCode}</p>
      <p>Logged in as: {user?.email}</p>
    </div>
  )
}

Managing organization members

Code
TypeScript
import { init } from '@kinde/management-api-js'

const client = init()

// Add a user to an organization
await client.organizations.addOrganizationUsers({
  orgCode: 'org_acme',
  users: [{ id: 'user_123' }],
})

// Get organization members
const members = await client.organizations.getOrganizationUsers({
  orgCode: 'org_acme',
})

// Remove a user from an organization
await client.organizations.removeOrganizationUser({
  orgCode: 'org_acme',
  userId: 'user_123',
})

Multi-organization user

TSapp/org-switcher/page.tsx
TypeScript
// app/org-switcher/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { LoginLink } from '@kinde-oss/kinde-auth-nextjs/components'

export default async function OrgSwitcher() {
  const { getUserOrganizations } = getKindeServerSession()

  const orgs = await getUserOrganizations()

  return (
    <div>
      <h2>Your Organizations</h2>
      <ul className="space-y-2">
        {orgs?.orgCodes?.map((orgCode) => (
          <li key={orgCode}>
            <LoginLink
              orgCode={orgCode}
              className="btn btn-outline w-full"
            >
              Switch to {orgCode}
            </LoginLink>
          </li>
        ))}
      </ul>
    </div>
  )
}

Permissions & roles

Defining roles and permissions

Code
TypeScript
// Kinde Dashboard → Settings → Roles & Permissions

// 1. Define permissions
const permissions = [
  { key: 'read:users', name: 'Read Users' },
  { key: 'create:users', name: 'Create Users' },
  { key: 'update:users', name: 'Update Users' },
  { key: 'delete:users', name: 'Delete Users' },
  { key: 'read:billing', name: 'Read Billing' },
  { key: 'manage:billing', name: 'Manage Billing' },
  { key: 'admin', name: 'Full Admin Access' },
]

// 2. Define roles and assign permissions
const roles = [
  {
    key: 'owner',
    name: 'Owner',
    permissions: ['admin'],
  },
  {
    key: 'admin',
    name: 'Admin',
    permissions: ['read:users', 'create:users', 'update:users', 'delete:users', 'read:billing', 'manage:billing'],
  },
  {
    key: 'member',
    name: 'Member',
    permissions: ['read:users'],
  },
  {
    key: 'billing',
    name: 'Billing Admin',
    permissions: ['read:billing', 'manage:billing'],
  },
]

Checking permissions (server)

TSapp/admin/users/page.tsx
TypeScript
// app/admin/users/page.tsx
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { redirect } from 'next/navigation'

export default async function AdminUsersPage() {
  const { getPermission, getPermissions } = getKindeServerSession()

  // Check a single permission
  const canReadUsers = await getPermission('read:users')

  if (!canReadUsers?.isGranted) {
    redirect('/unauthorized')
  }

  // Get all permissions
  const permissions = await getPermissions()
  // { permissions: ['read:users', 'create:users'], orgCode: 'org_123' }

  const canCreate = permissions?.permissions?.includes('create:users')
  const canDelete = permissions?.permissions?.includes('delete:users')

  return (
    <div>
      <h1>User Management</h1>
      <UserList />

      {canCreate && <CreateUserButton />}
      {canDelete && <DeleteUserButton />}
    </div>
  )
}

Checking permissions (client)

Code
TypeScript
'use client'

import { useKindeAuth } from '@kinde-oss/kinde-auth-nextjs'

export function AdminPanel() {
  const { getPermission, permissions } = useKindeAuth()

  const canManageBilling = getPermission('manage:billing')?.isGranted

  return (
    <div>
      <h2>Admin Panel</h2>

      {canManageBilling && (
        <section>
          <h3>Billing Management</h3>
          <BillingDashboard />
        </section>
      )}

      {/* Show all permissions */}
      <div className="text-sm text-gray-500">
        Your permissions: {permissions?.join(', ')}
      </div>
    </div>
  )
}

Permission-based middleware

TSmiddleware.ts
TypeScript
// middleware.ts
import { withAuth } from '@kinde-oss/kinde-auth-nextjs/middleware'
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { NextRequest, NextResponse } from 'next/server'

// Map routes to required permissions
const routePermissions: Record<string, string[]> = {
  '/admin/users': ['read:users'],
  '/admin/billing': ['read:billing'],
  '/admin/settings': ['admin'],
}

export default withAuth(async function middleware(req: NextRequest) {
  const { getPermissions } = getKindeServerSession()
  const { permissions } = await getPermissions() || { permissions: [] }

  const pathname = req.nextUrl.pathname
  const requiredPermissions = routePermissions[pathname]

  if (requiredPermissions) {
    const hasPermission = requiredPermissions.some(p =>
      permissions?.includes(p)
    )

    if (!hasPermission) {
      return NextResponse.redirect(new URL('/unauthorized', req.url))
    }
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/admin/:path*'],
}

Enterprise SSO

SAML configuration

Code
TypeScript
// Kinde Dashboard → Authentication → Enterprise SSO → SAML

// Configuration for the Identity Provider (IdP)
const samlConfig = {
  // Data from IdP
  idpEntityId: 'https://idp.example.com/saml/metadata',
  idpSsoUrl: 'https://idp.example.com/saml/sso',
  idpCertificate: '-----BEGIN CERTIFICATE-----...',

  // Kinde data (Service Provider)
  spEntityId: 'https://yourapp.kinde.com',
  spAcsUrl: 'https://yourapp.kinde.com/saml/acs',
  spMetadataUrl: 'https://yourapp.kinde.com/saml/metadata',
}

OIDC enterprise connection

Code
TypeScript
// For Azure AD, Google Workspace, Okta

const oidcConfig = {
  // Provider configuration
  issuer: 'https://login.microsoftonline.com/tenant-id/v2.0',
  clientId: 'AZURE_CLIENT_ID',
  clientSecret: 'AZURE_CLIENT_SECRET',
  scopes: ['openid', 'profile', 'email'],
}

Login with Enterprise SSO

Code
TypeScript
import { LoginLink } from '@kinde-oss/kinde-auth-nextjs/components'

// Login with a specific enterprise connection
<LoginLink
  authUrlParams={{
    connection_id: 'conn_enterprise_acme',
  }}
>
  Sign in with Acme SSO
</LoginLink>

// Automatic routing based on email domain
<LoginLink
  authUrlParams={{
    login_hint: 'user@acmecorp.com', // Kinde detects the domain and redirects
  }}
>
  Sign in with work email
</LoginLink>

Webhooks

Webhook configuration

Code
TypeScript
// Kinde Dashboard → Settings → Webhooks

// Available events:
const webhookEvents = [
  'user.created',
  'user.updated',
  'user.deleted',
  'user.authenticated',
  'organization.created',
  'organization.updated',
  'organization.deleted',
  'organization.user_added',
  'organization.user_removed',
  'role.created',
  'role.updated',
  'role.deleted',
  'permission.created',
  'permission.updated',
  'permission.deleted',
]

Webhook handler

TSapp/api/webhooks/kinde/route.ts
TypeScript
// app/api/webhooks/kinde/route.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'

const KINDE_WEBHOOK_SECRET = process.env.KINDE_WEBHOOK_SECRET!

function verifySignature(payload: string, signature: string): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', KINDE_WEBHOOK_SECRET)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

export async function POST(request: Request) {
  const payload = await request.text()
  const signature = request.headers.get('x-kinde-signature')

  if (!signature || !verifySignature(payload, signature)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const event = JSON.parse(payload)

  switch (event.type) {
    case 'user.created':
      await handleUserCreated(event.data)
      break

    case 'user.updated':
      await handleUserUpdated(event.data)
      break

    case 'organization.user_added':
      await handleOrgUserAdded(event.data)
      break

    default:
      console.log('Unhandled event type:', event.type)
  }

  return NextResponse.json({ received: true })
}

async function handleUserCreated(data: any) {
  // Sync user to your database
  await db.user.create({
    data: {
      kindeId: data.user.id,
      email: data.user.email,
      name: `${data.user.first_name} ${data.user.last_name}`,
    },
  })

  // Send welcome email
  await sendWelcomeEmail(data.user.email)
}

async function handleUserUpdated(data: any) {
  await db.user.update({
    where: { kindeId: data.user.id },
    data: {
      email: data.user.email,
      name: `${data.user.first_name} ${data.user.last_name}`,
    },
  })
}

async function handleOrgUserAdded(data: any) {
  // Add user to organization in your system
  await db.organizationMember.create({
    data: {
      userId: data.user.id,
      organizationId: data.organization.code,
      role: 'member',
    },
  })
}

Management API

Configuration

TSlib/kinde-management.ts
TypeScript
// lib/kinde-management.ts
import { init } from '@kinde/management-api-js'

const kindeManagement = init({
  kindeDomain: process.env.KINDE_ISSUER_URL!,
  clientId: process.env.KINDE_M2M_CLIENT_ID!,
  clientSecret: process.env.KINDE_M2M_CLIENT_SECRET!,
})

export default kindeManagement

User management

Code
TypeScript
import kindeManagement from '@/lib/kinde-management'

// Get users
export async function getUsers(page = 0, pageSize = 10) {
  return kindeManagement.users.getUsers({
    page_number: page,
    page_size: pageSize,
  })
}

// Get a user by ID
export async function getUser(userId: string) {
  return kindeManagement.users.getUserData({ id: userId })
}

// Create a user
export async function createUser(data: {
  email: string
  firstName?: string
  lastName?: string
}) {
  return kindeManagement.users.createUser({
    profile: {
      given_name: data.firstName,
      family_name: data.lastName,
    },
    identities: [
      {
        type: 'email',
        details: { email: data.email },
      },
    ],
  })
}

// Update a user
export async function updateUser(userId: string, data: {
  firstName?: string
  lastName?: string
}) {
  return kindeManagement.users.updateUser({
    id: userId,
    given_name: data.firstName,
    family_name: data.lastName,
  })
}

// Delete a user
export async function deleteUser(userId: string) {
  return kindeManagement.users.deleteUser({ id: userId })
}

// Assign a role
export async function assignRole(userId: string, roleId: string) {
  return kindeManagement.roles.addRoleUsers({
    roleId,
    users: [userId],
  })
}

Organization management

Code
TypeScript
import kindeManagement from '@/lib/kinde-management'

// Create an organization
export async function createOrganization(data: {
  name: string
  externalId?: string
}) {
  return kindeManagement.organizations.createOrganization({
    name: data.name,
    external_id: data.externalId,
  })
}

// Add a user to an organization
export async function addUserToOrganization(
  orgCode: string,
  userId: string,
  roles?: string[]
) {
  return kindeManagement.organizations.addOrganizationUsers({
    orgCode,
    users: [{ id: userId, roles }],
  })
}

// Get organization members
export async function getOrganizationMembers(orgCode: string) {
  return kindeManagement.organizations.getOrganizationUsers({
    orgCode,
  })
}

Database integration

Prisma + Kinde

prisma/schema.prisma
Prisma
// prisma/schema.prisma
model User {
  id        String   @id @default(cuid())
  kindeId   String   @unique
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  organizations OrganizationMember[]
  posts         Post[]
}

model Organization {
  id        String   @id @default(cuid())
  kindeOrgCode String @unique
  name      String
  createdAt DateTime @default(now())

  members OrganizationMember[]
}

model OrganizationMember {
  id             String       @id @default(cuid())
  userId         String
  organizationId String
  role           String       @default("member")
  createdAt      DateTime     @default(now())

  user         User         @relation(fields: [userId], references: [id])
  organization Organization @relation(fields: [organizationId], references: [id])

  @@unique([userId, organizationId])
}

Sync user hook

TSlib/sync-user.ts
TypeScript
// lib/sync-user.ts
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import { db } from '@/lib/db'

export async function syncUser() {
  const { getUser, getOrganization } = getKindeServerSession()

  const kindeUser = await getUser()
  const kindeOrg = await getOrganization()

  if (!kindeUser?.id) return null

  // Upsert user
  const user = await db.user.upsert({
    where: { kindeId: kindeUser.id },
    update: {
      email: kindeUser.email!,
      name: `${kindeUser.given_name || ''} ${kindeUser.family_name || ''}`.trim(),
    },
    create: {
      kindeId: kindeUser.id,
      email: kindeUser.email!,
      name: `${kindeUser.given_name || ''} ${kindeUser.family_name || ''}`.trim(),
    },
  })

  // Sync organization membership
  if (kindeOrg?.orgCode) {
    const org = await db.organization.upsert({
      where: { kindeOrgCode: kindeOrg.orgCode },
      update: { name: kindeOrg.orgName || kindeOrg.orgCode },
      create: {
        kindeOrgCode: kindeOrg.orgCode,
        name: kindeOrg.orgName || kindeOrg.orgCode,
      },
    })

    await db.organizationMember.upsert({
      where: {
        userId_organizationId: {
          userId: user.id,
          organizationId: org.id,
        },
      },
      update: {},
      create: {
        userId: user.id,
        organizationId: org.id,
        role: 'member',
      },
    })
  }

  return user
}

Pricing

PlanPriceMAUFeatures
Free$0/mo10,500Auth, Feature Flags, 3 Orgs
Pro$25/moUnlimitedEnterprise SSO, Unlimited Orgs
EnterpriseCustomCustomCustom SLA, Dedicated Support

What does the free tier include?

  • 10,500 Monthly Active Users
  • Social connections (Google, GitHub, etc.)
  • Email/Password authentication
  • Feature flags
  • 3 Organizations
  • Basic analytics
  • Community support

The Pro tier adds:

  • Unlimited MAU
  • Enterprise SSO (SAML, OIDC)
  • Unlimited Organizations
  • Advanced analytics
  • Priority support
  • Custom domains

FAQ - frequently asked questions

How does Kinde differ from Clerk?

Kinde has a larger free tier (10,500 vs 10,000 MAU), built-in feature flags, and organizations included in every plan. Clerk has better out-of-the-box UI components and is more opinionated.

Can I migrate from Auth0?

Yes, Kinde has documentation for migrating from Auth0. You can export users and import them into Kinde. Passwords require a reset flow for users.

How do feature flags with targeting work?

Feature flags can be targeted based on: user ID, organization, custom properties, and percentage rollout. Rules are evaluated both server-side and client-side.

Does Kinde support multi-region?

Yes, Kinde has data centers in the US, EU, and Australia. You can choose a region when creating your account for GDPR compliance.

What happens with pricing after exceeding the limit?

On the Free plan, you need to upgrade to Pro after exceeding 10,500 MAU. On Pro, you pay a flat rate with no additional charges per MAU.