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
- Generous free tier - 10,500 MAU bez karty kredytowej
- All-in-one - Auth + feature flags + organizations + analytics
- Developer-first - Doskonałe DX i dokumentacja
- Fast setup - Działająca auth w minuty
- Modern stack - Natywne wsparcie dla App Router
- Fair pricing - Przejrzysty, przewidywalny
- Privacy-focused - GDPR compliant out of the box
Kinde vs Auth0 vs Clerk
| Cecha | Kinde | Auth0 | Clerk |
|---|---|---|---|
| Free tier | 10,500 MAU | 7,500 MAU | 10,000 MAU |
| Feature flags | ✅ Wbudowane | ❌ Brak | ❌ Brak |
| Organizations | ✅ Wbudowane | ✅ Płatne | ✅ Płatne |
| Complexity | Niska | Wysoka | Niska |
| Enterprise SSO | ✅ Od Pro | ✅ Płatne | ✅ Płatne |
| Analytics | ✅ Wbudowane | Zewnętrzne | Podstawowe |
| Webhooks | ✅ Tak | ✅ Actions | ✅ Tak |
| Pricing | Od $25/mo | Od $35/mo | Od $25/mo |
| Setup time | 5 min | 30 min | 10 min |
Instalacja i Setup
Next.js App Router
npm install @kinde-oss/kinde-auth-nextjsKonfiguracja
# .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/dashboardAPI Route Handler
// 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
// 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
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
// 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
'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
// 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
// Kinde Dashboard → Authentication → Social Connections
const socialProviders = [
'google',
'github',
'microsoft',
'apple',
'facebook',
'linkedin',
'twitter',
'discord',
'gitlab',
'bitbucket',
'slack',
'spotify',
'twitch',
]Konfiguracja konkretnego providera
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
// 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
// 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
'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
// 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
// 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ą
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
// 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
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
// 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ń
// 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)
// 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)
'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
// 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
// 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
// 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
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
// 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
// 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
// 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 kindeManagementZarządzanie użytkownikami
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
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
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
// 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
| Plan | Cena | MAU | Features |
|---|---|---|---|
| Free | $0/mo | 10,500 | Auth, Feature Flags, 3 Orgs |
| Pro | $25/mo | Unlimited | Enterprise SSO, Unlimited Orgs |
| Enterprise | Custom | Custom | Custom 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
- Generous free tier - 10,500 MAU without a credit card
- All-in-one - Auth + feature flags + organizations + analytics
- Developer-first - Excellent DX and documentation
- Fast setup - Working auth in minutes
- Modern stack - Native support for App Router
- Fair pricing - Transparent, predictable
- Privacy-focused - GDPR compliant out of the box
Kinde vs Auth0 vs Clerk
| Feature | Kinde | Auth0 | Clerk |
|---|---|---|---|
| Free tier | 10,500 MAU | 7,500 MAU | 10,000 MAU |
| Feature flags | ✅ Built-in | ❌ None | ❌ None |
| Organizations | ✅ Built-in | ✅ Paid | ✅ Paid |
| Complexity | Low | High | Low |
| Enterprise SSO | ✅ From Pro | ✅ Paid | ✅ Paid |
| Analytics | ✅ Built-in | External | Basic |
| Webhooks | ✅ Yes | ✅ Actions | ✅ Yes |
| Pricing | From $25/mo | From $35/mo | From $25/mo |
| Setup time | 5 min | 30 min | 10 min |
Installation and setup
Next.js App Router
npm install @kinde-oss/kinde-auth-nextjsConfiguration
# .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/dashboardAPI route handler
// 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
// 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
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
// 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
'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
// 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
// Kinde Dashboard → Authentication → Social Connections
const socialProviders = [
'google',
'github',
'microsoft',
'apple',
'facebook',
'linkedin',
'twitter',
'discord',
'gitlab',
'bitbucket',
'slack',
'spotify',
'twitch',
]Configuring a specific provider
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
// 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
// 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
'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
// 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
// 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
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
// 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
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
// 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
// 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)
// 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)
'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
// 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
// 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
// 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
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
// 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
// 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
// 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 kindeManagementUser management
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
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
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
// 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
| Plan | Price | MAU | Features |
|---|---|---|---|
| Free | $0/mo | 10,500 | Auth, Feature Flags, 3 Orgs |
| Pro | $25/mo | Unlimited | Enterprise SSO, Unlimited Orgs |
| Enterprise | Custom | Custom | Custom 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.