🔐 Authentication System Implementation Guide
📖 Overview
This guide covers the authentication system implementation using Clerk for user management and Supabase for data storage. It includes recent fixes, best practices, and troubleshooting information.
🏗 Authentication Architecture
⚡ Technology Stack:
- Clerk: User authentication and management
- Supabase: Database and user data storage
- Next.js 13+: App Router with server/client components
- TypeScript: Type safety for auth flows
🔄 Authentication Flow:
User Login → Clerk Authentication → Next.js Middleware → Supabase User Lookup → Role Authorization
🗄 User Data Storage
-- users table structure
CREATE TABLE users (
id TEXT PRIMARY KEY, -- Clerk user ID
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'editor' CHECK (role IN ('super_admin', 'owner', 'editor')),
organization_id UUID REFERENCES organizations(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
status TEXT DEFAULT 'accepted' CHECK (status IN ('pending', 'accepted')),
first_name TEXT,
last_name TEXT,
invitation_token TEXT
);📌 Clerk Integration
📌 Environment Variables:
📌 Required Clerk variables
🔐 Client-Side Authentication:
📌 For React Components:
export default function MyComponent() { const { userId, isSignedIn } = useAuth(); const { user } = useUser();
// Access user profile data const userEmail = user?.primaryEmailAddress?.emailAddress; const userName = user?.fullName || user?.firstName; }
<div style="background: rgba(59, 130, 246, 0.05); border-left: 2px solid #3b82f6; padding: 1rem; margin: 1.5rem 0; border-radius: 6px;">
<span style="font-size: 1.2rem; font-weight: 500; color: #1d4ed8;">📌 **Common Mistakes to Avoid**:</span>
</div>
```typescript
// ❌ WRONG - useAuth() doesn't return user object
const { userId, user } = useAuth();
// ✅ CORRECT - Use separate hooks
const { userId } = useAuth();
const { user } = useUser();
🔐 Server-Side Authentication:
🔌 For API Routes:
export async function GET(request: NextRequest) { const { userId } = await auth();
if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
// Continue with authenticated logic }
<div style="background: rgba(59, 130, 246, 0.05); border-left: 2px solid #3b82f6; padding: 1rem; margin: 1.5rem 0; border-radius: 6px;">
<span style="font-size: 1.2rem; font-weight: 500; color: #1d4ed8;">📌 **Import Path Important**:</span>
</div>
```typescript
// ✅ CORRECT - For API routes
import { auth } from '@clerk/nextjs/server';
// ❌ WRONG - Old import path
import { auth } from '@clerk/nextjs';
🔐 Role-Based Authorization
📌 Role System:
type UserRole = 'super_admin' | 'owner' | 'editor';📌 Role Permissions:
📌 Role Checking Implementation:
🗄️ Database Role Lookup:
if (error || !user) { return null; }
return user.role as UserRole; }
<div style="background: rgba(59, 130, 246, 0.05); border-left: 2px solid #3b82f6; padding: 1rem; margin: 1.5rem 0; border-radius: 6px;">
<span style="font-size: 1.2rem; font-weight: 500; color: #1d4ed8;">🔌 **API Route Authorization**:</span>
</div>
```typescript
export async function GET(request: NextRequest) {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check admin role for admin endpoints
const userRole = await getUserRole(userId);
if (!userRole || !['admin', 'super_admin'].includes(userRole)) {
return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 });
}
// Continue with admin logic
}
📌 Client-Side Role Display:
useEffect(() => { async function fetchUserRole() { if (!userId) return;
const { data } = await supabase
.from('users')
.select('role')
.eq('id', userId)
.single();
if (data) {
setRole(data.role as UserRole);
}
}
fetchUserRole();
}, [userId]);
return (
{/* Regular navigation items */} {/* Admin-only navigation */}
{role && ['admin', 'super_admin'].includes(role) && (
<AdminNavigation />
)}
</nav>
); }
<div style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; padding: 1.5rem; border-radius: 12px; margin: 2rem 0;">
<span style="font-size: 1.8rem; font-weight: 700;">📌 Team Management System</span>
</div>
<div style="background: rgba(220, 38, 38, 0.1); border-left: 4px solid #dc2626; padding: 1.5rem; margin: 2rem 0; border-radius: 8px;">
<span style="font-size: 1.5rem; font-weight: 600; color: #b91c1c;">📌 Invitation Flow:</span>
</div>
Admin Invites User → Email Sent → User Clicks Link → Clerk Registration → Database Record Created
<div style="background: rgba(220, 38, 38, 0.1); border-left: 4px solid #dc2626; padding: 1.5rem; margin: 2rem 0; border-radius: 8px;">
<span style="font-size: 1.5rem; font-weight: 600; color: #b91c1c;">🗄️ Database Schema for Invitations:</span>
</div>
```sql
-- Invitation tracking in users table
INSERT INTO users (
id, -- Temporary ID: 'invited_[uuid]'
email, -- Invitation email
role, -- Assigned role: 'user' or 'admin'
organization_id, -- Organization they're joining
status, -- 'pending' until they accept
invited_at, -- Invitation timestamp
invitation_token -- Security token for verification
);
📌 Super Admin Creation System
📊 Overview
📌 Method 1: Environment Variable (Recommended)
Best for: Production environments, team-based development
Setup:
- Add authorized emails to your environment variables:
SUPER_ADMIN_EMAILS=tfortner@banyanlabs.io,thalsell@banyanlabs.io,scallins@banyanlabs.io- The system automatically creates super admin accounts when authorized users sign in:
// lib/auth/super-admin-creation.ts
export async function autoCreateSuperAdminIfAuthorized(): Promise<SuperAdminCreationResult | null> {
const user = await currentUser();
const email = user.emailAddresses[0].emailAddress;
if (isAuthorizedForSuperAdmin(email)) {
return await createSuperAdminAccount(user.id, email);
}
return null;
}Implementation Steps:
- Update
.env.localwithSUPER_ADMIN_EMAILS - Restart your development server
- Have authorized team members sign in - they'll automatically become super admins
- Verify in Supabase that their role is set to
super_admin
📌 Method 2: First User Super Admin
Best for: Initial setup, single-admin scenarios
Setup:
- Enable in environment variables:
ENABLE_FIRST_USER_SUPER_ADMIN=true- The system checks if any super admins exist and promotes the first user:
// Auto-promotes first user if no super admins exist
if (ENABLE_FIRST_USER_SUPER_ADMIN) {
const superAdminExists = await checkSuperAdminExists();
if (!superAdminExists) {
return await createSuperAdminAccount(user.id, email);
}
}Implementation Steps:
- Set
ENABLE_FIRST_USER_SUPER_ADMIN=truein.env.local - Ensure no super admins exist in your database
- Sign up/sign in as the first user - you'll become super admin
- Disable this setting after initial setup for security
📌 Method 3: Secret Key Emergency Access
Best for: Emergency situations, backup access
Setup:
- Configure secret key and enable the feature:
SUPER_ADMIN_SECRET=your_ultra_secure_secret_key_here
ENABLE_SECRET_URL_CREATION=true- Access the emergency creation page at
/create-super-admin
Implementation Steps:
- Set both environment variables in
.env.local - Navigate to
http://localhost:3000/create-super-admin - Enter the target email and secret key
- Sign in with the target email to complete super admin creation
- Disable this feature after use for security
🔒 Security Considerations
Environment Variable Method (Most Secure):
- ✅ No user interface exposure
- ✅ Controlled by environment configuration
- ✅ Easy to audit and manage
- ✅ Works automatically on sign-in
First User Method (Moderate Security):
⚠️ Should be disabled after initial setup⚠️ Could accidentally promote wrong user- ✅ Good for initial development setup
- ✅ No secrets to manage
Secret Key Method (Emergency Only):
⚠️ Exposes UI endpoint (can be disabled)⚠️ Requires secure secret management- ✅ Works when other methods fail
- ✅ Provides audit trail
🔍 Troubleshooting Super Admin Creation
Common Issues:
-
"Email not authorized" error:
- Check
SUPER_ADMIN_EMAILSspelling and format - Ensure no extra spaces around email addresses
- Verify environment variables are loaded (restart dev server)
- Check
-
Auto-creation not working:
- Verify the user signed in with Clerk successfully
- Check browser console for errors
- Confirm environment variables are set correctly
-
Secret key method failing:
- Ensure both
SUPER_ADMIN_SECRETandENABLE_SECRET_URL_CREATION=true - Check the secret key matches exactly (case-sensitive)
- Verify the user signed in before accessing the page
- Ensure both
Database Verification:
-- Check super admin users
SELECT id, email, role, created_at
FROM users
WHERE role = 'super_admin';
-- Check user role after sign-in
SELECT id, email, role, status, organization_id
FROM users
WHERE email = 'your-email@banyanlabs.io';📌 Role System Updates:
-- Current roles (active)
'editor' -- Basic team member, content editing
'owner' -- Organization administrator
'super_admin' -- Platform administrator🔌 Team Invitation API:
// Validate role against current system if (!['user', 'admin'].includes(role)) { return NextResponse.json({ error: 'Invalid role' }, { status: 400 }); }
// Create pending user record
const tempId = invited_${crypto.randomUUID()};
const invitationToken = createInvitationToken();
await supabaseAdmin.from('users').insert({ id: tempId, email, role, organization_id: organizationId, status: 'pending', invited_at: new Date().toISOString(), invitation_token: invitationToken });
// Send invitation email await sendInvitationEmail({ email, role, invitationToken }); }
<div style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: white; padding: 1.5rem; border-radius: 12px; margin: 2rem 0;">
<span style="font-size: 1.8rem; font-weight: 700;">🗄️ Database Integration</span>
</div>
<div style="background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 1.5rem; margin: 2rem 0; border-radius: 8px;">
<span style="font-size: 1.5rem; font-weight: 600; color: #1d4ed8;">🔧 Supabase Configuration:</span>
</div>
<div style="background: rgba(59, 130, 246, 0.05); border-left: 2px solid #3b82f6; padding: 1rem; margin: 1.5rem 0; border-radius: 6px;">
<span style="font-size: 1.2rem; font-weight: 500; color: #1d4ed8;">🔒 **Row Level Security (RLS)**:</span>
</div>
For most tables, we use application-level authorization instead of RLS due to Clerk integration complexity.
```sql
-- Disable RLS for tables with complex auth requirements
ALTER TABLE support_tickets DISABLE ROW LEVEL SECURITY;
ALTER TABLE users DISABLE ROW LEVEL SECURITY;
-- Keep RLS for simple cases
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
📌 User Data Synchronization:
if (error) { console.error('Failed to sync user to Supabase:', error); } }
<div style="background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 1.5rem; margin: 2rem 0; border-radius: 8px;">
<span style="font-size: 1.5rem; font-weight: 600; color: #1d4ed8;">🗄️ Common Database Queries:</span>
</div>
<div style="background: rgba(59, 130, 246, 0.05); border-left: 2px solid #3b82f6; padding: 1rem; margin: 1.5rem 0; border-radius: 6px;">
<span style="font-size: 1.2rem; font-weight: 500; color: #1d4ed8;">📌 **Get User with Organization**:</span>
</div>
```typescript
const { data: userWithOrg } = await supabase
.from('users')
.select(`
*,
organizations (
id,
name,
)
`)
.eq('id', userId)
.single();
📌 Get Organization Members:
📌 Error Handling
🔐 Common Authentication Errors:
📌 1. Import Path Errors:
📌 2. User Property Not Found:
🗄️ 3. Database Column Mismatch:
📌 Error Response Patterns:
🔌 API Error Responses:
// Forbidden (valid user, wrong permissions) return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 });
// Not Found (resource doesn't exist) return NextResponse.json({ error: 'User not found' }, { status: 404 });
// Server Error (database/system error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
<div style="background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%); color: white; padding: 1.5rem; border-radius: 12px; margin: 2rem 0;">
<span style="font-size: 1.8rem; font-weight: 700;">🔒 Security Best Practices</span>
</div>
<div style="background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 1.5rem; margin: 2rem 0; border-radius: 8px;">
<span style="font-size: 1.5rem; font-weight: 600; color: #1d4ed8;">🔒 API Route Security:</span>
</div>
```typescript
export async function POST(request: NextRequest) {
try {
// 1. Always verify authentication first
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// 2. Validate request data
const body = await request.json();
if (!body.email || !isValidEmail(body.email)) {
return NextResponse.json({ error: 'Invalid email' }, { status: 400 });
}
// 3. Check authorization for protected resources
if (isAdminEndpoint) {
const hasPermission = await checkAdminPermission(userId);
if (!hasPermission) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
}
// 4. Sanitize data before database operations
const sanitizedData = sanitizeInput(body);
// 5. Use parameterized queries
const { data, error } = await supabase
.from('table')
.select('*')
.eq('user_id', userId); // User can only access their own data
} catch (error) {
console.error('API Error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
🔒 Client-Side Security:
// ✅ CORRECT - Use API route const deleteUser = async (userId: string) => { const response = await fetch('/api/admin/users/delete', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId }) }); };
<div style="background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; padding: 1.5rem; border-radius: 12px; margin: 2rem 0;">
<span style="font-size: 1.8rem; font-weight: 700;">🧪 Testing Authentication</span>
</div>
<div style="background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 1.5rem; margin: 2rem 0; border-radius: 8px;">
<span style="font-size: 1.5rem; font-weight: 600; color: #1d4ed8;">🧪 Manual Testing Checklist:</span>
</div>
- [ ] **User registration** creates Supabase record
- [ ] **Role assignment** works correctly
- [ ] **Team invitations** send and process properly
- [ ] **Admin routes** require proper permissions
- [ ] **API authentication** blocks unauthorized access
- [ ] **User data sync** between Clerk and Supabase
<div style="background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; padding: 1.5rem; margin: 2rem 0; border-radius: 8px;">
<span style="font-size: 1.5rem; font-weight: 600; color: #1d4ed8;">🧪 Automated Testing:</span>
</div>
```typescript
// Example API route test
describe('POST /api/team/invite', () => {
it('requires authentication', async () => {
const response = await request(app)
.post('/api/team/invite')
.send({ email: 'test@example.com', role: 'user' })
.expect(401);
expect(response.body.error).toBe('Unauthorized');
});
it('validates role values', async () => {
const response = await authenticatedRequest
.post('/api/team/invite')
.send({ email: 'test@example.com', role: 'invalid_role' })
.expect(400);
expect(response.body.error).toContain('Invalid role');
});
});
🔍 Troubleshooting Guide
📌 Development Issues:
📌 Clerk Webhook Not Working:
🗄️ User Not Found in Database:
📌 Role Permissions Not Working:
📌 Production Issues:
🔐 Authentication Failing:
🗄️ Database Connection Issues:
📌 Debug Logging:
📖 Migration Guide
📌 Updating from Old Role System:
-- Update existing users
UPDATE users SET role = 'user' WHERE role = 'editor';
UPDATE users SET role = 'admin' WHERE role = 'owner';
-- Update UI components to use new role names
-- Update API validation to accept new roles
-- Update database constraints📌 Clerk Version Updates:
- Check breaking changes in Clerk documentation
- Update import paths if necessary
- Test authentication flows thoroughly
- Update TypeScript types if changed
Author: Development Team
Date: January 30, 2025
Last Updated: January 30, 2025
Next Review: March 30, 2025