# Multi-Tenant MCP Server Architecture (better-auth)
**Version**: 2.0
**Created**: 2026-01-02
**Status**: Planning
**Supersedes**: `MULTI_TENANT_ARCHITECTURE.md` (DIY approach)
---
## Table of Contents
1. [Overview](#overview)
2. [Why better-auth](#why-better-auth)
3. [Plugin Stack](#plugin-stack)
4. [Architecture Mapping](#architecture-mapping)
5. [Configuration](#configuration)
6. [Route Structure](#route-structure)
7. [Database Schema](#database-schema)
8. [Implementation Guide](#implementation-guide)
9. [Migration from DIY](#migration-from-diy)
10. [Implementation Phases](#implementation-phases)
---
## Overview
This document describes how to implement the multi-tenant MCP server using [better-auth](https://www.better-auth.com/) instead of DIY authentication.
**Key insight**: better-auth's OAuth Provider plugin has **explicit MCP support** with dynamic client registration, `mcpHandler()`, and all required RFC endpoints.
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ MULTI-TENANT MCP SERVER (better-auth) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ MCP CLIENT │ │ USER DASHBOARD │ │ ADMIN DASHBOARD │ │
│ │ (Claude.ai) │ │ (Self-Service) │ │ (Full Control) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ better-auth │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │OAuth Provider│ │ Admin │ │ API Key │ │ Organization │ │ │
│ │ │ (MCP OAuth) │ │ Plugin │ │ Plugin │ │ Plugin │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Social │ │ JWT │ │ Two-Factor │ │ │
│ │ │ (G/MS/GH) │ │ Plugin │ │ (optional) │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Cloudflare D1 (via Drizzle) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
---
## Why better-auth
### DIY vs better-auth Comparison
| Component | DIY Approach | better-auth | Winner |
|-----------|--------------|-------------|--------|
| MCP OAuth Server | Custom with `@cloudflare/workers-oauth-provider` | OAuth Provider plugin with MCP support | **better-auth** (same features, less code) |
| Social Login | Manual OAuth flows per provider | Built-in (20+ providers) | **better-auth** |
| Session Management | Custom KV storage | Built-in with hooks | **better-auth** |
| Admin Features | Build from scratch | Admin plugin (ban, impersonate, roles) | **better-auth** |
| API Keys | Custom bearer tokens | API Key plugin with scopes/quotas | **better-auth** |
| Multi-tenant | Not planned initially | Organization plugin | **better-auth** |
| User Management | Custom CRUD | Built-in + Admin plugin | **better-auth** |
| Token Encryption | DIY with Web Crypto | Automatic | **better-auth** |
| Schema/Migrations | Manual SQL | CLI generates | **better-auth** |
| Security | Self-maintained | Community-reviewed | **better-auth** |
### Lines of Code Estimate
| Component | DIY (estimated) | better-auth |
|-----------|-----------------|-------------|
| Auth infrastructure | ~2000 lines | ~200 lines config |
| Admin features | ~1000 lines | Plugin config |
| API key system | ~500 lines | Plugin config |
| Multi-tenant | ~1500 lines | Plugin config |
| **Total** | **~5000 lines** | **~300 lines** |
---
## Plugin Stack
### Required Plugins
```typescript
import { betterAuth } from "better-auth";
import { oAuthProvider } from "better-auth/plugins/oauth-provider";
import { admin } from "better-auth/plugins/admin";
import { apiKey } from "better-auth/plugins/api-key";
import { organization } from "better-auth/plugins/organization";
import { jwt } from "better-auth/plugins/jwt";
```
### Plugin Responsibilities
| Plugin | Responsibility |
|--------|----------------|
| **OAuth Provider** | MCP OAuth server (Claude.ai/Code authentication) |
| **Social Providers** | Google, Microsoft, GitHub login |
| **Admin** | User management, ban/unban, impersonation |
| **API Key** | Programmatic API access with scopes |
| **Organization** | Multi-tenant workspaces, teams, roles |
| **JWT** | Required by OAuth Provider for OIDC |
---
## Architecture Mapping
### Three-Tier Access Model
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ACCESS TIERS │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ TIER 1: MCP Client Access │
│ ├── Route: /api/auth/oauth2/* (better-auth OAuth Provider) │
│ ├── Auth: OAuth 2.1 with PKCE │
│ ├── Clients: Claude.ai, Claude Code, API │
│ └── Features: Tool execution via MCP protocol │
│ │
│ TIER 2: User Dashboard Access │
│ ├── Route: /dashboard/* │
│ ├── Auth: better-auth session (social login) │
│ ├── Features: │
│ │ ├── View own profile │
│ │ ├── Manage connected services (Layer 3) │
│ │ ├── Test tools interactively │
│ │ ├── View own sessions and usage │
│ │ └── Organization membership │
│ └── Middleware: requireSession() │
│ │
│ TIER 3: Admin Dashboard Access │
│ ├── Route: /admin/* │
│ ├── Auth: better-auth session + admin role │
│ ├── Features: │
│ │ ├── All user dashboard features │
│ │ ├── User management (ban, impersonate) │
│ │ ├── Configure shared services (Layer 2) │
│ │ ├── Manage API keys │
│ │ ├── Organization management │
│ │ └── Analytics and audit logs │
│ └── Middleware: requireAdmin() │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
### Three-Layer Auth Model
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ AUTH LAYERS │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: Identity ("Who are you?") │
│ ├── Provider: better-auth core + social providers │
│ ├── Storage: D1 `user` table (managed by better-auth) │
│ ├── Session: D1 `session` table (managed by better-auth) │
│ └── Features: │
│ ├── Google OAuth │
│ ├── Microsoft OAuth │
│ ├── GitHub OAuth │
│ └── Session management with hooks │
│ │
│ LAYER 2: Shared Services ("What can everyone use?") │
│ ├── Provider: Custom (on top of better-auth) │
│ ├── Storage: D1 `shared_service` table + KV for secrets │
│ ├── Access: Admin-only configuration │
│ └── Examples: │
│ ├── Team Google Calendar (service account) │
│ ├── Synergy Wholesale API key │
│ └── Shared database connection │
│ │
│ LAYER 3: User Services ("What can THIS user access?") │
│ ├── Provider: Custom OAuth flows (on top of better-auth) │
│ ├── Storage: D1 `user_service_token` table (encrypted) │
│ ├── Access: Per-user self-service │
│ └── Examples: │
│ ├── User's personal Google Calendar │
│ ├── User's Xero account │
│ └── User's Notion workspace │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
---
## Configuration
### better-auth Configuration
```typescript
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { oAuthProvider } from "better-auth/plugins/oauth-provider";
import { admin } from "better-auth/plugins/admin";
import { apiKey } from "better-auth/plugins/api-key";
import { organization } from "better-auth/plugins/organization";
import { jwt } from "better-auth/plugins/jwt";
// Factory pattern for Cloudflare Workers (bindings not available at module level)
export function createAuth(env: Env) {
return betterAuth({
database: drizzleAdapter(drizzle(env.DB), {
provider: "sqlite",
}),
// ═══════════════════════════════════════════════════════════════
// SOCIAL PROVIDERS (Layer 1 Identity)
// ═══════════════════════════════════════════════════════════════
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
microsoft: {
clientId: env.MICROSOFT_CLIENT_ID,
clientSecret: env.MICROSOFT_CLIENT_SECRET,
tenantId: env.MICROSOFT_TENANT_ID || "common",
},
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
},
// ═══════════════════════════════════════════════════════════════
// PLUGINS
// ═══════════════════════════════════════════════════════════════
plugins: [
// JWT (required for OAuth Provider)
jwt(),
// OAuth Provider for MCP clients
oAuthProvider({
// Required for MCP
allowDynamicClientRegistration: true,
allowUnauthenticatedClientRegistration: true, // MCP public clients
// Consent and login pages
loginPage: "/login",
consentPage: "/consent",
// Access token configuration
accessTokenExpiresIn: 3600, // 1 hour
refreshTokenExpiresIn: 604800, // 7 days
}),
// Admin features
admin({
defaultRole: "user",
adminRole: "admin",
}),
// API key authentication
apiKey({
// Default permissions for new keys
defaultPermissions: {
tools: ["execute"],
},
// Rate limiting
rateLimit: {
window: 60, // seconds
max: 100, // requests per window
},
}),
// Multi-tenant organizations
organization({
// Allow users to create organizations
allowUserToCreateOrganization: true,
// Default role when joining
defaultRole: "member",
// Enable teams within orgs
teams: {
enabled: true,
maximumTeams: 10,
},
// Invitation settings
invitations: {
expiresIn: 48 * 60 * 60, // 48 hours
sendInvitationEmail: async ({ email, invitationId, organization }) => {
// Custom email sending logic
console.log(`Invite ${email} to ${organization.name}`);
},
},
}),
],
// ═══════════════════════════════════════════════════════════════
// SESSION CONFIGURATION
// ═══════════════════════════════════════════════════════════════
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // Update session every 24 hours
cookieCache: {
enabled: true,
maxAge: 60 * 5, // 5 minutes
},
},
// ═══════════════════════════════════════════════════════════════
// ADVANCED OPTIONS
// ═══════════════════════════════════════════════════════════════
advanced: {
// Use secure cookies
useSecureCookies: true,
// Cross-subdomain cookies (if needed)
crossSubDomainCookies: {
enabled: false,
},
},
});
}
// Type export for client
export type Auth = ReturnType<typeof createAuth>;
```
### Environment Variables
```jsonc
// wrangler.jsonc
{
"vars": {
// Feature flags
"ENABLE_USER_DASHBOARD": "true",
"ENABLE_ADMIN_CHAT": "true",
// better-auth
"BETTER_AUTH_URL": "https://my.mcp.jezweb.ai",
"BETTER_AUTH_SECRET": "", // Set via wrangler secret
}
}
```
### Secrets
```bash
# better-auth secret (generate with: openssl rand -base64 32)
wrangler secret put BETTER_AUTH_SECRET
# Social providers
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put MICROSOFT_CLIENT_ID
wrangler secret put MICROSOFT_CLIENT_SECRET
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET
```
---
## Route Structure
### Hono Router Integration
```typescript
// src/index.ts
import { Hono } from "hono";
import { createAuth } from "./lib/auth";
const app = new Hono<{ Bindings: Env }>();
// ═══════════════════════════════════════════════════════════════════
// better-auth handles these automatically:
// ═══════════════════════════════════════════════════════════════════
// /api/auth/* - All auth endpoints
// /api/auth/oauth2/authorize - OAuth authorization
// /api/auth/oauth2/token - Token exchange
// /api/auth/oauth2/register - Dynamic client registration
// /api/auth/oauth2/introspect - Token introspection
// /api/auth/callback/google - Google OAuth callback
// /api/auth/callback/microsoft - Microsoft OAuth callback
// /api/auth/callback/github - GitHub OAuth callback
// Mount better-auth
app.on(["GET", "POST"], "/api/auth/*", async (c) => {
const auth = createAuth(c.env);
return auth.handler(c.req.raw);
});
// ═══════════════════════════════════════════════════════════════════
// Well-known endpoints (required for MCP)
// ═══════════════════════════════════════════════════════════════════
app.get("/.well-known/oauth-authorization-server", async (c) => {
const auth = createAuth(c.env);
const metadata = await auth.api.getOAuthMetadata();
return c.json(metadata);
});
app.get("/.well-known/oauth-protected-resource", async (c) => {
const auth = createAuth(c.env);
const metadata = await auth.api.getProtectedResourceMetadata({
resource: c.env.BETTER_AUTH_URL,
});
return c.json(metadata);
});
// ═══════════════════════════════════════════════════════════════════
// MCP endpoint (protected by OAuth)
// ═══════════════════════════════════════════════════════════════════
app.all("/mcp", async (c) => {
const auth = createAuth(c.env);
// Use mcpHandler for token validation
return auth.api.mcpHandler(c.req.raw, async (req, jwt) => {
// jwt contains validated token claims
// jwt.sub = user ID
// jwt.scope = granted scopes
// Handle MCP protocol (SSE/WebSocket)
return handleMcpRequest(c, jwt);
});
});
// ═══════════════════════════════════════════════════════════════════
// User Dashboard (requires session)
// ═══════════════════════════════════════════════════════════════════
app.get("/dashboard/*", async (c) => {
const auth = createAuth(c.env);
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
return c.redirect("/login?redirect=/dashboard");
}
// Render dashboard
return renderUserDashboard(c, session);
});
// ═══════════════════════════════════════════════════════════════════
// Admin Dashboard (requires admin role)
// ═══════════════════════════════════════════════════════════════════
app.get("/admin/*", async (c) => {
const auth = createAuth(c.env);
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
return c.redirect("/login?redirect=/admin");
}
// Check admin role
if (session.user.role !== "admin") {
return c.text("Forbidden", 403);
}
// Render admin dashboard
return renderAdminDashboard(c, session);
});
// ═══════════════════════════════════════════════════════════════════
// API endpoints (API key or session auth)
// ═══════════════════════════════════════════════════════════════════
app.use("/api/*", async (c, next) => {
const auth = createAuth(c.env);
// Try API key first
const apiKeyHeader = c.req.header("x-api-key");
if (apiKeyHeader) {
const result = await auth.api.verifyApiKey({
key: apiKeyHeader,
permissions: { tools: ["execute"] },
});
if (result.valid) {
c.set("user", result.user);
c.set("authMethod", "apiKey");
return next();
}
}
// Fall back to session
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (session) {
c.set("user", session.user);
c.set("authMethod", "session");
return next();
}
return c.json({ error: "Unauthorized" }, 401);
});
export default app;
```
### Complete Route Map
```
/ Public homepage
│
├── /.well-known/
│ ├── oauth-authorization-server MCP OAuth metadata
│ └── oauth-protected-resource MCP resource metadata
│
├── /api/auth/ better-auth endpoints (auto-generated)
│ ├── /callback/google Google OAuth callback
│ ├── /callback/microsoft Microsoft OAuth callback
│ ├── /callback/github GitHub OAuth callback
│ ├── /oauth2/authorize OAuth authorization
│ ├── /oauth2/token Token exchange
│ ├── /oauth2/register Dynamic client registration
│ ├── /oauth2/introspect Token introspection
│ ├── /oauth2/consent Consent page handler
│ ├── /session Get current session
│ ├── /sign-out Sign out
│ └── /... Other auth endpoints
│
├── /mcp MCP protocol endpoint (OAuth protected)
│
├── /login Login page (social providers)
├── /consent OAuth consent page
│
├── /dashboard/ User dashboard (session protected)
│ ├── / Overview
│ ├── /profile User profile
│ ├── /services Connected services (Layer 3)
│ ├── /tools Tool explorer
│ ├── /sessions Active sessions
│ └── /organizations Organization membership
│
├── /admin/ Admin dashboard (admin role required)
│ ├── / Overview
│ ├── /users User management
│ ├── /services Shared services (Layer 2)
│ ├── /api-keys API key management
│ ├── /organizations Organization management
│ └── /analytics Usage analytics
│
└── /api/ API endpoints (API key or session)
├── /user/* User API
├── /admin/* Admin API
└── /tools/* Tool API
```
---
## Database Schema
better-auth manages its own tables. We add custom tables for Layers 2 and 3.
### better-auth Managed Tables
```sql
-- These are created/managed by better-auth CLI
-- Run: npx @better-auth/cli generate
-- Core tables
user -- User accounts
session -- Active sessions
account -- OAuth provider links
-- OAuth Provider plugin
oauth_client -- Registered OAuth clients
oauth_authorization_code -- Authorization codes
oauth_access_token -- Access tokens
oauth_refresh_token -- Refresh tokens
oauth_consent -- User consents
-- Admin plugin
-- (Uses user.role field)
-- API Key plugin
api_key -- API keys with permissions
-- Organization plugin
organization -- Organizations/workspaces
member -- Organization members
invitation -- Pending invitations
team -- Teams within organizations
team_member -- Team memberships
```
### Custom Tables (Layers 2 & 3)
```sql
-- ═══════════════════════════════════════════════════════════════════
-- LAYER 2: Shared Services (Admin-configured)
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE shared_service (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- e.g., "google_calendar", "xero"
display_name TEXT NOT NULL, -- e.g., "Team Google Calendar"
type TEXT NOT NULL, -- "api_key" | "oauth" | "service_account"
config_encrypted TEXT NOT NULL, -- Encrypted JSON config
configured_by TEXT NOT NULL, -- User ID of admin who configured
configured_at INTEGER NOT NULL,
last_verified_at INTEGER,
status TEXT DEFAULT 'active' -- "active" | "error" | "disabled"
);
CREATE INDEX idx_shared_service_name ON shared_service(name);
-- ═══════════════════════════════════════════════════════════════════
-- LAYER 3: User Services (Per-user connections)
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE user_service_token (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL, -- References better-auth user.id
service TEXT NOT NULL, -- e.g., "google_calendar", "xero"
access_token_encrypted TEXT NOT NULL,
refresh_token_encrypted TEXT,
expires_at INTEGER,
scopes TEXT, -- Space-separated scopes
service_user_id TEXT, -- User's ID in external service
service_email TEXT, -- User's email in external service
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(user_id, service),
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
);
CREATE INDEX idx_user_service_token_user ON user_service_token(user_id);
CREATE INDEX idx_user_service_token_service ON user_service_token(service);
-- ═══════════════════════════════════════════════════════════════════
-- Tool Executions (Audit log)
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE tool_execution (
id TEXT PRIMARY KEY,
user_id TEXT, -- NULL if anonymous
session_id TEXT,
organization_id TEXT, -- If executed in org context
tool_name TEXT NOT NULL,
auth_type TEXT, -- "none" | "shared" | "user"
service_used TEXT, -- Which Layer 2/3 service
success INTEGER NOT NULL,
error_message TEXT,
duration_ms INTEGER,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE SET NULL
);
CREATE INDEX idx_tool_execution_user ON tool_execution(user_id);
CREATE INDEX idx_tool_execution_org ON tool_execution(organization_id);
CREATE INDEX idx_tool_execution_created ON tool_execution(created_at);
```
---
## Implementation Guide
### 1. Setup better-auth
```bash
# Install dependencies
npm install better-auth drizzle-orm
npm install -D @better-auth/cli drizzle-kit
# Generate schema
npx @better-auth/cli generate --config ./src/lib/auth-cli.ts
# Apply migrations
npx wrangler d1 execute DB --local --file=./drizzle/0001_better_auth.sql
npx wrangler d1 execute DB --remote --file=./drizzle/0001_better_auth.sql
```
### 2. CLI Config (for schema generation)
```typescript
// src/lib/auth-cli.ts
// Used by better-auth CLI (not runtime - uses process.env)
import { betterAuth } from "better-auth";
import { oAuthProvider } from "better-auth/plugins/oauth-provider";
import { admin } from "better-auth/plugins/admin";
import { apiKey } from "better-auth/plugins/api-key";
import { organization } from "better-auth/plugins/organization";
import { jwt } from "better-auth/plugins/jwt";
export default betterAuth({
database: {
provider: "sqlite",
url: "file:./local.db", // Placeholder for CLI
},
plugins: [
jwt(),
oAuthProvider({
allowDynamicClientRegistration: true,
allowUnauthenticatedClientRegistration: true,
loginPage: "/login",
consentPage: "/consent",
}),
admin(),
apiKey(),
organization({ teams: { enabled: true } }),
],
});
```
### 3. Login Page
```typescript
// src/pages/login.ts
export function getLoginPage(providers: string[]) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Sign In</title>
<style>
body { font-family: system-ui; max-width: 400px; margin: 50px auto; padding: 20px; }
.btn { display: block; padding: 12px; margin: 10px 0; text-align: center;
text-decoration: none; border-radius: 8px; font-weight: 500; }
.google { background: #4285f4; color: white; }
.microsoft { background: #00a4ef; color: white; }
.github { background: #333; color: white; }
</style>
</head>
<body>
<h1>Sign In</h1>
<p>Choose a provider to continue:</p>
${providers.includes('google') ? `
<a href="/api/auth/sign-in/social?provider=google" class="btn google">
Continue with Google
</a>
` : ''}
${providers.includes('microsoft') ? `
<a href="/api/auth/sign-in/social?provider=microsoft" class="btn microsoft">
Continue with Microsoft
</a>
` : ''}
${providers.includes('github') ? `
<a href="/api/auth/sign-in/social?provider=github" class="btn github">
Continue with GitHub
</a>
` : ''}
</body>
</html>
`;
}
```
### 4. Consent Page
```typescript
// src/pages/consent.ts
export function getConsentPage(client: { name: string }, scopes: string[]) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Authorize ${client.name}</title>
<style>
body { font-family: system-ui; max-width: 500px; margin: 50px auto; padding: 20px; }
.scopes { background: #f5f5f5; padding: 15px; border-radius: 8px; margin: 20px 0; }
.scope { padding: 5px 0; }
.buttons { display: flex; gap: 10px; }
.btn { padding: 12px 24px; border-radius: 8px; font-weight: 500; cursor: pointer; border: none; }
.allow { background: #10b981; color: white; }
.deny { background: #ef4444; color: white; }
</style>
</head>
<body>
<h1>Authorize ${client.name}</h1>
<p>This application is requesting access to:</p>
<div class="scopes">
${scopes.map(s => `<div class="scope">✓ ${s}</div>`).join('')}
</div>
<form method="POST" action="/api/auth/oauth2/consent">
<input type="hidden" name="client_id" value="${client.name}">
<div class="buttons">
<button type="submit" name="consent" value="granted" class="btn allow">Allow</button>
<button type="submit" name="consent" value="denied" class="btn deny">Deny</button>
</div>
</form>
</body>
</html>
`;
}
```
### 5. MCP Handler
```typescript
// src/mcp/handler.ts
import { createAuth } from "../lib/auth";
import type { JWTPayload } from "better-auth";
export async function handleMcpRequest(c: Context, jwt: JWTPayload) {
// jwt.sub = user ID
// jwt.scope = granted scopes
const auth = createAuth(c.env);
// Get full user from database
const user = await auth.api.getUser({ userId: jwt.sub });
// Get organization context (if applicable)
const orgId = c.req.header("X-Organization-Id");
let organization = null;
if (orgId) {
organization = await auth.api.getOrganization({ organizationId: orgId });
}
// Handle MCP protocol
// ... existing MCP logic with user context
}
```
---
## Migration from DIY
### Files to Remove
```
src/oauth/ # All DIY OAuth handlers
├── google-handler.ts DELETE
├── microsoft-handler.ts DELETE
├── github-handler.ts DELETE
├── handler-factory.ts DELETE
└── workers-oauth-utils.ts DELETE
src/auth/ # DIY auth module
├── identity/ DELETE entire directory
└── backend/ DELETE entire directory
src/admin/
├── session.ts DELETE (replaced by better-auth)
├── middleware.ts REPLACE with better-auth middleware
└── tokens.ts DELETE (replaced by API Key plugin)
```
### Files to Keep/Modify
```
src/index.ts MODIFY - Mount better-auth, update routes
src/admin/routes.ts MODIFY - Use better-auth for auth checks
src/admin/ui.ts KEEP - Dashboard HTML (update auth checks)
src/mcp/ KEEP - MCP protocol handling
src/tools/ KEEP - Tool definitions
src/lib/ai/ KEEP - AI chat functionality
```
### Migration Steps
1. **Install better-auth** and dependencies
2. **Generate schema** with better-auth CLI
3. **Run migrations** on D1
4. **Create auth config** (`src/lib/auth.ts`)
5. **Update index.ts** to mount better-auth
6. **Create login/consent pages**
7. **Update dashboard** to use better-auth sessions
8. **Remove DIY auth code**
9. **Test all flows**
---
## Implementation Phases
### Phase 3a: better-auth Core Setup
**Estimate**: 2-3 hours
- [ ] Install better-auth + plugins
- [ ] Create auth configuration
- [ ] Generate and apply schema
- [ ] Mount better-auth routes in Hono
- [ ] Create login page with social providers
- [ ] Create consent page for MCP
- [ ] Test social login (Google/Microsoft/GitHub)
### Phase 3b: MCP OAuth Integration
**Estimate**: 2-3 hours
- [ ] Configure OAuth Provider plugin for MCP
- [ ] Set up well-known endpoints
- [ ] Implement `mcpHandler` for token validation
- [ ] Update MCP endpoint to use better-auth
- [ ] Test with Claude.ai connection
- [ ] Test with Claude Code connection
### Phase 3c: Admin Features
**Estimate**: 2-3 hours
- [ ] Configure Admin plugin
- [ ] Update admin dashboard to use better-auth session
- [ ] Implement user management UI
- [ ] Implement ban/unban functionality
- [ ] Add impersonation feature
- [ ] Remove DIY admin auth code
### Phase 3d: API Key System
**Estimate**: 1-2 hours
- [ ] Configure API Key plugin
- [ ] Create API key management UI in admin
- [ ] Update API endpoints to accept API keys
- [ ] Add permission scopes for tools
- [ ] Remove DIY bearer token code
### Phase 3e: Organization Support
**Estimate**: 2-3 hours
- [ ] Configure Organization plugin
- [ ] Add organization UI to user dashboard
- [ ] Implement invitation flow
- [ ] Add team management
- [ ] Test multi-tenant tool execution
### Phase 3f: Layer 2 - Shared Services
**Estimate**: 3-4 hours
- [ ] Create shared_service table
- [ ] Implement encryption utilities
- [ ] Admin UI for service configuration
- [ ] API key storage flow
- [ ] OAuth service account flow
- [ ] Tool authorization integration
### Phase 3g: Layer 3 - User Services
**Estimate**: 3-4 hours
- [ ] Create user_service_token table
- [ ] User dashboard service connection UI
- [ ] OAuth flows for external services
- [ ] Token refresh automation
- [ ] Tool authorization integration
### Phase 3h: Cleanup & Testing
**Estimate**: 2-3 hours
- [ ] Remove all DIY auth code
- [ ] Update documentation
- [ ] End-to-end testing
- [ ] Performance testing
- [ ] Security review
---
## References
- [better-auth Documentation](https://www.better-auth.com/docs)
- [OAuth Provider Plugin](https://www.better-auth.com/docs/plugins/oauth-provider)
- [Admin Plugin](https://www.better-auth.com/docs/plugins/admin)
- [API Key Plugin](https://www.better-auth.com/docs/plugins/api-key)
- [Organization Plugin](https://www.better-auth.com/docs/plugins/organization)
- [better-auth-cloudflare](https://github.com/zpg6/better-auth-cloudflare)
- [Hono + better-auth on Cloudflare](https://hono.dev/examples/better-auth-on-cloudflare)
- [Drizzle Adapter](https://www.better-auth.com/docs/adapters/drizzle)