How Kaneo's Authentication System Works with Better Auth and API Keys
Kaneo uses the Better Auth library as its single source of truth for authentication, configured in apps/api/src/auth.ts with support for email/password, OAuth, magic links, bearer tokens, and API key authentication via the x-api-key header.
Kaneo's backend authentication is powered entirely by Better Auth, a modern TypeScript authentication library. The @kaneo/api package centralizes all auth concerns in a single configuration file that wires together PostgreSQL-backed sessions, multiple sign-in methods, and fine-grained access controls.
Better Auth Configuration Architecture
The auth engine is instantiated in apps/api/src/auth.ts using betterAuth({...}) with a server-side base path of /api/auth and custom trusted origins. The configuration spans nearly 600 lines and defines every aspect of how users prove their identity and access the system.
Database Adapter and Schema
Better Auth connects to PostgreSQL through the Drizzle ORM adapter:
// apps/api/src/auth.ts L75-L91
drizzleAdapter(db, {
provider: "pg",
schema: {
user, account, session, verification,
workspace, workspaceMember, ...
}
})
All required tables—including user, account, session, verification, and Kaneo's custom workspace tables—are defined in apps/api/src/database/schema.ts and passed directly to the adapter.
Extended User Model
The user schema adds a locale field for localized email flows:
// apps/api/src/auth.ts L94-L100
{
type: "string",
input: true
}
This field is captured during registration and used to render emails in the user's preferred language.
Authentication Methods in Kaneo
Kaneo supports multiple authentication strategies through Better Auth plugins. Each method can be enabled or disabled via environment variables.
Email and Password Authentication
Password login uses bcrypt hashing with custom hash and verify callbacks:
// apps/api/src/auth.ts L116-L127
emailAndPassword: {
enabled: true,
async hash(password) { /* bcrypt implementation */ },
async verify({ password, hash }) { /* bcrypt verify */ },
autoSignInAfterRegistration: true,
}
The autoSignInAfterRegistration flag eliminates an extra round-trip after account creation.
Social OAuth Providers
GitHub, Google, and Discord are pre-configured. GitHub credentials are resolved through a helper function:
// apps/api/src/auth.ts L128-L140
socialProviders: {
github: {
clientId: getGithubSsoOAuthCredentials().clientId,
clientSecret: getGithubSsoOAuthCredentials().clientSecret,
},
google: { /* env-based */ },
discord: { /* env-based */ },
}
The organization plugin enables automatic account linking when OAuth accounts share a verified email with an existing local account. Trusted providers are whitelisted: github, google, discord, and custom.
Magic Links and Email OTP
Passwordless authentication flows are handled by dedicated plugins:
- Magic Link: Sends time-limited sign-in links via
sendMagicLinkEmail - Email OTP: One-time passcodes (disabled when
DISABLE_EMAIL_OTP_SIGN_INis true)
Both methods integrate with the same session system as password logins.
API Key Authentication
The apiKey Plugin
Kaneo supports machine-to-machine authentication through the apiKey plugin, configured at lines 268-278:
// apps/api/src/auth.ts L268-L278
apiKey({
apiKeyPrefixes: {
default: "kaneo_",
nextJs: "kaneo_nextjs_",
},
apiKeySession: { enabled: false },
rateLimit: { enabled: true },
})
How to Use API Keys
API keys are passed in the x-api-key header:
// Client request example
await fetch(`${process.env.KANEO_API_URL}/api/tasks`, {
headers: { "x-api-key": "kaneo_live_abc123xyz789" },
});
Unlike bearer tokens from user sessions, API keys:
- Use configurable prefixes for key type identification
- Optionally create ephemeral sessions (disabled in Kaneo's config)
- Have independent rate-limiting rules
Bearer Token Alternative
The bearer plugin enables standard OAuth-style bearer token authentication for API clients that obtain tokens through user flows rather than static keys.
Workspace (Organization) Integration
Kaneo maps Better Auth's organization concept to its own workspace model. This plugin handles:
- Role-based access: Dynamic permissions within workspaces
- Team management: Grouping members into functional teams
- Invitations: Email-based workspace invites via
sendWorkspaceInvitationEmail
Custom metadata fields store workspace descriptions:
// apps/api/src/auth.ts L212-L255
{
customFields: {
description: { type: "string", nullable: true }
},
requireEmailVerificationOnInvitation: false,
}
The requireEmailVerificationOnInvitation: false setting streamlines onboarding by bypassing email verification for invited users.
Session and Security Controls
Session Management
Sessions are database-backed with a 5-minute cookie cache:
// apps/api/src/auth.ts L336-L341
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
},
}
Cookie attributes are hardened for production:
sameSite: "none"for cross-subdomain deploymentssecure: truewhen HTTPS is available
Rate Limiting
Global and per-endpoint rate limits protect against abuse:
// apps/api/src/auth.ts L496-L506
rateLimit: {
windowMs: 10000, // 10 seconds
quota: 100,
customRules: {
"/sign-up/email": { quota: 3 },
"/organization/invite-member": { quota: 10 },
},
}
Stricter limits apply to registration and invitation endpoints when IS_CLOUD mode is active.
Authentication Hooks
Better Auth's hook system implements Kaneo's custom business logic.
Before Hook
The createAuthMiddleware before hook enforces:
- Disabled login form checks
- Guest invitation abuse prevention in cloud mode
- Disposable email blocking
- Turnstile verification for suspicious registrations
After Hook
Post-authentication, the after hook attaches the active workspace ID to the session record after successful sign-up or sign-in, ensuring consistent workspace context across requests.
Device Authorization for CLI Tools
The deviceAuthorization plugin enables OAuth-style device flows for command-line clients:
// apps/api/src/auth.ts L280-L283
deviceAuthorization({
verificationUri: (clientUrl, deviceCode) =>
`${clientUrl}/verify-device?code=${deviceCode}`,
clientIds: ["kaneo-cli"],
})
CLI tools initiate flow with:
const device = await auth.api.deviceAuthorization({
clientId: "kaneo-cli",
});
console.log(`Visit ${device.verificationUri} and enter ${device.userCode}`);
Key Files Reference
| File | Purpose |
|---|---|
apps/api/src/auth.ts |
Central Better Auth configuration (~600 lines) |
apps/api/src/database/schema.ts |
Drizzle tables for users, sessions, workspaces |
apps/api/src/utils/check-registration-allowed.ts |
Registration policy enforcement |
apps/web/src/lib/auth-client.ts |
Frontend React wrapper |
apps/web/src/routes/auth/* |
Sign-up, sign-in, magic-link UI routes |
Summary
- Better Auth serves as Kaneo's unified authentication engine, configured in
apps/api/src/auth.ts - Database layer: Drizzle adapter with PostgreSQL, all auth tables in shared workspace schema
- Human authentication: Email/password, OAuth (GitHub/Google/Discord), magic links, email OTP, anonymous guests
- Machine authentication: API keys via
x-api-keyheader, bearer tokens, device authorization flow for CLI - Workspace mapping: Better Auth organizations mapped to Kaneo workspaces with custom roles and invitations
- Security: bcrypt password hashing, rate limiting, hardened cookies, registration hooks with disposable email checks
Frequently Asked Questions
How do I authenticate API requests to Kaneo?
Use the x-api-key header with a key generated through the API key plugin. Keys use the kaneo_ prefix by default. Bearer tokens from user sessions are also accepted via the bearer plugin.
Can I disable password registration while keeping OAuth sign-in?
Yes. Set DISABLE_PASSWORD_REGISTRATION=true to block email/password sign-ups while leaving OAuth providers active. The before hook enforces this check in apps/api/src/auth.ts.
How does Kaneo handle the first user created?
The registration flow checks if any users exist. If not, the new user is promoted to instance admin within a PostgreSQL transaction guarded by an advisory lock, preventing race conditions in multi-replica deployments.
What's the difference between workspace invitations and regular sign-up?
Invited users skip email verification (requireEmailVerificationOnInvitation: false) and are directly added to the target workspace. Standard registration may require verification depending on your configuration, and users start without workspace membership.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →