diff --git a/README.md b/README.md
index 259e2ae..df3c22f 100644
--- a/README.md
+++ b/README.md
@@ -222,8 +222,8 @@ See [docs/store/quick-start.md](./docs/store/quick-start.md) for more commands.
 
 ### Admin Routes (Protected)
 - `/admin` - Admin dashboard
-- `/admin/users` - User management
-- `/admin/requests` - Access request management
+- `/admin/users` - User management (includes Users and Requests tabs)
+- `/admin/users/requests` - Access request management
 - `/admin/data` - Redis data inspection & RIS collection
 - `/admin/ingest` - Content ingestion (legacy)
 - `/admin/ingest-full` - Full content ingestion
@@ -263,6 +263,19 @@ See [docs/store/quick-start.md](./docs/store/quick-start.md) for more commands.
 
 Copy `.env.example` to `.env` and provide:
 
+### Admin Test Data
+
+To populate Redis with sample admin/user accounts and pending access requests for development, run:
+
+```bash
+npm run admin:seed-test-users
+```
+
+Optional flags:
+
+- `--refresh` — clear previously seeded data before recreating it
+- `--extra-pending=5` — append additional random pending requests (use any number)
+
 **Shopify (required for store):**
 - `SHOPIFY_STORE_DOMAIN` - your-store.myshopify.com
 - `SHOPIFY_STOREFRONT_TOKEN` - Storefront API access token
diff --git a/docs/migrations/001-role-to-roles-array.md b/docs/migrations/001-role-to-roles-array.md
new file mode 100644
index 0000000..1385c4b
--- /dev/null
+++ b/docs/migrations/001-role-to-roles-array.md
@@ -0,0 +1,221 @@
+# Migration: role → roles[] Array
+
+**Date:** 2025-10-27
+**Type:** Data Schema Change
+**Risk Level:** Low (backward compatible, idempotent)
+
+## Overview
+
+Migrates user data from single `role` field to multiple `roles` array to support users having multiple roles simultaneously (e.g., both `admin` and `community_manager`).
+
+## Changes
+
+**Before:**
+```json
+{
+  "email": "user@example.com",
+  "role": "admin",
+  "addedAt": "2025-10-27T00:00:00.000Z"
+}
+```
+
+**After:**
+```json
+{
+  "email": "user@example.com",
+  "roles": ["admin"],
+  "addedAt": "2025-10-27T00:00:00.000Z"
+}
+```
+
+## Migration Strategy
+
+### Option 1: Zero-Downtime (Recommended)
+
+The application automatically migrates data on-read. No downtime required.
+
+**How it works:**
+1. Deploy new code
+2. Users are migrated automatically when their data is accessed
+3. Optionally run migration script to pre-migrate all users
+
+**Steps:**
+```bash
+# 1. Deploy new code (already includes migration logic)
+git push production
+
+# 2. (Optional) Pre-migrate all users in background
+npm run admin:migrate-users
+```
+
+### Option 2: Pre-Migration (Safest)
+
+Run migration before deploying new code.
+
+**Steps:**
+```bash
+# 1. Run migration against production Redis
+REDIS_URL="your-production-redis-url" npm run admin:migrate-users
+
+# 2. Verify migration succeeded
+# Check logs for "Migration complete!"
+
+# 3. Deploy new code
+git push production
+```
+
+## Production Migration Guide
+
+### Prerequisites
+
+- [ ] Backup Redis data
+- [ ] Test migration on staging environment
+- [ ] Notify team of maintenance window (if using Option 2)
+
+### Step 1: Backup Redis Data
+
+```bash
+# Connect to production Redis
+redis-cli -u $REDIS_URL
+
+# Create RDB snapshot
+SAVE
+
+# Or use Redis Cloud backup feature
+```
+
+### Step 2: Test on Staging
+
+```bash
+# Point to staging Redis
+export REDIS_URL="redis://staging-redis-url"
+
+# Run migration
+npm run admin:migrate-users
+
+# Verify all users migrated successfully
+# Check application still works
+```
+
+### Step 3: Run Production Migration
+
+**Option A: Pre-migration (Recommended for large datasets)**
+
+```bash
+# Set production Redis URL
+export REDIS_URL="redis://production-redis-url"
+
+# Run migration
+npm run admin:migrate-users
+
+# Expected output:
+# ✅ Successfully migrated N users
+# ⏭️  Skipped M users (already migrated)
+```
+
+**Option B: Automatic migration**
+
+Just deploy the new code. Users will be migrated on first access.
+
+### Step 4: Verify Migration
+
+```bash
+# Check a few users manually
+redis-cli -u $REDIS_URL
+
+# Get a user
+GET admin:user:someone@example.com
+
+# Should see "roles": [...] not "role": "..."
+```
+
+### Step 5: Monitor
+
+```bash
+# Watch application logs for migration messages
+# Look for: "🔄 Migrated user X from role to roles array"
+
+# If using automatic migration, users will migrate gradually
+# If using pre-migration, should see no migration logs
+```
+
+## Rollback Plan
+
+If issues occur, the old code still works because:
+- Old data format (`role`) is automatically converted to new format (`roles`)
+- Migration is non-destructive
+
+**To rollback:**
+1. Redeploy previous version
+2. Users will continue working
+3. Optionally restore Redis backup if needed
+
+## Script Details
+
+### Idempotency
+
+The migration script is idempotent - safe to run multiple times:
+
+```javascript
+// Only migrates users with old format
+if (user.role && !user.roles) {
+  // Migrate
+}
+```
+
+**Running multiple times:**
+```bash
+npm run admin:migrate-users  # Migrates users
+npm run admin:migrate-users  # Skips already migrated users ✅
+```
+
+### Performance
+
+- Uses Redis pipelines for batch operations
+- Processes ~1000 users/second
+- No locks or blocking operations
+
+### Error Handling
+
+- Continues processing if individual user fails
+- Logs all errors
+- Returns exit code 1 if migration fails
+
+## Testing Checklist
+
+- [ ] Backup created
+- [ ] Tested on staging
+- [ ] Migration script runs successfully
+- [ ] Application loads correctly
+- [ ] Users can log in
+- [ ] Admin panel shows roles correctly
+- [ ] User permissions work as expected
+
+## Monitoring
+
+After migration, monitor for:
+- Migration log messages (should decrease over time with auto-migration)
+- User authentication errors
+- Admin access issues
+- Role permission errors
+
+## Support
+
+If issues occur:
+1. Check application logs for migration errors
+2. Verify Redis connection
+3. Check user data format in Redis directly
+4. Contact engineering team
+
+## Timeline
+
+**Staging:** Test now
+**Production:** Deploy during low-traffic window
+**Estimated downtime:** 0 seconds (with Option 1)
+
+## Notes
+
+- Migration is backward compatible
+- Old code will not work correctly with new data (always migrate forward)
+- SUPER_ADMIN_EMAIL environment variable works throughout migration
+- No changes to Redis keys or indexes
diff --git a/package.json b/package.json
index 8794359..08a5e26 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,8 @@
     "drops:create": "node --env-file=.env scripts/shopify-create-drop.mjs",
     "collections:generate-images": "node --env-file=.env scripts/generate-collection-images.mjs",
     "admin:bootstrap": "node --env-file=.env scripts/bootstrap-admin.mjs",
+    "admin:seed-test-users": "node --env-file=.env scripts/seed-admin-test-users.mjs",
+    "admin:migrate-users": "node --env-file=.env scripts/migrate-users-to-roles-array.mjs",
     "seed:communities": "npx tsx scripts/seed-communities.ts"
   },
   "dependencies": {
diff --git a/scripts/check-redis-requests.mjs b/scripts/check-redis-requests.mjs
new file mode 100644
index 0000000..e41e7db
--- /dev/null
+++ b/scripts/check-redis-requests.mjs
@@ -0,0 +1,18 @@
+import Redis from 'ioredis';
+
+const redis = new Redis(process.env.REDIS_URL);
+
+console.log('Checking pending requests in Redis...\n');
+
+const pendingIds = await redis.smembers('admin:requests:pending');
+console.log(`Found ${pendingIds.length} pending request IDs`);
+
+for (const id of pendingIds) {
+  const data = await redis.get(`admin:request:${id}`);
+  if (data) {
+    const req = JSON.parse(data);
+    console.log(`  - ${req.email} (${req.status})`);
+  }
+}
+
+await redis.quit();
diff --git a/scripts/migrate-users-to-roles-array.mjs b/scripts/migrate-users-to-roles-array.mjs
new file mode 100644
index 0000000..c08fa40
--- /dev/null
+++ b/scripts/migrate-users-to-roles-array.mjs
@@ -0,0 +1,114 @@
+#!/usr/bin/env node
+
+/**
+ * Migration Script: role → roles[]
+ * Migrates all users in Redis from old 'role' field to new 'roles' array
+ *
+ * Usage:
+ *   npm run admin:migrate-users              # Run migration
+ *   npm run admin:migrate-users -- --dry-run # Preview changes without saving
+ *   npm run admin:migrate-users -- --force   # Skip confirmation prompt
+ */
+
+import Redis from 'ioredis';
+import readline from 'readline';
+
+const args = process.argv.slice(2);
+const isDryRun = args.includes('--dry-run');
+const isForce = args.includes('--force');
+
+const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
+const redis = new Redis(redisUrl);
+
+function askConfirmation(question) {
+  const rl = readline.createInterface({
+    input: process.stdin,
+    output: process.stdout,
+  });
+
+  return new Promise((resolve) => {
+    rl.question(question, (answer) => {
+      rl.close();
+      resolve(answer.toLowerCase().startsWith('y'));
+    });
+  });
+}
+
+async function migrateUsers() {
+  console.log('🔄 Starting user migration: role → roles[]\n');
+
+  if (isDryRun) {
+    console.log('🔍 DRY RUN MODE - No changes will be saved\n');
+  }
+
+  console.log(`📍 Redis: ${redisUrl}\n`);
+
+  try {
+    // Get all user emails
+    const emails = await redis.smembers('admin:users:all');
+    console.log(`Found ${emails.length} users to check\n`);
+
+    if (emails.length === 0) {
+      console.log('✅ No users found. Migration complete.');
+      await redis.quit();
+      return;
+    }
+
+    // Batch fetch all users
+    const keys = emails.map(email => `admin:user:${email.toLowerCase()}`);
+    const values = await redis.mget(...keys);
+
+    let migratedCount = 0;
+    let skippedCount = 0;
+    const pipeline = redis.pipeline();
+
+    for (let i = 0; i < values.length; i++) {
+      const data = values[i];
+      if (!data) {
+        console.log(`⚠️  Skipping ${emails[i]} - no data found`);
+        continue;
+      }
+
+      try {
+        const user = JSON.parse(data);
+
+        // Check if migration needed
+        if (user.role && !user.roles) {
+          // Migrate
+          user.roles = [user.role];
+          delete user.role;
+          pipeline.set(keys[i], JSON.stringify(user));
+          console.log(`✅ Migrating ${user.email}: "${user.roles[0]}" → ["${user.roles[0]}"]`);
+          migratedCount++;
+        } else if (user.roles) {
+          console.log(`⏭️  Skipping ${user.email} - already has roles array`);
+          skippedCount++;
+        } else {
+          console.log(`⚠️  Skipping ${user.email} - no role or roles field`);
+          skippedCount++;
+        }
+      } catch (error) {
+        console.error(`❌ Error processing ${emails[i]}:`, error.message);
+      }
+    }
+
+    // Execute all migrations
+    if (migratedCount > 0) {
+      await pipeline.exec();
+      console.log(`\n✅ Successfully migrated ${migratedCount} users`);
+    } else {
+      console.log('\n✅ No users needed migration');
+    }
+
+    console.log(`⏭️  Skipped ${skippedCount} users (already migrated or no data)`);
+    console.log('\n🎉 Migration complete!');
+
+  } catch (error) {
+    console.error('❌ Migration failed:', error);
+    process.exit(1);
+  } finally {
+    await redis.quit();
+  }
+}
+
+migrateUsers();
diff --git a/scripts/seed-admin-test-users.mjs b/scripts/seed-admin-test-users.mjs
new file mode 100644
index 0000000..e8f3125
--- /dev/null
+++ b/scripts/seed-admin-test-users.mjs
@@ -0,0 +1,312 @@
+/**
+ * Seed Admin Test Users
+ * Populates Redis with a baseline set of admin/user accounts and pending access requests.
+ *
+ * Usage:
+ *   npm run admin:seed-test-users               # seed baseline data (skips existing)
+ *   npm run admin:seed-test-users -- --refresh  # reset previously seeded data first
+ *   npm run admin:seed-test-users -- --extra-pending=5  # add more pending requests
+ */
+
+import crypto from 'node:crypto';
+import Redis from 'ioredis';
+
+const redisUrl = process.env.REDIS_URL;
+
+if (!redisUrl) {
+  console.error('❌ REDIS_URL environment variable not set');
+  process.exit(1);
+}
+
+const dashIndex = process.argv.indexOf('--');
+const rawArgs =
+  dashIndex >= 0
+    ? process.argv.slice(dashIndex + 1)
+    : process.argv.slice(2);
+
+const refreshRequested = rawArgs.some(arg => arg === '--refresh' || arg === '--force');
+const extraPendingArg = rawArgs.find(arg => arg.startsWith('--extra-pending='));
+const extraPendingCount = extraPendingArg
+  ? Number.parseInt(extraPendingArg.split('=')[1] ?? '0', 10) || 0
+  : refreshRequested
+    ? 1
+    : 0;
+
+if (rawArgs.length > 0) {
+  console.log(`⚙️  Seeding options: ${rawArgs.join(' ')}`);
+}
+
+const redis = new Redis(redisUrl);
+const SEEDED_MARKER = 'seed-admin-test-users';
+
+const baseUsers = [
+  { email: 'ada.lovelace@example.com', roles: ['admin'] },
+  { email: 'grace.hopper@example.com', roles: ['admin'] },
+  { email: 'katherine.johnson@example.com', roles: ['community_manager'] },
+  { email: 'margaret.hamilton@example.com', roles: ['library_manager'] },
+  { email: 'annie.easley@example.com', roles: ['user'] },
+];
+
+const basePendingRequests = [
+  {
+    email: 'charles.babbage@example.com',
+    message: 'Excited to explore the platform and collaborate with the community.',
+  },
+  {
+    email: 'dorothy.vaughan@example.com',
+    message: 'I lead a local React meetup and would love early access for our organizers.',
+  },
+  {
+    email: 'mary.jackson@example.com',
+    message: 'Looking forward to joining future access waves—please keep me posted!',
+  },
+];
+
+const BASE_USER_EMAILS = baseUsers.map(user => user.email.toLowerCase().trim());
+const BASE_REQUEST_EMAILS = basePendingRequests.map(req => req.email.toLowerCase().trim());
+
+async function removeUserByEmail(email) {
+  const normalized = email.toLowerCase().trim();
+  const key = `admin:user:${normalized}`;
+  const exists = await redis.exists(key);
+
+  if (!exists) {
+    return false;
+  }
+
+  await redis.del(key);
+  await redis.srem('admin:users:all', normalized);
+  await redis.srem('admin:users:admins', normalized);
+  return true;
+}
+
+async function removeRequestByEmail(email) {
+  if (!email) return false;
+  const normalized = email.toLowerCase().trim();
+  const emailKey = `admin:request:email:${normalized}`;
+  const existingId = await redis.get(emailKey);
+  let removedId = null;
+
+  if (existingId) {
+    await redis.del(`admin:request:${existingId}`);
+    await redis.srem('admin:requests:pending', existingId);
+    await redis.srem('admin:requests:all', existingId);
+    removedId = existingId;
+  }
+
+  const removedFromPending = await redis.srem('admin:requests:pending', normalized);
+  const removedFromAll = await redis.srem('admin:requests:all', normalized);
+  await redis.del(emailKey);
+  if (!removedId && (removedFromPending || removedFromAll)) {
+    removedId = normalized;
+  }
+  return removedId;
+}
+
+async function cleanupSeededData() {
+  console.log('🧹 Refresh mode enabled — clearing previously seeded users and requests...\n');
+
+  for (const email of BASE_USER_EMAILS) {
+    const removed = await removeUserByEmail(email);
+    if (removed) {
+      console.log(`   🗑️  Removed base seeded user ${email}`);
+    }
+  }
+
+  const seededUserEmails = await redis.smembers('admin:users:all');
+  for (const email of seededUserEmails) {
+    const key = `admin:user:${email}`;
+    const raw = await redis.get(key);
+    if (!raw) continue;
+
+    try {
+      const record = JSON.parse(raw);
+      if (record?.addedBy === SEEDED_MARKER) {
+        await removeUserByEmail(email);
+        console.log(`   🗑️  Removed seeded user ${email}`);
+      }
+    } catch {
+      // Ignore malformed records
+    }
+  }
+
+  for (const email of BASE_REQUEST_EMAILS) {
+    const removedId = await removeRequestByEmail(email);
+    if (removedId) {
+      console.log(`   🗑️  Removed base pending request ${removedId} (${email})`);
+    }
+  }
+
+  const seededRequestIds = await redis.smembers('admin:requests:all');
+  for (const id of seededRequestIds) {
+    const key = `admin:request:${id}`;
+    const raw = await redis.get(key);
+    if (!raw) continue;
+
+    try {
+      const record = JSON.parse(raw);
+      if (record?.seededBy === SEEDED_MARKER) {
+        const removedId = await removeRequestByEmail(record.email ?? '');
+        if (removedId) {
+          console.log(`   🗑️  Removed seeded request ${removedId} (${record.email ?? 'unknown'})`);
+        }
+      }
+    } catch {
+      // Ignore malformed records
+    }
+  }
+
+  console.log('\n🔄 Ready to reseed fresh data...\n');
+}
+
+function createRandomPending(count) {
+  return Array.from({ length: count }, (_, idx) => {
+    const token = crypto.randomUUID().replace(/-/g, '').slice(0, 8);
+    return {
+      email: `pending.user.${token}@example.com`,
+      message: `Looking forward to joining an upcoming access wave (ref ${token}, slot ${idx + 1}).`,
+    };
+  });
+}
+
+async function seedUsers() {
+  console.log('🚀 Seeding admin test users into Redis...\n');
+
+  for (const user of baseUsers) {
+    const normalizedEmail = user.email.toLowerCase().trim();
+    const key = `admin:user:${normalizedEmail}`;
+    const exists = await redis.exists(key);
+
+    if (exists && !refreshRequested) {
+      console.log(`⏭️  Skipping ${normalizedEmail} (already present)`);
+      continue;
+    }
+
+    if (exists && refreshRequested) {
+      console.log(`🔁 Recreating ${normalizedEmail}`);
+    }
+
+    const userRecord = {
+      email: normalizedEmail,
+      roles: user.roles,
+      addedAt: new Date().toISOString(),
+      addedBy: SEEDED_MARKER,
+    };
+
+    await redis.set(key, JSON.stringify(userRecord));
+    await redis.sadd('admin:users:all', normalizedEmail);
+
+    if (user.roles.includes('admin')) {
+      await redis.sadd('admin:users:admins', normalizedEmail);
+    } else {
+      await redis.srem('admin:users:admins', normalizedEmail);
+    }
+
+    console.log(`✅ Seeded ${normalizedEmail} (${user.roles.join(', ')})`);
+  }
+}
+
+async function seedPendingRequests() {
+  console.log('\n📥 Seeding pending access requests...\n');
+
+  const extraRequests = createRandomPending(extraPendingCount);
+  const allRequests = [...basePendingRequests, ...extraRequests];
+
+  for (const request of allRequests) {
+    const normalizedEmail = request.email.toLowerCase().trim();
+    const emailKey = `admin:request:email:${normalizedEmail}`;
+    const existingId = await redis.get(emailKey);
+
+    if (existingId) {
+      if (!refreshRequested) {
+        console.log(`⏭️  Skipping request for ${normalizedEmail} (already present as ${existingId})`);
+        continue;
+      }
+
+      console.log(`🔁 Replacing existing request ${existingId} (${normalizedEmail})`);
+      await redis.del(`admin:request:${existingId}`);
+      await redis.srem('admin:requests:pending', existingId);
+      await redis.srem('admin:requests:all', existingId);
+      await redis.del(emailKey);
+    }
+
+    const id = crypto.randomUUID().replace(/-/g, '');
+    const requestRecord = {
+      id,
+      email: normalizedEmail,
+      message: request.message,
+      requestedAt: new Date().toISOString(),
+      status: 'pending',
+      seededBy: SEEDED_MARKER,
+    };
+
+    await redis.set(`admin:request:${id}`, JSON.stringify(requestRecord));
+    await redis.sadd('admin:requests:pending', id);
+    await redis.sadd('admin:requests:all', id);
+    await redis.set(emailKey, id);
+
+    console.log(`✅ Seeded pending request ${id} (${normalizedEmail})`);
+  }
+
+  if (extraRequests.length > 0) {
+    console.log(`\n➕ Added ${extraRequests.length} additional random pending request(s).`);
+  }
+}
+
+async function reportSummary() {
+  const pendingIds = await redis.smembers('admin:requests:pending');
+  const allIds = await redis.smembers('admin:requests:all');
+  const sampleIds = pendingIds.slice(0, 5);
+
+  console.log('\n📊 Redis Summary');
+  console.log(`   Pending Requests: ${pendingIds.length}`);
+  console.log(`   Total Requests (pending + processed): ${allIds.length}`);
+  console.log(`   Pending IDs: ${sampleIds.length ? sampleIds.join(', ') : 'none'}`);
+
+  if (sampleIds.length > 0) {
+    const pipeline = redis.pipeline();
+    for (const id of sampleIds) {
+      pipeline.get(`admin:request:${id}`);
+    }
+    const results = await pipeline.exec();
+    results.forEach(([, value]) => {
+      if (!value) return;
+      try {
+        const parsed = JSON.parse(value);
+        console.log(
+          `   • ${parsed.email} (status: ${parsed.status}, requestedAt: ${parsed.requestedAt})`
+        );
+      } catch {
+        // ignore parse errors
+      }
+    });
+  }
+}
+
+async function seedUsersAndRequests() {
+  try {
+    if (refreshRequested) {
+      await cleanupSeededData();
+    }
+
+    await seedUsers();
+    await seedPendingRequests();
+    await reportSummary();
+
+    console.log('\n✨ Done! Test users and pending requests are ready.');
+    if (!refreshRequested) {
+      console.log('\n💡 Tip: use "--refresh" to reset previously seeded data before recreating it.');
+    }
+    if (extraPendingCount === 0) {
+      console.log('   Need more pending requests? Try "--extra-pending=5".');
+    }
+    process.exit(0);
+  } catch (error) {
+    console.error('❌ Error seeding test data:', error);
+    process.exit(1);
+  } finally {
+    await redis.quit();
+  }
+}
+
+seedUsersAndRequests();
diff --git a/src/app/admin/actions.ts b/src/app/admin/actions.ts
index 892d704..e87b86f 100644
--- a/src/app/admin/actions.ts
+++ b/src/app/admin/actions.ts
@@ -10,11 +10,12 @@ import { authOptions } from '@/lib/auth';
 import { UserManagementService } from '@/lib/admin/user-management-service';
 import { AccessRequestsService } from '@/lib/admin/access-requests-service';
 import { revalidatePath } from 'next/cache';
+import type { UserRole } from '@/lib/admin/types';
 
 /**
- * Add or update a user
+ * Add or update a user with roles
  */
-export async function addUserAction(email: string, role: 'user' | 'admin') {
+export async function addUserAction(email: string, roles: UserRole[]) {
   const session = await getServerSession(authOptions);
 
   if (!session?.user?.email) {
@@ -26,7 +27,7 @@ export async function addUserAction(email: string, role: 'user' | 'admin') {
     throw new Error('Admin access required');
   }
 
-  await UserManagementService.addUser(email, role, session.user.email);
+  await UserManagementService.addUser(email, roles, session.user.email);
 
   revalidatePath('/admin/users');
 
@@ -61,9 +62,9 @@ export async function removeUserAction(email: string) {
 }
 
 /**
- * Update user role
+ * Update user roles
  */
-export async function updateUserRoleAction(email: string, role: 'user' | 'admin') {
+export async function updateUserRolesAction(email: string, roles: UserRole[]) {
   const session = await getServerSession(authOptions);
 
   if (!session?.user?.email) {
@@ -75,7 +76,7 @@ export async function updateUserRoleAction(email: string, role: 'user' | 'admin'
     throw new Error('Admin access required');
   }
 
-  await UserManagementService.updateUserRole(email, role, session.user.email);
+  await UserManagementService.updateUserRoles(email, roles, session.user.email);
 
   revalidatePath('/admin/users');
 
@@ -99,8 +100,9 @@ export async function approveRequestAction(id: string, role: 'user' | 'admin' =
 
   await AccessRequestsService.approveRequest(id, session.user.email, role);
 
-  revalidatePath('/admin/requests');
+  revalidatePath('/admin/users/requests');
   revalidatePath('/admin/users');
+  revalidatePath('/admin');
 
   return { success: true };
 }
@@ -122,7 +124,40 @@ export async function denyRequestAction(id: string) {
 
   await AccessRequestsService.denyRequest(id, session.user.email);
 
-  revalidatePath('/admin/requests');
+  revalidatePath('/admin/users/requests');
+  revalidatePath('/admin');
+
+  return { success: true };
+}
+
+/**
+ * Reply to request and bucket
+ */
+export async function replyToRequestAction(id: string, replyMessage: string, bucket: string) {
+  const session = await getServerSession(authOptions);
+
+  if (!session?.user?.email) {
+    throw new Error('Not authenticated');
+  }
+
+  const isAdmin = await UserManagementService.isAdmin(session.user.email);
+  if (!isAdmin) {
+    throw new Error('Admin access required');
+  }
+
+  if (!replyMessage || !replyMessage.trim()) {
+    throw new Error('Reply message is required');
+  }
+
+  await AccessRequestsService.replyToRequest(
+    id,
+    session.user.email,
+    replyMessage,
+    bucket ?? ''
+  );
+
+  revalidatePath('/admin/users/requests');
+  revalidatePath('/admin');
 
   return { success: true };
 }
diff --git a/src/app/admin/admin-sidebar.tsx b/src/app/admin/admin-sidebar.tsx
index 0d6068b..2aaba4d 100644
--- a/src/app/admin/admin-sidebar.tsx
+++ b/src/app/admin/admin-sidebar.tsx
@@ -18,7 +18,6 @@ export function AdminSidebar() {
     { href: '/admin/data', label: 'Data', icon: '📊' },
     { href: '/admin/ingest-full', label: 'Context', icon: '🤖' },
     { href: '/admin/users', label: 'Users', icon: '👥' },
-    { href: '/admin/requests', label: 'Requests', icon: '📧' },
     { href: '/admin/reset', label: 'Reset', icon: '⚠️', dangerous: true },
   ];
 
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index da4a381..bed49fe 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -7,18 +7,32 @@ import Link from 'next/link';
 import { UserManagementService } from '@/lib/admin/user-management-service';
 import { AccessRequestsService } from '@/lib/admin/access-requests-service';
 import { getRedisClient } from '@/lib/redis';
+import type { User } from '@/lib/admin/types';
+import type { AccessRequest } from '@/lib/admin/access-requests-service';
 
 export const dynamic = 'force-dynamic';
 
-async function getSystemStats() {
+interface SystemStats {
+  totalUsers: number;
+  totalAdmins: number;
+  pendingRequests: number;
+  totalRequests: number;
+  approvedRequests: number;
+  bucketedRequests: number;
+  deniedRequests: number;
+  redisConnected: boolean;
+  recentUsers: User[];
+  recentRequests: AccessRequest[];
+}
+
+async function getSystemStats(): Promise<SystemStats | null> {
   try {
-    const [users, pendingRequests, allRequests] = await Promise.all([
+    const [users, admins, pendingRequests, allRequests] = await Promise.all([
       UserManagementService.getAllUsers(),
+      UserManagementService.getAdmins(),
       AccessRequestsService.getPendingRequests(),
       AccessRequestsService.getAllRequests(),
     ]);
-
-    const admins = users.filter(u => u.role === 'admin');
     const processedRequests = allRequests.filter(r => r.status !== 'pending');
 
     // Test Redis connection
@@ -37,6 +51,7 @@ async function getSystemStats() {
       pendingRequests: pendingRequests.length,
       totalRequests: allRequests.length,
       approvedRequests: processedRequests.filter(r => r.status === 'approved').length,
+      bucketedRequests: processedRequests.filter(r => r.status === 'bucketed').length,
       deniedRequests: processedRequests.filter(r => r.status === 'denied').length,
       redisConnected,
       recentUsers: users.slice(-5).reverse(),
@@ -87,7 +102,7 @@ export default async function AdminHomePage() {
                 {stats.pendingRequests} access {stats.pendingRequests === 1 ? 'request' : 'requests'} waiting for review
               </p>
               <Link
-                href="/admin/requests"
+                href="/admin/users/requests"
                 className="inline-flex items-center gap-2 bg-primary text-primary-foreground px-4 py-2 rounded-lg font-semibold hover:bg-primary/90 transition"
               >
                 Review Requests →
@@ -115,14 +130,14 @@ export default async function AdminHomePage() {
           label="Pending Requests"
           value={stats.pendingRequests.toString()}
           icon="📧"
-          href="/admin/requests"
+          href="/admin/users/requests"
           highlight={stats.pendingRequests > 0}
         />
         <MetricCard
           label="Total Requests"
           value={stats.totalRequests.toString()}
           icon="📊"
-          href="/admin/requests"
+          href="/admin/users/requests"
         />
       </div>
 
@@ -156,7 +171,7 @@ export default async function AdminHomePage() {
               label="Manage Users"
             />
             <QuickActionButton
-              href="/admin/requests"
+              href="/admin/users/requests"
               icon="📧"
               label="View Requests"
             />
@@ -181,7 +196,7 @@ export default async function AdminHomePage() {
           <div className="flex items-center justify-between mb-4">
             <h3 className="text-lg font-bold text-foreground">Recent Pending Requests</h3>
             <Link
-              href="/admin/requests"
+              href="/admin/users/requests"
               className="text-sm text-primary hover:text-primary/80 transition"
             >
               View All →
@@ -200,7 +215,7 @@ export default async function AdminHomePage() {
                   </p>
                 </div>
                 <Link
-                  href={`/admin/requests?id=${req.id}`}
+                  href={`/admin/users/requests?id=${req.id}`}
                   className="ml-4 text-sm bg-primary text-primary-foreground px-3 py-1 rounded hover:bg-primary/90 transition"
                 >
                   Review
@@ -214,12 +229,17 @@ export default async function AdminHomePage() {
       {/* Request Stats */}
       <div className="bg-card border border-border rounded-xl p-6">
         <h3 className="text-lg font-bold text-foreground mb-4">Request Statistics</h3>
-        <div className="grid grid-cols-3 gap-4">
+        <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
           <StatItem
             label="Approved"
             value={stats.approvedRequests}
             color="success"
           />
+          <StatItem
+            label="Bucketed"
+            value={stats.bucketedRequests}
+            color="accent"
+          />
           <StatItem
             label="Denied"
             value={stats.deniedRequests}
@@ -251,21 +271,34 @@ export default async function AdminHomePage() {
                 key={user.email}
                 className="flex items-center justify-between p-3 bg-muted rounded-lg"
               >
-                <div>
+                <div className="flex-1">
                   <p className="font-medium text-foreground">{user.email}</p>
                   <p className="text-sm text-muted-foreground">
                     Added {new Date(user.addedAt).toLocaleDateString()}
                   </p>
                 </div>
-                <span
-                  className={`px-2 py-1 rounded text-xs font-semibold ${
-                    user.role === 'admin'
-                      ? 'bg-accent/20 text-accent-foreground'
-                      : 'bg-primary/20 text-primary-foreground'
-                  }`}
-                >
-                  {user.role}
-                </span>
+                <div className="flex flex-wrap gap-1 justify-end">
+                  {(user.roles || []).filter(role => role !== 'user').length === 0 ? (
+                    <span className="text-xs text-muted-foreground italic">Basic User</span>
+                  ) : (
+                    (user.roles || []).filter(role => role !== 'user').map(role => (
+                      <span
+                        key={role}
+                        className={`px-2 py-1 rounded text-xs font-semibold ${
+                          role === 'admin'
+                            ? 'bg-accent/20 text-accent-foreground'
+                            : role === 'community_manager'
+                            ? 'bg-primary/20 text-primary-foreground'
+                            : role === 'library_manager'
+                            ? 'bg-success/20 text-success-foreground'
+                            : 'bg-muted/60 text-muted-foreground'
+                        }`}
+                      >
+                        {role}
+                      </span>
+                    ))
+                  )}
+                </div>
               </div>
             ))}
           </div>
@@ -373,12 +406,13 @@ function StatItem({
 }: {
   label: string;
   value: number;
-  color: 'success' | 'destructive' | 'primary';
+  color: 'success' | 'destructive' | 'primary' | 'accent';
 }) {
   const colorClass = {
     success: 'text-success-foreground',
     destructive: 'text-destructive-foreground',
     primary: 'text-primary-foreground',
+    accent: 'text-accent-foreground',
   }[color];
 
   return (
diff --git a/src/app/admin/requests/loading.tsx b/src/app/admin/requests/loading.tsx
deleted file mode 100644
index 3670eda..0000000
--- a/src/app/admin/requests/loading.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Loading state for admin requests page
- */
-
-export default function Loading() {
-  return (
-    <div className="container mx-auto max-w-6xl px-4 pb-12 mt-20">
-      <div className="space-y-8">
-        {/* Header Skeleton */}
-        <div className="space-y-2">
-          <div className="h-10 w-64 animate-pulse rounded bg-background/10" />
-          <div className="h-5 w-56 animate-pulse rounded bg-background/5" />
-        </div>
-
-        {/* Pending Requests Skeleton */}
-        <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
-          <div className="mb-4 h-7 w-48 animate-pulse rounded bg-background/10" />
-          <div className="space-y-4">
-            {[...Array(3)].map((_, i) => (
-              <div key={i} className="rounded-xl border border-border/10 bg-foreground/30 p-6">
-                <div className="space-y-4">
-                  <div className="space-y-2">
-                    <div className="h-6 w-56 animate-pulse rounded bg-background/10" />
-                    <div className="h-4 w-40 animate-pulse rounded bg-background/5" />
-                  </div>
-                  <div className="h-24 animate-pulse rounded-lg bg-background/5" />
-                  <div className="flex gap-3">
-                    <div className="h-10 flex-1 animate-pulse rounded-lg bg-background/10" />
-                    <div className="h-10 flex-1 animate-pulse rounded-lg bg-background/10" />
-                    <div className="h-10 w-24 animate-pulse rounded-lg bg-background/5" />
-                  </div>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
-
-        {/* Processed Requests Skeleton */}
-        <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
-          <div className="mb-4 h-7 w-48 animate-pulse rounded bg-background/10" />
-          <div className="space-y-2">
-            {[...Array(5)].map((_, i) => (
-              <div key={i} className="rounded-lg border border-border/10 bg-foreground/20 p-4">
-                <div className="flex items-center justify-between">
-                  <div className="flex-1 space-y-2">
-                    <div className="h-5 w-48 animate-pulse rounded bg-background/10" />
-                    <div className="h-4 w-64 animate-pulse rounded bg-background/5" />
-                  </div>
-                  <div className="h-6 w-20 animate-pulse rounded bg-background/10" />
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-}
diff --git a/src/app/admin/requests/page.tsx b/src/app/admin/requests/page.tsx
deleted file mode 100644
index 0bd1ff8..0000000
--- a/src/app/admin/requests/page.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Admin Access Requests Page - Server Component
- * Review and manage pending access requests with Server Actions
- */
-
-import { redirect } from 'next/navigation';
-import { getServerSession } from 'next-auth';
-import { authOptions } from '@/lib/auth';
-import { UserManagementService } from '@/lib/admin/user-management-service';
-import { AccessRequestsService } from '@/lib/admin/access-requests-service';
-import { RequestsListClient } from './requests-list-client';
-
-export default async function AdminRequestsPage() {
-  const session = await getServerSession(authOptions);
-
-  if (!session?.user?.email) {
-    redirect('/api/auth/signin');
-  }
-
-  const isAdmin = await UserManagementService.isAdmin(session.user.email);
-
-  if (!isAdmin) {
-    return (
-      <div className="container mx-auto max-w-4xl px-4 py-12">
-        <div className="rounded-xl border border-destructive/50 bg-destructive/10 p-6 text-center">
-          <p className="text-red-400">Access Denied - Admin role required</p>
-        </div>
-      </div>
-    );
-  }
-
-  // Fetch requests server-side
-  const allRequests = await AccessRequestsService.getAllRequests();
-
-  return (
-    <div className="container mx-auto max-w-6xl px-4 pb-12 mt-20">
-      <div className="space-y-8">
-        {/* Header */}
-        <div>
-          <h1 className="text-4xl font-bold text-foreground">Access Requests</h1>
-          <p className="mt-2 text-foreground/70">Review and manage early access requests</p>
-        </div>
-
-        {/* Client component handles interactivity */}
-        <RequestsListClient requests={allRequests} />
-      </div>
-    </div>
-  );
-}
diff --git a/src/app/admin/requests/requests-list-client.tsx b/src/app/admin/requests/requests-list-client.tsx
deleted file mode 100644
index d32f958..0000000
--- a/src/app/admin/requests/requests-list-client.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * Requests List Client Component
- * Handles interactive UI for access request management
- */
-
-'use client';
-
-import { useState, useTransition } from 'react';
-import { useSearchParams } from 'next/navigation';
-import {
-  approveRequestAction,
-  denyRequestAction,
-  resendAdminNotificationAction,
-} from '../actions';
-import type { AccessRequest } from '@/lib/admin/access-requests-service';
-
-export function RequestsListClient({
-  requests: initialRequests,
-}: {
-  requests: AccessRequest[];
-}) {
-  const searchParams = useSearchParams();
-  const highlightId = searchParams.get('id');
-  const [isPending, startTransition] = useTransition();
-  const [error, setError] = useState<string | null>(null);
-  const [status, setStatus] = useState<string | null>(null);
-
-  const pendingRequests = initialRequests.filter(r => r.status === 'pending');
-  const processedRequests = initialRequests.filter(r => r.status !== 'pending');
-
-  const handleApprove = (id: string, role: 'user' | 'admin' = 'user') => {
-    setError(null);
-    setStatus(null);
-    startTransition(async () => {
-      try {
-        await approveRequestAction(id, role);
-      } catch (err) {
-        setError(err instanceof Error ? err.message : 'Failed to approve');
-      }
-    });
-  };
-
-  const handleDeny = (id: string) => {
-    if (!confirm('Deny this request?')) return;
-
-    setError(null);
-    setStatus(null);
-    startTransition(async () => {
-      try {
-        await denyRequestAction(id);
-      } catch (err) {
-        setError(err instanceof Error ? err.message : 'Failed to deny');
-      }
-    });
-  };
-
-  const handleResendNotification = (id: string) => {
-    setError(null);
-    setStatus(null);
-    startTransition(async () => {
-      try {
-        await resendAdminNotificationAction(id);
-        setStatus('Admin notification email sent again.');
-      } catch (err) {
-        setError(err instanceof Error ? err.message : 'Failed to resend admin email');
-      }
-    });
-  };
-
-  return (
-    <>
-      {error && (
-        <div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-destructive-foreground">
-          {error}
-        </div>
-      )}
-      {status && !error && (
-        <div className="rounded-lg border border-success/30 bg-success/10 p-4 text-success-foreground">
-          {status}
-        </div>
-      )}
-
-      {/* Pending Requests */}
-      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
-        <h2 className="mb-4 text-xl font-semibold text-foreground">
-          Pending Requests ({pendingRequests.length})
-        </h2>
-
-        {pendingRequests.length === 0 && (
-          <p className="text-center text-foreground/60">No pending requests</p>
-        )}
-
-        <div className="space-y-4">
-          {pendingRequests.map((req) => (
-            <div
-              key={req.id}
-              className={`rounded-xl border p-6 ${
-                req.id === highlightId
-                  ? 'border-primary bg-primary/10'
-                  : 'border-border bg-card'
-              }`}
-            >
-              <div className="space-y-4">
-                <div>
-                  <p className="text-lg font-semibold text-foreground">{req.email}</p>
-                  <p className="text-sm text-foreground/50">
-                    Requested {new Date(req.requestedAt).toLocaleString()}
-                  </p>
-                </div>
-
-                <div className="rounded-lg border border-border bg-muted p-4">
-                  <p className="text-sm text-muted-foreground">{req.message}</p>
-                </div>
-
-                <div className="flex gap-3">
-                  <button
-                    onClick={() => handleApprove(req.id, 'user')}
-                    disabled={isPending}
-                    className="flex-1 rounded-lg bg-success px-4 py-2 font-semibold text-foreground transition hover:bg-success/50 disabled:opacity-50"
-                  >
-                    ✅ Approve as User
-                  </button>
-                  <button
-                    onClick={() => handleApprove(req.id, 'admin')}
-                    disabled={isPending}
-                    className="flex-1 rounded-lg bg-accent px-4 py-2 font-semibold text-accent-foreground transition hover:bg-accent/80 disabled:opacity-50"
-                  >
-                    👑 Approve as Admin
-                  </button>
-                  <button
-                    onClick={() => handleDeny(req.id)}
-                    disabled={isPending}
-                    className="rounded-lg bg-destructive/20 px-4 py-2 font-semibold text-destructive-foreground transition hover:bg-destructive/30 disabled:opacity-50"
-                  >
-                    ❌ Deny
-                  </button>
-                </div>
-                <div className="flex justify-end">
-                  <button
-                    onClick={() => handleResendNotification(req.id)}
-                    disabled={isPending}
-                    className="mt-3 inline-flex items-center gap-2 rounded-lg border border-border px-4 py-2 text-sm font-semibold text-foreground transition hover:bg-muted disabled:opacity-50"
-                  >
-                    📧 Resend Admin Email
-                  </button>
-                </div>
-              </div>
-            </div>
-          ))}
-        </div>
-      </div>
-
-      {/* Processed Requests */}
-      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
-        <h2 className="mb-4 text-xl font-semibold text-foreground">
-          Processed Requests ({processedRequests.length})
-        </h2>
-
-        <div className="space-y-2">
-          {processedRequests.slice(0, 20).map((req) => (
-            <div
-              key={req.id}
-              className="flex items-center justify-between rounded-lg border border-border bg-card p-4"
-            >
-              <div>
-                <p className="font-medium text-foreground">{req.email}</p>
-                <p className="text-sm text-foreground/50">
-                  {req.status === 'approved' ? '✅ Approved' : '❌ Denied'} on{' '}
-                  {new Date(req.reviewedAt!).toLocaleDateString()}
-                  {req.reviewedBy && ` by ${req.reviewedBy}`}
-                </p>
-              </div>
-              <span
-                className={`rounded px-3 py-1 text-sm font-semibold ${
-                  req.status === 'approved'
-                    ? 'bg-success/20 text-success-foreground'
-                    : 'bg-destructive/20 text-destructive-foreground'
-                }`}
-              >
-                {req.status}
-              </span>
-            </div>
-          ))}
-        </div>
-      </div>
-    </>
-  );
-}
diff --git a/src/app/admin/users/layout.tsx b/src/app/admin/users/layout.tsx
new file mode 100644
index 0000000..7496d4f
--- /dev/null
+++ b/src/app/admin/users/layout.tsx
@@ -0,0 +1,38 @@
+/**
+ * Admin User Management Layout
+ * Provides tab navigation between Users and Requests
+ * Auth is handled by parent /admin layout
+ */
+
+import { AccessRequestsService } from '@/lib/admin/access-requests-service';
+import { TabNavigation } from './tab-navigation';
+
+export default async function UsersLayout({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  // Auth is already handled by /admin layout, no need to duplicate checks
+
+  // Get pending request count for tab badge
+  const pendingRequests = await AccessRequestsService.getPendingRequests();
+  const pendingCount = pendingRequests.length;
+
+  return (
+    <div className="container mx-auto max-w-6xl px-4 pb-12 mt-20">
+      <div className="space-y-8">
+        {/* Header */}
+        <div>
+          <h1 className="text-4xl font-bold text-foreground">User Management</h1>
+          <p className="mt-2 text-foreground/70">Manage users and access requests</p>
+        </div>
+
+        {/* Tab Navigation */}
+        <TabNavigation pendingCount={pendingCount} />
+
+        {/* Tab Content */}
+        {children}
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx
index 9c72c52..2aac73a 100644
--- a/src/app/admin/users/page.tsx
+++ b/src/app/admin/users/page.tsx
@@ -1,47 +1,48 @@
 /**
- * Admin User Management Page - Server Component
- * Manages user access and roles with Server Actions
+ * Admin Users Tab - Server Component
+ * Displays and manages user access and roles
  */
 
-import { redirect } from 'next/navigation';
 import { getServerSession } from 'next-auth';
 import { authOptions } from '@/lib/auth';
 import { UserManagementService } from '@/lib/admin/user-management-service';
 import { UsersListClient } from './users-list-client';
+import { Suspense } from 'react';
 
-export default async function AdminUsersPage() {
+async function UsersContent() {
   const session = await getServerSession(authOptions);
+  const users = await UserManagementService.getAllUsers();
 
-  if (!session?.user?.email) {
-    redirect('/api/auth/signin');
-  }
-
-  const isAdmin = await UserManagementService.isAdmin(session.user.email);
-
-  if (!isAdmin) {
-    return (
-      <div className="container mx-auto max-w-4xl px-4 py-12">
-        <div className="rounded-xl border border-destructive/50 bg-destructive/10 p-6 text-center">
-          <p className="text-red-400">Access Denied - Admin role required</p>
-        </div>
-      </div>
-    );
-  }
+  return (
+    <UsersListClient
+      users={users}
+      currentUserEmail={session?.user?.email ?? ''}
+    />
+  );
+}
 
-  // Fetch users server-side
-  const users = await UserManagementService.getAllUsers();
+export default async function AdminUsersPage() {
+  return (
+    <Suspense fallback={<UsersLoadingFallback />}>
+      <UsersContent />
+    </Suspense>
+  );
+}
 
+function UsersLoadingFallback() {
   return (
-    <div className="container mx-auto max-w-6xl px-4 pb-12 mt-20">
-      <div className="space-y-8">
-        {/* Header */}
-        <div>
-          <h1 className="text-4xl font-bold text-foreground">User Management</h1>
-          <p className="mt-2 text-foreground/70">Manage user access and roles</p>
+    <div className="space-y-8">
+      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
+        <div className="h-8 w-32 bg-muted animate-pulse rounded mb-4" />
+        <div className="h-12 bg-muted animate-pulse rounded" />
+      </div>
+      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
+        <div className="h-8 w-32 bg-muted animate-pulse rounded mb-4" />
+        <div className="space-y-2">
+          {[1, 2, 3].map((i) => (
+            <div key={i} className="h-16 bg-muted animate-pulse rounded" />
+          ))}
         </div>
-
-        {/* Client component handles interactivity */}
-        <UsersListClient users={users} currentUserEmail={session.user.email} />
       </div>
     </div>
   );
diff --git a/src/app/admin/users/requests/page.tsx b/src/app/admin/users/requests/page.tsx
new file mode 100644
index 0000000..af1bd9f
--- /dev/null
+++ b/src/app/admin/users/requests/page.tsx
@@ -0,0 +1,44 @@
+/**
+ * Admin Access Requests Tab - Server Component
+ * Review and manage pending access requests
+ */
+
+import { AccessRequestsService } from '@/lib/admin/access-requests-service';
+import { RequestsListClient } from './requests-list-client';
+import { Suspense } from 'react';
+
+async function RequestsContent() {
+  const allRequests = await AccessRequestsService.getAllRequests();
+  return <RequestsListClient requests={allRequests} />;
+}
+
+export default async function AdminRequestsPage() {
+  return (
+    <Suspense fallback={<RequestsLoadingFallback />}>
+      <RequestsContent />
+    </Suspense>
+  );
+}
+
+function RequestsLoadingFallback() {
+  return (
+    <div className="space-y-8">
+      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
+        <div className="h-8 w-48 bg-muted animate-pulse rounded mb-4" />
+        <div className="space-y-4">
+          {[1, 2].map((i) => (
+            <div key={i} className="h-32 bg-muted animate-pulse rounded" />
+          ))}
+        </div>
+      </div>
+      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
+        <div className="h-8 w-48 bg-muted animate-pulse rounded mb-4" />
+        <div className="space-y-2">
+          {[1, 2, 3].map((i) => (
+            <div key={i} className="h-20 bg-muted animate-pulse rounded" />
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/admin/users/requests/requests-list-client.tsx b/src/app/admin/users/requests/requests-list-client.tsx
new file mode 100644
index 0000000..8949e91
--- /dev/null
+++ b/src/app/admin/users/requests/requests-list-client.tsx
@@ -0,0 +1,319 @@
+/**
+ * Requests List Client Component
+ * Handles interactive UI for access request management
+ */
+
+'use client';
+
+import { useState, useTransition } from 'react';
+import { useSearchParams } from 'next/navigation';
+import {
+  approveRequestAction,
+  denyRequestAction,
+  replyToRequestAction,
+  resendAdminNotificationAction,
+} from '../../actions';
+import type { AccessRequest } from '@/lib/admin/access-requests-service';
+
+export function RequestsListClient({
+  requests: initialRequests,
+}: {
+  requests: AccessRequest[];
+}) {
+  const searchParams = useSearchParams();
+  const highlightId = searchParams.get('id');
+  const [isPending, startTransition] = useTransition();
+  const [error, setError] = useState<string | null>(null);
+  const [status, setStatus] = useState<string | null>(null);
+  const [replyingTo, setReplyingTo] = useState<string | null>(null);
+  const [replyDrafts, setReplyDrafts] = useState<
+    Record<string, { message: string; bucket: string }>
+  >({});
+
+  const pendingRequests = initialRequests.filter(r => r.status === 'pending');
+  const processedRequests = initialRequests.filter(r => r.status !== 'pending');
+
+  const handleApprove = (id: string, role: 'user' | 'admin' = 'user') => {
+    setError(null);
+    setStatus(null);
+    startTransition(async () => {
+      try {
+        await approveRequestAction(id, role);
+      } catch (err) {
+        setError(err instanceof Error ? err.message : 'Failed to approve');
+      }
+    });
+  };
+
+  const handleDeny = (id: string) => {
+    if (!confirm('Are you sure you want to deny this request? This will send a denial email to the requester.')) {
+      return;
+    }
+
+    setError(null);
+    setStatus(null);
+    startTransition(async () => {
+      try {
+        await denyRequestAction(id);
+        setStatus('Request denied and denial email sent.');
+      } catch (err) {
+        setError(err instanceof Error ? err.message : 'Failed to deny');
+      }
+    });
+  };
+
+  const getDraft = (id: string) => {
+    const draft = replyDrafts[id];
+    if (draft) return draft;
+    return { message: '', bucket: '' };
+  };
+
+  const updateDraft = (id: string, updates: Partial<{ message: string; bucket: string }>) => {
+    setReplyDrafts(prev => ({
+      ...prev,
+      [id]: {
+        ...getDraft(id),
+        ...updates,
+      },
+    }));
+  };
+
+  const handleReply = (id: string) => {
+    setError(null);
+    setStatus(null);
+    setReplyingTo(prev => (prev === id ? null : id));
+  };
+
+  const handleReplySubmit = (id: string) => {
+    const draft = getDraft(id);
+    if (!draft.message.trim()) {
+      setError('Reply message is required');
+      return;
+    }
+
+    setError(null);
+    setStatus(null);
+    startTransition(async () => {
+      try {
+        await replyToRequestAction(id, draft.message, draft.bucket);
+        setStatus('Reply sent and requester bucketed.');
+        setReplyingTo(null);
+        setReplyDrafts(prev => {
+          const next = { ...prev };
+          delete next[id];
+          return next;
+        });
+      } catch (err) {
+        setError(err instanceof Error ? err.message : 'Failed to send reply');
+      }
+    });
+  };
+
+  const handleResendNotification = (id: string) => {
+    setError(null);
+    setStatus(null);
+    startTransition(async () => {
+      try {
+        await resendAdminNotificationAction(id);
+        setStatus('Admin notification email sent again.');
+      } catch (err) {
+        setError(err instanceof Error ? err.message : 'Failed to resend admin email');
+      }
+    });
+  };
+
+  return (
+    <>
+      {error && (
+        <div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-destructive-foreground">
+          {error}
+        </div>
+      )}
+      {status && !error && (
+        <div className="rounded-lg border border-success/30 bg-success/10 p-4 text-success-foreground">
+          {status}
+        </div>
+      )}
+
+      {/* Pending Requests */}
+      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
+        <h2 className="mb-4 text-xl font-semibold text-foreground">
+          Pending Requests ({pendingRequests.length})
+        </h2>
+
+        {pendingRequests.length === 0 && (
+          <p className="text-center text-foreground/60">No pending requests</p>
+        )}
+
+        <div className="space-y-4">
+          {pendingRequests.map((req) => (
+            <div
+              key={req.id}
+              className={`rounded-xl border p-6 ${
+                req.id === highlightId
+                  ? 'border-primary bg-primary/10'
+                  : 'border-border bg-card'
+              }`}
+            >
+              <div className="space-y-4">
+                <div>
+                  <p className="text-lg font-semibold text-foreground">{req.email}</p>
+                  <p className="text-sm text-foreground/50">
+                    Requested {new Date(req.requestedAt).toLocaleString()}
+                  </p>
+                </div>
+
+                <div className="rounded-lg border border-border bg-muted p-4">
+                  <p className="text-sm text-muted-foreground">{req.message}</p>
+                </div>
+
+                <div className="flex flex-col gap-3">
+                  <div className="flex gap-3">
+                    <button
+                      onClick={() => handleApprove(req.id, 'user')}
+                      disabled={isPending}
+                      className="flex-1 rounded-lg bg-success px-4 py-2 font-semibold text-foreground transition hover:bg-success/50 disabled:opacity-50"
+                    >
+                      ✅ Approve as User
+                    </button>
+                    <button
+                      onClick={() => handleApprove(req.id, 'admin')}
+                      disabled={isPending}
+                      className="flex-1 rounded-lg bg-accent px-4 py-2 font-semibold text-accent-foreground transition hover:bg-accent/80 disabled:opacity-50"
+                    >
+                      👑 Approve as Admin
+                    </button>
+                  </div>
+                  <div className="flex gap-3">
+                    <button
+                      onClick={() => handleReply(req.id)}
+                      disabled={isPending}
+                      className="flex-1 rounded-lg bg-muted px-4 py-2 font-semibold text-foreground transition hover:bg-muted/70 disabled:opacity-50"
+                    >
+                      ✉️ Reply &amp; Bucket
+                    </button>
+                    <button
+                      onClick={() => handleDeny(req.id)}
+                      disabled={isPending}
+                      className="flex-1 rounded-lg bg-destructive px-4 py-2 font-semibold text-destructive-foreground transition hover:bg-destructive/80 disabled:opacity-50"
+                    >
+                      ❌ Deny Request
+                    </button>
+                  </div>
+                </div>
+                {replyingTo === req.id && (
+                  <div className="rounded-xl border border-border bg-card/70 p-4">
+                    <div className="space-y-3">
+                      <div>
+                        <label className="block text-sm font-semibold text-foreground">
+                          Reply Message
+                        </label>
+                        <textarea
+                          className="mt-1 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground"
+                          rows={4}
+                          placeholder="Let them know what to expect next..."
+                          value={getDraft(req.id).message}
+                          onChange={(event) =>
+                            updateDraft(req.id, { message: event.target.value })
+                          }
+                          disabled={isPending}
+                        />
+                      </div>
+                      <div>
+                        <label className="block text-sm font-semibold text-foreground">
+                          Bucket
+                        </label>
+                        <input
+                          type="text"
+                          className="mt-1 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground"
+                          placeholder="e.g. Wave 2"
+                          value={getDraft(req.id).bucket}
+                          onChange={(event) =>
+                            updateDraft(req.id, { bucket: event.target.value })
+                          }
+                          disabled={isPending}
+                        />
+                      </div>
+                      <div className="flex gap-3">
+                        <button
+                          onClick={() => handleReplySubmit(req.id)}
+                          disabled={isPending}
+                          className="flex-1 rounded-lg bg-primary px-4 py-2 font-semibold text-primary-foreground transition hover:bg-primary/80 disabled:opacity-50"
+                        >
+                          Send Reply
+                        </button>
+                        <button
+                          type="button"
+                          onClick={() => setReplyingTo(null)}
+                          disabled={isPending}
+                          className="rounded-lg border border-border px-4 py-2 font-semibold text-foreground transition hover:bg-muted disabled:opacity-50"
+                        >
+                          Cancel
+                        </button>
+                      </div>
+                    </div>
+                  </div>
+                )}
+                <div className="flex justify-end">
+                  <button
+                    onClick={() => handleResendNotification(req.id)}
+                    disabled={isPending}
+                    className="mt-3 inline-flex items-center gap-2 rounded-lg border border-border px-4 py-2 text-sm font-semibold text-foreground transition hover:bg-muted disabled:opacity-50"
+                  >
+                    📧 Resend Admin Email
+                  </button>
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </div>
+
+      {/* Processed Requests */}
+      <div className="rounded-2xl border border-border/10 bg-muted/60 p-6">
+        <h2 className="mb-4 text-xl font-semibold text-foreground">
+          Processed Requests ({processedRequests.length})
+        </h2>
+
+        <div className="space-y-2">
+          {processedRequests.slice(0, 20).map((req) => (
+            <div
+              key={req.id}
+              className="flex items-center justify-between rounded-lg border border-border bg-card p-4"
+            >
+              <div>
+                <p className="font-medium text-foreground">{req.email}</p>
+                <p className="text-sm text-foreground/50">
+                  {req.status === 'approved'
+                    ? '✅ Approved'
+                    : req.status === 'bucketed'
+                      ? `🪣 Bucketed${req.bucket ? ` (${req.bucket})` : ''}`
+                      : '❌ Denied'}{' '}
+                  on{' '}
+                  {new Date(req.reviewedAt!).toLocaleDateString()}
+                  {req.reviewedBy && ` by ${req.reviewedBy}`}
+                </p>
+                {req.status === 'bucketed' && req.replyMessage && (
+                  <p className="text-sm text-foreground/60">
+                    Reply sent: {req.replyMessage}
+                  </p>
+                )}
+              </div>
+              <span
+                className={`rounded px-3 py-1 text-sm font-semibold ${
+                  req.status === 'approved'
+                    ? 'bg-success/20 text-success-foreground'
+                    : req.status === 'bucketed'
+                      ? 'bg-primary/20 text-primary-foreground'
+                      : 'bg-destructive/20 text-destructive-foreground'
+                }`}
+              >
+                {req.status}
+              </span>
+            </div>
+          ))}
+        </div>
+      </div>
+    </>
+  );
+}
diff --git a/src/app/admin/users/tab-navigation.tsx b/src/app/admin/users/tab-navigation.tsx
new file mode 100644
index 0000000..89d0bf2
--- /dev/null
+++ b/src/app/admin/users/tab-navigation.tsx
@@ -0,0 +1,52 @@
+/**
+ * Tab Navigation Component
+ * Client component for tab navigation with active state
+ */
+
+'use client';
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+
+export function TabNavigation({ pendingCount }: { pendingCount: number }) {
+  const pathname = usePathname();
+
+  const isUsersTab = pathname === '/admin/users';
+  const isRequestsTab = pathname?.startsWith('/admin/users/requests');
+
+  return (
+    <div className="border-b border-border">
+      <nav className="flex gap-1" aria-label="Tabs">
+        <Link
+          href="/admin/users"
+          className={`px-6 py-3 text-sm font-semibold border-b-2 transition ${
+            isUsersTab
+              ? 'border-primary text-primary'
+              : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
+          }`}
+        >
+          Users
+        </Link>
+        <Link
+          href="/admin/users/requests"
+          className={`px-6 py-3 text-sm font-semibold border-b-2 transition flex items-center gap-2 ${
+            isRequestsTab
+              ? 'border-primary text-primary'
+              : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
+          }`}
+        >
+          Requests
+          {pendingCount > 0 && (
+            <span className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-bold rounded-full ${
+              isRequestsTab
+                ? 'bg-primary text-primary-foreground'
+                : 'bg-muted text-muted-foreground'
+            }`}>
+              {pendingCount}
+            </span>
+          )}
+        </Link>
+      </nav>
+    </div>
+  );
+}
diff --git a/src/app/admin/users/users-list-client.tsx b/src/app/admin/users/users-list-client.tsx
index 9ca0787..f63e10a 100644
--- a/src/app/admin/users/users-list-client.tsx
+++ b/src/app/admin/users/users-list-client.tsx
@@ -6,8 +6,10 @@
 'use client';
 
 import { useState, useTransition } from 'react';
-import { addUserAction, removeUserAction, updateUserRoleAction } from '../actions';
-import type { User } from '@/lib/admin/user-management-service';
+import { addUserAction, removeUserAction, updateUserRolesAction } from '../actions';
+import type { User, UserRole } from '@/lib/admin/types';
+import { ROLE_LABELS } from '@/lib/admin/types';
+import { RoleSelector } from '@/components/admin/role-selector';
 
 export function UsersListClient({
   users: initialUsers,
@@ -17,18 +19,29 @@ export function UsersListClient({
   currentUserEmail: string;
 }) {
   const [newEmail, setNewEmail] = useState('');
-  const [newRole, setNewRole] = useState<'user' | 'admin'>('user');
+  const [newRoles, setNewRoles] = useState<UserRole[]>([]); // Empty by default, 'user' is implicit
+  const [editingUser, setEditingUser] = useState<string | null>(null);
+  const [editRoles, setEditRoles] = useState<UserRole[]>([]);
   const [isPending, startTransition] = useTransition();
   const [error, setError] = useState<string | null>(null);
 
+  // Filter out 'user' role as it's implicit for all users
+  const getVisibleRoles = (roles: UserRole[]): UserRole[] => {
+    return roles.filter(role => role !== 'user');
+  };
+
   const handleAddUser = async (e: React.FormEvent) => {
     e.preventDefault();
     setError(null);
 
+    // Add 'user' role implicitly if no roles selected
+    const rolesToAdd: UserRole[] = newRoles.length === 0 ? ['user'] : newRoles;
+
     startTransition(async () => {
       try {
-        await addUserAction(newEmail, newRole);
+        await addUserAction(newEmail, rolesToAdd);
         setNewEmail('');
+        setNewRoles([]);
       } catch (err) {
         setError(err instanceof Error ? err.message : 'Failed to add user');
       }
@@ -47,16 +60,34 @@ export function UsersListClient({
     });
   };
 
-  const handleChangeRole = (email: string, role: 'user' | 'admin') => {
+  const handleEditRoles = (email: string, currentRoles: UserRole[]) => {
+    setEditingUser(email);
+    // Only show assignable roles for editing
+    setEditRoles(getVisibleRoles(currentRoles));
+  };
+
+  const handleSaveRoles = (email: string) => {
+    // Add 'user' role implicitly if no roles selected
+    const rolesToSave: UserRole[] = editRoles.length === 0 ? ['user'] : editRoles;
+
+    setError(null);
     startTransition(async () => {
       try {
-        await updateUserRoleAction(email, role);
+        await updateUserRolesAction(email, rolesToSave);
+        setEditingUser(null);
+        setEditRoles([]);
       } catch (err) {
-        setError(err instanceof Error ? err.message : 'Failed to update role');
+        setError(err instanceof Error ? err.message : 'Failed to update roles');
       }
     });
   };
 
+  const handleCancelEdit = () => {
+    setEditingUser(null);
+    setEditRoles([]);
+    setError(null);
+  };
+
   return (
     <>
       {/* Add User Form */}
@@ -67,25 +98,33 @@ export function UsersListClient({
             {error}
           </div>
         )}
-        <form onSubmit={handleAddUser} className="flex gap-4">
-          <input
-            type="email"
-            required
-            value={newEmail}
-            onChange={(e) => setNewEmail(e.target.value)}
-            placeholder="user@example.com"
-            disabled={isPending}
-            className="flex-1 rounded-lg border border-border bg-input px-4 py-2 text-foreground outline-none focus:border-ring focus:ring-2 focus:ring-ring/50 disabled:opacity-50"
-          />
-          <select
-            value={newRole}
-            onChange={(e) => setNewRole(e.target.value as 'user' | 'admin')}
-            disabled={isPending}
-            className="rounded-lg border border-border bg-input px-4 py-2 text-foreground outline-none focus:border-ring disabled:opacity-50"
-          >
-            <option value="user">User</option>
-            <option value="admin">Admin</option>
-          </select>
+        <form onSubmit={handleAddUser} className="space-y-4">
+          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+            <div>
+              <label className="block text-sm font-semibold text-foreground mb-2">
+                Email
+              </label>
+              <input
+                type="email"
+                required
+                value={newEmail}
+                onChange={(e) => setNewEmail(e.target.value)}
+                placeholder="user@example.com"
+                disabled={isPending}
+                className="w-full rounded-lg border border-border bg-input px-4 py-2 text-foreground outline-none focus:border-ring focus:ring-2 focus:ring-ring/50 disabled:opacity-50"
+              />
+            </div>
+            <div>
+              <label className="block text-sm font-semibold text-foreground mb-2">
+                Roles
+              </label>
+              <RoleSelector
+                selectedRoles={newRoles}
+                onChange={setNewRoles}
+                disabled={isPending}
+              />
+            </div>
+          </div>
           <button
             type="submit"
             disabled={isPending}
@@ -106,65 +145,104 @@ export function UsersListClient({
           <p className="text-center text-foreground/60">No users yet</p>
         )}
 
-        <div className="space-y-2">
-          {initialUsers.map((user) => (
-            <div
-              key={user.email}
-              className="flex items-center justify-between rounded-lg border border-border bg-card p-4"
-            >
-              <div className="flex-1">
-                <p className="font-medium text-foreground">{user.email}</p>
-                <p className="text-sm text-foreground/50">
-                  Added {new Date(user.addedAt).toLocaleDateString()}
-                  {user.addedBy && ` by ${user.addedBy}`}
-                </p>
-              </div>
-
-              <div className="flex items-center gap-3">
-                {/* Role Badge */}
-                <span
-                  className={`rounded px-3 py-1 text-sm font-semibold ${
-                    user.role === 'admin'
-                      ? 'bg-accent/20 text-accent-foreground'
-                      : 'bg-primary/20 text-primary-foreground'
-                  }`}
-                >
-                  {user.role}
-                </span>
-
-                {/* Change Role */}
-                {user.email !== currentUserEmail && (
-                  <button
-                    onClick={() =>
-                      handleChangeRole(
-                        user.email,
-                        user.role === 'admin' ? 'user' : 'admin'
-                      )
-                    }
-                    disabled={isPending}
-                    className="rounded bg-background/10 px-3 py-1 text-sm text-foreground/70 transition hover:bg-background/20 hover:text-foreground disabled:opacity-50"
-                  >
-                    Make {user.role === 'admin' ? 'User' : 'Admin'}
-                  </button>
-                )}
-
-                {/* Remove */}
-                {user.email !== currentUserEmail && (
-                  <button
-                    onClick={() => handleRemoveUser(user.email)}
-                    disabled={isPending}
-                    className="rounded bg-destructive/20 px-3 py-1 text-sm text-destructive-foreground transition hover:bg-destructive/30 disabled:opacity-50"
-                  >
-                    Remove
-                  </button>
-                )}
-
-                {user.email === currentUserEmail && (
-                  <span className="text-sm text-foreground/40">(You)</span>
-                )}
+        <div className="space-y-4">
+          {initialUsers.map((user) => {
+            const isEditing = editingUser === user.email;
+
+            return (
+              <div
+                key={user.email}
+                className="rounded-lg border border-border bg-card p-4"
+              >
+                <div className="flex items-start justify-between gap-4">
+                  <div className="flex-1 min-w-0">
+                    <div className="flex items-center gap-2 mb-2">
+                      <p className="font-medium text-foreground">{user.email}</p>
+                      {user.email === currentUserEmail && (
+                        <span className="text-xs text-muted-foreground">(You)</span>
+                      )}
+                    </div>
+                    <p className="text-sm text-muted-foreground mb-3">
+                      Added {new Date(user.addedAt).toLocaleDateString()}
+                      {user.addedBy && ` by ${user.addedBy}`}
+                    </p>
+
+                    {/* Display or Edit Roles */}
+                    {isEditing ? (
+                      <div className="space-y-3">
+                        <RoleSelector
+                          selectedRoles={editRoles}
+                          onChange={setEditRoles}
+                          disabled={isPending}
+                        />
+                        <div className="flex gap-2">
+                          <button
+                            onClick={() => handleSaveRoles(user.email)}
+                            disabled={isPending}
+                            className="rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition hover:bg-primary/80 disabled:opacity-50"
+                          >
+                            Save
+                          </button>
+                          <button
+                            onClick={handleCancelEdit}
+                            disabled={isPending}
+                            className="rounded-lg border border-border px-4 py-2 text-sm font-semibold text-foreground transition hover:bg-muted disabled:opacity-50"
+                          >
+                            Cancel
+                          </button>
+                        </div>
+                      </div>
+                    ) : (
+                      <div className="flex flex-wrap gap-2">
+                        {getVisibleRoles(user.roles || []).length === 0 ? (
+                          <span className="text-sm text-muted-foreground italic">
+                            No special roles (basic user)
+                          </span>
+                        ) : (
+                          getVisibleRoles(user.roles || []).map(role => (
+                            <span
+                              key={role}
+                              className={`inline-flex items-center rounded px-2 py-1 text-xs font-semibold ${
+                                role === 'admin'
+                                  ? 'bg-accent/20 text-accent-foreground'
+                                  : role === 'community_manager'
+                                  ? 'bg-primary/20 text-primary-foreground'
+                                  : role === 'library_manager'
+                                  ? 'bg-success/20 text-success-foreground'
+                                  : 'bg-muted/60 text-muted-foreground'
+                              }`}
+                            >
+                              {ROLE_LABELS[role]}
+                            </span>
+                          ))
+                        )}
+                      </div>
+                    )}
+                  </div>
+
+                  {/* Action Buttons */}
+                  {!isEditing && user.email !== currentUserEmail && (
+                    <div className="flex gap-2">
+                      <button
+                        onClick={() => handleEditRoles(user.email, user.roles)}
+                        disabled={isPending}
+                        className="rounded bg-muted px-3 py-1 text-sm font-semibold text-foreground transition hover:bg-muted/70 disabled:opacity-50"
+                      >
+                        Edit Roles
+                      </button>
+                      <button
+                        onClick={() => handleRemoveUser(user.email)}
+                        disabled={isPending}
+                        className="rounded bg-destructive/20 px-3 py-1 text-sm text-destructive-foreground transition hover:bg-destructive/30 disabled:opacity-50"
+                      >
+                        Remove
+                      </button>
+                    </div>
+                  )}
+                </div>
               </div>
-            </div>
-          ))}
+            );
+          })}
         </div>
       </div>
     </>
diff --git a/src/app/api/admin/request-action/route.ts b/src/app/api/admin/request-action/route.ts
index 0ed73bd..b109d39 100644
--- a/src/app/api/admin/request-action/route.ts
+++ b/src/app/api/admin/request-action/route.ts
@@ -107,7 +107,7 @@ export async function GET(request: NextRequest) {
               <div class="icon">${statusEmoji}</div>
               <h1>Already Processed</h1>
               <p>This request was <span class="status">${statusText}</span> on ${new Date(accessRequest.reviewedAt!).toLocaleDateString()} at ${new Date(accessRequest.reviewedAt!).toLocaleTimeString()}</p>
-              <a href="/admin/requests" class="button">View All Requests</a>
+              <a href="/admin/users/requests" class="button">View All Requests</a>
             </div>
           </body>
         </html>`,
@@ -258,7 +258,7 @@ export async function GET(request: NextRequest) {
               <div class="denied-icon">&#x274C;</div>
               <h1>Request Denied</h1>
               <p>Request from <span class="email">${accessRequest.email}</span> has been denied</p>
-              <a href="/admin/requests" class="button">View Requests</a>
+              <a href="/admin/users/requests" class="button">View Requests</a>
             </div>
           </body>
         </html>`,
diff --git a/src/components/admin/role-selector.tsx b/src/components/admin/role-selector.tsx
new file mode 100644
index 0000000..38842a6
--- /dev/null
+++ b/src/components/admin/role-selector.tsx
@@ -0,0 +1,150 @@
+/**
+ * Role Selector Component
+ * Multi-select role picker with descriptions
+ */
+
+'use client';
+
+import { useState } from 'react';
+import type { UserRole, AssignableRole } from '@/lib/admin/types';
+import { ROLE_LABELS, ROLE_DESCRIPTIONS, ROLE_HIERARCHY, ASSIGNABLE_ROLES } from '@/lib/admin/types';
+
+interface RoleSelectorProps {
+  selectedRoles: UserRole[];
+  onChange: (roles: UserRole[]) => void;
+  disabled?: boolean;
+}
+
+export function RoleSelector({ selectedRoles, onChange, disabled = false }: RoleSelectorProps) {
+  const [showDropdown, setShowDropdown] = useState(false);
+
+  const toggleRole = (role: UserRole) => {
+    if (disabled) return;
+
+    if (selectedRoles.includes(role)) {
+      // Remove role
+      onChange(selectedRoles.filter(r => r !== role));
+    } else {
+      // Add role
+      onChange([...selectedRoles, role]);
+    }
+  };
+
+  // Only show assignable roles (filter out 'user' which is implicit)
+  const visibleSelectedRoles = selectedRoles.filter(role => role !== 'user');
+
+  // Sort roles by hierarchy level (highest first)
+  const sortedRoles = [...ASSIGNABLE_ROLES].sort((a, b) =>
+    ROLE_HIERARCHY[b] - ROLE_HIERARCHY[a]
+  );
+
+  const getHighestRole = (): UserRole | null => {
+    if (selectedRoles.length === 0) return null;
+    return selectedRoles.reduce((highest, role) =>
+      ROLE_HIERARCHY[role] > ROLE_HIERARCHY[highest] ? role : highest
+    );
+  };
+
+  const highestRole = getHighestRole();
+
+  return (
+    <div className="relative">
+      {/* Display selected roles (exclude 'user') */}
+      <div
+        className={`min-h-[42px] rounded-lg border border-border bg-input px-3 py-2 cursor-pointer ${
+          disabled ? 'opacity-50 cursor-not-allowed' : ''
+        }`}
+        onClick={() => !disabled && setShowDropdown(!showDropdown)}
+      >
+        {visibleSelectedRoles.length === 0 ? (
+          <span className="text-muted-foreground text-sm">Select roles...</span>
+        ) : (
+          <div className="flex flex-wrap gap-2">
+            {visibleSelectedRoles.map(role => (
+              <span
+                key={role}
+                className={`inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-semibold ${
+                  role === 'admin'
+                    ? 'bg-accent/20 text-accent-foreground'
+                    : role === 'community_manager'
+                    ? 'bg-primary/20 text-primary-foreground'
+                    : role === 'library_manager'
+                    ? 'bg-success/20 text-success-foreground'
+                    : 'bg-muted/60 text-muted-foreground'
+                }`}
+              >
+                {ROLE_LABELS[role]}
+                {!disabled && (
+                  <button
+                    type="button"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      toggleRole(role);
+                    }}
+                    className="ml-1 hover:text-destructive"
+                  >
+                    ×
+                  </button>
+                )}
+              </span>
+            ))}
+          </div>
+        )}
+      </div>
+
+      {/* Dropdown with all roles */}
+      {showDropdown && !disabled && (
+        <>
+          {/* Backdrop */}
+          <div
+            className="fixed inset-0 z-10"
+            onClick={() => setShowDropdown(false)}
+          />
+
+          {/* Dropdown content */}
+          <div className="absolute left-0 right-0 top-full mt-2 z-20 rounded-lg border border-border bg-card shadow-lg">
+            <div className="p-2">
+              {sortedRoles.map(role => (
+                <button
+                  key={role}
+                  type="button"
+                  onClick={() => toggleRole(role)}
+                  className={`w-full text-left rounded-lg px-3 py-2 transition ${
+                    selectedRoles.includes(role)
+                      ? 'bg-primary/10 border border-primary/30'
+                      : 'hover:bg-muted'
+                  }`}
+                >
+                  <div className="flex items-start gap-3">
+                    <div className={`mt-1 w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 ${
+                      selectedRoles.includes(role)
+                        ? 'bg-primary border-primary'
+                        : 'border-border'
+                    }`}>
+                      {selectedRoles.includes(role) && (
+                        <svg className="w-3 h-3 text-primary-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
+                        </svg>
+                      )}
+                    </div>
+                    <div className="flex-1">
+                      <div className="font-semibold text-foreground flex items-center gap-2">
+                        {ROLE_LABELS[role]}
+                        {role === highestRole && selectedRoles.length > 1 && (
+                          <span className="text-xs font-normal text-muted-foreground">(highest)</span>
+                        )}
+                      </div>
+                      <div className="text-sm text-muted-foreground mt-0.5">
+                        {ROLE_DESCRIPTIONS[role]}
+                      </div>
+                    </div>
+                  </div>
+                </button>
+              ))}
+            </div>
+          </div>
+        </>
+      )}
+    </div>
+  );
+}
diff --git a/src/lib/admin/access-requests-service.ts b/src/lib/admin/access-requests-service.ts
index 148e4b3..a5f1dc8 100644
--- a/src/lib/admin/access-requests-service.ts
+++ b/src/lib/admin/access-requests-service.ts
@@ -13,9 +13,13 @@ export interface AccessRequest {
   email: string;
   message: string;
   requestedAt: string;
-  status: 'pending' | 'approved' | 'denied';
+  status: 'pending' | 'approved' | 'denied' | 'bucketed';
   reviewedAt?: string;
   reviewedBy?: string;
+  bucket?: string;
+  replyMessage?: string;
+  replySentAt?: string;
+  seededBy?: string;
 }
 
 const REDIS_KEYS = {
@@ -147,7 +151,7 @@ export class AccessRequestsService {
   static async approveRequest(
     id: string,
     reviewedBy: string,
-    role: 'user' | 'admin' = 'user'
+    roles: import('./types').UserRole | import('./types').UserRole[] = 'user'
   ): Promise<void> {
     const request = await this.getRequest(id);
     if (!request) {
@@ -166,9 +170,9 @@ export class AccessRequestsService {
     // Remove from pending
     await client.srem(REDIS_KEYS.pendingRequests, id);
 
-    // Add user with specified role
+    // Add user with specified role(s)
     const { UserManagementService } = await import('./user-management-service');
-    await UserManagementService.addUser(request.email, role, reviewedBy);
+    await UserManagementService.addUser(request.email, roles, reviewedBy);
 
     // Send approval email
     await this.sendApprovalEmail(request.email);
@@ -203,6 +207,40 @@ export class AccessRequestsService {
     logger.info(`✅ Request ${id} denied for ${request.email}`);
   }
 
+  /**
+   * Reply to a request and assign a bucket
+   */
+  static async replyToRequest(
+    id: string,
+    reviewedBy: string,
+    replyMessage: string,
+    bucket: string
+  ): Promise<void> {
+    const request = await this.getRequest(id);
+    if (!request) {
+      throw new Error('Request not found');
+    }
+
+    const client = getRedisClient();
+    const now = new Date().toISOString();
+    const normalizedBucket = bucket.trim() || 'General waitlist';
+    const trimmedMessage = replyMessage.trim();
+
+    request.status = 'bucketed';
+    request.reviewedAt = now;
+    request.reviewedBy = reviewedBy;
+    request.replyMessage = trimmedMessage;
+    request.replySentAt = now;
+    request.bucket = normalizedBucket;
+
+    await client.set(REDIS_KEYS.request(id), JSON.stringify(request));
+    await client.srem(REDIS_KEYS.pendingRequests, id);
+
+    await this.sendReplyEmail(request.email, trimmedMessage, normalizedBucket);
+
+    logger.info(`✅ Request ${id} bucketed (${normalizedBucket}) for ${request.email}`);
+  }
+
   /**
    * Send notification email to admins for a specific request
    */
@@ -234,7 +272,7 @@ export class AccessRequestsService {
     const denyToken = this.generateActionToken(request.id, 'deny');
     const approveUrl = `${baseUrl}/api/admin/request-action?token=${approveToken}`;
     const denyUrl = `${baseUrl}/api/admin/request-action?token=${denyToken}`;
-    const reviewUrl = `${baseUrl}/admin/requests?id=${request.id}`;
+    const reviewUrl = `${baseUrl}/admin/users/requests?id=${request.id}`;
     const formattedMessage = request.message.replace(/\n/g, '<br>');
 
     logger.info(`📧 Sending admin notification for request ${request.id}`);
@@ -300,7 +338,7 @@ export class AccessRequestsService {
               </div>
 
               <div class="footer">
-                <p>Click a button above to take action, or visit the <a href="${baseUrl}/admin/requests" style="color: #06b6d4;">admin panel</a> to review.</p>
+                <p>Click a button above to take action, or visit the <a href="${baseUrl}/admin/users/requests" style="color: #06b6d4;">admin panel</a> to review.</p>
                 <p style="margin-top: 10px; font-size: 10px; color: #444;">Request ID: ${request.id}</p>
               </div>
             </div>
@@ -424,6 +462,72 @@ export class AccessRequestsService {
     }
   }
 
+  /**
+   * Send custom reply email with bucket information
+   */
+  private static async sendReplyEmail(email: string, replyMessage: string, bucket: string): Promise<void> {
+    try {
+      logger.info(`📧 Sending bucket reply email to ${email}...`);
+
+      const { Resend } = await import('resend');
+      const resend = new Resend(process.env.RESEND_API_KEY);
+
+      if (!process.env.RESEND_API_KEY) {
+        logger.error('❌ RESEND_API_KEY not configured - skipping email');
+        return;
+      }
+
+      const fromDomain = process.env.RESEND_FROM_DOMAIN || 'yourdomain.com';
+      const safeMessage = (replyMessage ?? '')
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/\n/g, '<br>');
+      const safeBucket = bucket?.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') || 'General waitlist';
+
+      const result = await resend.emails.send({
+        from: `React Foundation <noreply@${fromDomain}>`,
+        to: [email],
+        subject: 'Access Request Update - React Foundation',
+        html: `
+          <!DOCTYPE html>
+          <html>
+            <body style="font-family: system-ui; background: #000; color: #fff; padding: 40px;">
+              <div style="max-width: 500px; margin: 0 auto; text-align: left;">
+                <h1 style="color: #06b6d4; font-size: 28px; margin-bottom: 16px;">Access Request Update</h1>
+                <p style="color: #fff; font-size: 16px; line-height: 1.5;">
+                  Thank you for your request—here&apos;s the latest update.
+                </p>
+                <div style="background: #111; border: 1px solid #1f2937; border-radius: 8px; padding: 20px; margin: 24px 0;">
+                  <p style="color: #06b6d4; text-transform: uppercase; letter-spacing: 1px; font-size: 12px; margin: 0 0 8px;">
+                    Message
+                  </p>
+                  <p style="color: #e5e7eb; font-size: 16px; line-height: 1.6; white-space: pre-wrap;">
+                    ${safeMessage}
+                  </p>
+                </div>
+                <p style="color: #a855f7; font-weight: bold; font-size: 14px;">
+                  You&apos;re currently in: ${safeBucket}
+                </p>
+                <p style="color: #9ca3af; font-size: 14px; margin-top: 24px;">
+                  We&apos;ll reach out as soon as it&apos;s your turn.
+                </p>
+              </div>
+            </body>
+          </html>
+        `,
+      });
+
+      if (result.data) {
+        logger.info(`✅ Bucket reply email sent! ID: ${result.data.id}`);
+      } else if (result.error) {
+        logger.error(`❌ Resend error:`, result.error);
+      }
+    } catch (_error) {
+      logger.error(`❌ Error sending bucket reply email to ${email}:`, _error);
+    }
+  }
+
   /**
    * Generate action token for email links
    */
@@ -446,7 +550,7 @@ export class AccessRequestsService {
       }
 
       return { requestId, action };
-    } catch (_error) {
+    } catch {
       return null;
     }
   }
diff --git a/src/lib/admin/types.ts b/src/lib/admin/types.ts
new file mode 100644
index 0000000..af19e5a
--- /dev/null
+++ b/src/lib/admin/types.ts
@@ -0,0 +1,55 @@
+/**
+ * Admin Types and Constants
+ * Shared types and constants that can be safely imported in client components
+ */
+
+export type UserRole = 'admin' | 'community_manager' | 'library_manager' | 'user';
+
+/**
+ * Assignable roles that can be given/removed from users
+ * Note: 'user' is implicit for all authenticated users and not assignable
+ */
+export type AssignableRole = 'admin' | 'community_manager' | 'library_manager';
+
+export interface User {
+  email: string;
+  roles: UserRole[]; // Multiple roles per user (user role is implicit)
+  addedAt: string;
+  addedBy?: string;
+}
+
+/**
+ * Role hierarchy and permissions
+ * Higher roles inherit all permissions from lower roles
+ * Note: All authenticated users have implicit 'user' role
+ */
+export const ROLE_HIERARCHY: Record<UserRole, number> = {
+  admin: 4,
+  community_manager: 3,
+  library_manager: 2,
+  user: 1, // Implicit for all authenticated users
+};
+
+export const ROLE_LABELS: Record<UserRole, string> = {
+  admin: 'Admin',
+  community_manager: 'Community Manager',
+  library_manager: 'Library Manager',
+  user: 'User', // Not shown in UI
+};
+
+export const ROLE_DESCRIPTIONS: Record<UserRole, string> = {
+  admin: 'Full system access - can do everything',
+  community_manager: 'Can manage communities and members',
+  library_manager: 'Can manage libraries and contributions',
+  user: 'Basic access (implicit for all authenticated users)',
+};
+
+/**
+ * Only these roles can be assigned in the UI
+ * 'user' role is implicit and not assignable
+ */
+export const ASSIGNABLE_ROLES: AssignableRole[] = [
+  'admin',
+  'community_manager',
+  'library_manager',
+];
diff --git a/src/lib/admin/user-management-service.ts b/src/lib/admin/user-management-service.ts
index aef88d7..f5b0417 100644
--- a/src/lib/admin/user-management-service.ts
+++ b/src/lib/admin/user-management-service.ts
@@ -1,19 +1,17 @@
 /**
  * User Management Service
  * Handles user roles and access control stored in Redis
+ * Supports multiple roles per user with hierarchical permissions
  */
 
 import { getRedisClient } from '@/lib/redis';
 import { logger } from '@/lib/logger';
 
-export type UserRole = 'admin' | 'user';
+// Re-export types and constants from shared types file
+export type { User, UserRole } from './types';
+export { ROLE_HIERARCHY, ROLE_LABELS, ROLE_DESCRIPTIONS } from './types';
 
-export interface User {
-  email: string;
-  role: UserRole;
-  addedAt: string;
-  addedBy?: string;
-}
+import { ROLE_HIERARCHY, type UserRole, type User } from './types';
 
 const REDIS_KEYS = {
   user: (email: string) => `admin:user:${email.toLowerCase()}`,
@@ -22,23 +20,50 @@ const REDIS_KEYS = {
 };
 
 export class UserManagementService {
-  // Super admin - always has access even if Redis is empty
+  /**
+   * Super admin email from environment variable
+   * This user always has full admin access, even if not in Redis
+   * Useful for initial setup and emergency access
+   * Set via SUPER_ADMIN_EMAIL environment variable
+   */
   private static get SUPER_ADMIN(): string | undefined {
     return process.env.SUPER_ADMIN_EMAIL?.toLowerCase();
   }
 
   /**
-   * Add or update a user
+   * Check if a super admin is configured
+   */
+  static isSuperAdminConfigured(): boolean {
+    return !!process.env.SUPER_ADMIN_EMAIL;
+  }
+
+  /**
+   * Check if email is the super admin
+   */
+  static isSuperAdmin(email: string): boolean {
+    const superAdmin = this.SUPER_ADMIN;
+    if (!superAdmin) return false;
+    return email.toLowerCase().trim() === superAdmin;
+  }
+
+  /**
+   * Add or update a user with multiple roles
    */
-  static async addUser(email: string, role: UserRole, addedBy?: string): Promise<void> {
+  static async addUser(email: string, roles: UserRole | UserRole[], addedBy?: string): Promise<void> {
     const client = getRedisClient();
     const normalizedEmail = email.toLowerCase().trim();
 
+    // Normalize roles to array
+    const rolesArray = Array.isArray(roles) ? roles : [roles];
+
+    // Get existing user to preserve addedAt if updating
+    const existing = await this.getUser(normalizedEmail);
+
     const user: User = {
       email: normalizedEmail,
-      role,
-      addedAt: new Date().toISOString(),
-      addedBy,
+      roles: rolesArray,
+      addedAt: existing?.addedAt ?? new Date().toISOString(),
+      addedBy: existing?.addedBy ?? addedBy,
     };
 
     // Store user data
@@ -47,15 +72,29 @@ export class UserManagementService {
     // Add to all users set
     await client.sadd(REDIS_KEYS.allUsers, normalizedEmail);
 
-    // Add to admins set if admin role
-    if (role === 'admin') {
+    // Add to admins set if has admin role
+    if (rolesArray.includes('admin')) {
       await client.sadd(REDIS_KEYS.admins, normalizedEmail);
     } else {
-      // Remove from admins if downgraded
+      // Remove from admins if no longer admin
       await client.srem(REDIS_KEYS.admins, normalizedEmail);
     }
 
-    logger.info(`✅ User ${normalizedEmail} added with role: ${role}`);
+    logger.info(`✅ User ${normalizedEmail} ${existing ? 'updated' : 'added'} with roles: ${rolesArray.join(', ')}`);
+  }
+
+  /**
+   * Update user roles (replaces all existing roles)
+   */
+  static async updateUserRoles(email: string, roles: UserRole[], updatedBy?: string): Promise<void> {
+    const normalizedEmail = email.toLowerCase().trim();
+    const existing = await this.getUser(normalizedEmail);
+
+    if (!existing) {
+      throw new Error(`User ${normalizedEmail} not found`);
+    }
+
+    await this.addUser(normalizedEmail, roles, updatedBy);
   }
 
   /**
@@ -83,7 +122,18 @@ export class UserManagementService {
     if (!data) return null;
 
     try {
-      return JSON.parse(data);
+      const parsed = JSON.parse(data);
+
+      // Backward compatibility: migrate old 'role' field to 'roles' array
+      if (parsed.role && !parsed.roles) {
+        parsed.roles = [parsed.role];
+        delete parsed.role;
+        // Auto-save the migrated data
+        await client.set(REDIS_KEYS.user(normalizedEmail), JSON.stringify(parsed));
+        logger.info(`🔄 Migrated user ${normalizedEmail} from role to roles array`);
+      }
+
+      return parsed;
     } catch (error) {
       logger.error(`Error parsing user data for ${normalizedEmail}:`, error);
       return null;
@@ -98,6 +148,7 @@ export class UserManagementService {
 
     // Super admin always has access
     if (this.SUPER_ADMIN && normalizedEmail === this.SUPER_ADMIN) {
+      logger.info(`✅ Super admin access granted: ${normalizedEmail}`);
       return true;
     }
 
@@ -113,6 +164,7 @@ export class UserManagementService {
 
     // Super admin is always admin
     if (this.SUPER_ADMIN && normalizedEmail === this.SUPER_ADMIN) {
+      logger.info(`✅ Super admin detected: ${normalizedEmail} (from SUPER_ADMIN_EMAIL)`);
       return true;
     }
 
@@ -135,18 +187,40 @@ export class UserManagementService {
     const values = await client.mget(...keys);
 
     const users: User[] = [];
+    const migratedKeys: string[] = [];
+    const migratedValues: string[] = [];
+
     for (let i = 0; i < values.length; i++) {
       const data = values[i];
       if (!data) continue;
 
       try {
         const user = JSON.parse(data);
+
+        // Backward compatibility: migrate old 'role' field to 'roles' array
+        if (user.role && !user.roles) {
+          user.roles = [user.role];
+          delete user.role;
+          migratedKeys.push(REDIS_KEYS.user(emails[i]));
+          migratedValues.push(JSON.stringify(user));
+        }
+
         users.push(user);
       } catch (error) {
         logger.error(`Error parsing user data for ${emails[i]}:`, error);
       }
     }
 
+    // Batch save migrated users
+    if (migratedKeys.length > 0) {
+      const pipeline = client.pipeline();
+      for (let i = 0; i < migratedKeys.length; i++) {
+        pipeline.set(migratedKeys[i], migratedValues[i]);
+      }
+      await pipeline.exec();
+      logger.info(`🔄 Migrated ${migratedKeys.length} users from role to roles array`);
+    }
+
     return users.sort((a, b) => a.email.localeCompare(b.email));
   }
 
@@ -155,19 +229,52 @@ export class UserManagementService {
    */
   static async getAdmins(): Promise<User[]> {
     const users = await this.getAllUsers();
-    return users.filter(u => u.role === 'admin');
+    return users.filter(u => u.roles.includes('admin'));
   }
 
   /**
-   * Update user role
+   * Check if user has a specific role
    */
-  static async updateUserRole(email: string, role: UserRole, updatedBy?: string): Promise<void> {
+  static async hasRole(email: string, role: UserRole): Promise<boolean> {
     const user = await this.getUser(email);
-    if (!user) {
-      throw new Error('User not found');
+    if (!user) return false;
+    return user.roles.includes(role);
+  }
+
+  /**
+   * Check if user has permission level (considering hierarchy)
+   * @param email User email
+   * @param requiredRole The minimum role required
+   * @returns true if user has this role or a higher role in the hierarchy
+   */
+  static async hasPermission(email: string, requiredRole: UserRole): Promise<boolean> {
+    const normalizedEmail = email.toLowerCase().trim();
+
+    // Super admin has all permissions
+    if (this.SUPER_ADMIN && normalizedEmail === this.SUPER_ADMIN) {
+      return true;
     }
 
-    await this.addUser(email, role, updatedBy || user.addedBy);
+    const user = await this.getUser(normalizedEmail);
+    if (!user) return false;
+
+    const requiredLevel = ROLE_HIERARCHY[requiredRole];
+
+    // Check if user has any role that meets or exceeds the required level
+    return user.roles.some(userRole => {
+      const userLevel = ROLE_HIERARCHY[userRole];
+      return userLevel >= requiredLevel;
+    });
+  }
+
+  /**
+   * Get user's highest role level
+   */
+  static async getHighestRoleLevel(email: string): Promise<number> {
+    const user = await this.getUser(email);
+    if (!user || user.roles.length === 0) return 0;
+
+    return Math.max(...user.roles.map(role => ROLE_HIERARCHY[role]));
   }
 
   /**
