Epic Doc

Your data lives in the Docker volume and is not deleted when containers are recreated.
Epic Doc

🎯 Ready for Production

Database Integration

// Easy migration from JSON mock to real database
export class DatabaseCourseAdapter {
  async findById(id: string): Promise<Course | null> {
    // Replace JSON file access with database query
    const result = await db.course.findUnique({ where: { id } })
    if (!result) return null
    
    // Keep Nostr integration exactly the same
    if (result.noteId) {
      const nostrEvent = await fetchNostrEvent(result.noteId)
      return { ...result, note: nostrEvent }
    }
    return result
  }
}

πŸ” Dual Authentication Architecture: Nostr-First vs OAuth-First

Revolutionary Identity-Source System with Complete Profile Collection

Latest Enhancement: Complete NIP-01 Profile Support

// Session now includes comprehensive user data
const { data: session } = useSession()

// Basic fields (all users)
session?.user?.name          // Display name
session?.user?.email         // Email address  
session?.user?.image         // Avatar/profile picture
session?.user?.pubkey        // Nostr public key

// Enhanced Nostr profile fields (all users)
session?.user?.nip05         // Nostr address verification
session?.user?.lud16         // Lightning address
session?.user?.banner        // Profile banner image

// Complete Nostr profile (Nostr-first accounts)
session?.user?.nostrProfile?.about      // Bio/description
session?.user?.nostrProfile?.website    // Personal website
session?.user?.nostrProfile?.location   // Geographic location
session?.user?.nostrProfile?.github     // GitHub username
session?.user?.nostrProfile?.twitter    // Twitter handle
// ... plus any other fields from user's NIP-01 profile

Revolutionary Identity-Source System

/**
 * DUAL AUTHENTICATION ARCHITECTURE - Two distinct paradigms
 * 
 * πŸ”΅ NOSTR-FIRST ACCOUNTS (Nostr as identity source):
 * --------------------------------------------------
 * β€’ NIP07 Authentication (nostr provider) - User custody of keys
 * β€’ Anonymous Authentication (anonymous provider) - Platform custody for experimentation
 * 
 * Behavior:
 * - Nostr profile is the SOURCE OF TRUTH for user data
 * - Profile sync happens on every login from Nostr relays
 * - Database user fields are updated if Nostr profile differs
 * - User's Nostr identity drives their platform identity
 * 
 * 🟠 OAUTH-FIRST ACCOUNTS (Platform as identity source):
 * -----------------------------------------------------
 * β€’ Email Authentication (email provider) - May not know about Nostr
 * β€’ GitHub Authentication (github provider) - May not know about Nostr
 * 
 * Behavior:
 * - OAuth profile is the SOURCE OF TRUTH for user data
 * - Ephemeral Nostr keypairs generated for background Nostr functionality
 * - No profile sync from Nostr - OAuth data takes precedence
 * - Platform identity drives their Nostr identity (not vice versa)
 */

// OAUTH-FIRST: Ephemeral keypair generation for transparent Nostr access
events: {
  async createUser({ user }) {
    // Only OAuth-first accounts get ephemeral keys automatically
    if (!user.pubkey) {
      const keys = await generateKeypair()
      await prisma.user.update({
        where: { id: user.id },
        data: {
          pubkey: keys.publicKey,
          privkey: keys.privateKey, // Background Nostr capabilities
        }
      })
    }
  },
  
  async signIn({ user, account }) {
    // NOSTR-FIRST: Sync profile from Nostr relays (source of truth)
    const isNostrFirst = ['nostr', 'anonymous', 'recovery'].includes(account?.provider)
    if (user.pubkey && isNostrFirst) {
      await syncUserProfileFromNostr(user.id, user.pubkey)
    }
    // OAUTH-FIRST: Skip Nostr sync, OAuth profile is authoritative
  }
}

// Universal session with proper key handling
async session({ session, token }) {
  if (session.user.pubkey) {
    // Include privkey for ephemeral accounts (anonymous, email, GitHub)
    // NIP07 users never have privkey stored (user-controlled keys)
    const dbUser = await prisma.user.findUnique({
      where: { id: token.userId },
      select: { privkey: true }
    })
    if (dbUser?.privkey) {
      session.user.privkey = dbUser.privkey // Enable client-side signing
    }
  }
}

Four Authentication Methods with Universal Nostr Capabilities

const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    // 🟠 OAUTH-FIRST: Email Magic Links (User may not know about Nostr)
    EmailProvider({
      server: {
        host: process.env.EMAIL_SERVER_HOST,
        port: parseInt(process.env.EMAIL_SERVER_PORT || '587'),
        auth: {
          user: process.env.EMAIL_SERVER_USER,
          pass: process.env.EMAIL_SERVER_PASSWORD
        }
      },
      from: process.env.EMAIL_FROM
      // β†’ Gets ephemeral keypair for background Nostr functionality
      // β†’ Email profile is source of truth, no Nostr profile sync
    }),
    
    // 🟠 OAUTH-FIRST: GitHub OAuth (User may not know about Nostr)  
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
      profile(profile) {
        // Simplified GitHub profile mapping (essential fields only)
        return {
          id: profile.id.toString(),
          email: profile.email,
          name: profile.name || profile.login,
          image: profile.avatar_url,
          // β†’ Gets ephemeral keypair for background Nostr functionality  
          // β†’ GitHub profile is source of truth, no Nostr profile sync
        }
      }
    }),
    
    // πŸ”΅ NOSTR-FIRST: Anonymous (User trying things out)
    CredentialsProvider({
      id: 'anonymous',
      async authorize() {
        const keys = await generateKeypair()
        // Create anonymous user with fresh keypair (platform custody)
        const user = await prisma.user.create({
          data: {
            pubkey: keys.publicKey,
            privkey: keys.privateKey, // Platform manages keys for experiments
            username: `anon_${keys.publicKey.substring(0, 8)}`
          }
        })
        // β†’ Attempts to sync with any existing Nostr profile
        await syncUserProfileFromNostr(user.id, keys.publicKey)
        return user
      }
    }),
    
    // πŸ”΅ NOSTR-FIRST: NIP07 Browser Extension (User in custody)
    CredentialsProvider({
      id: 'nostr',
      async authorize(credentials) {
        // User provides pubkey via browser extension (user controls keys)
        let user = await prisma.user.findUnique({
          where: { pubkey: credentials.pubkey }
        })
        if (!user) {
          user = await prisma.user.create({
            data: { 
              pubkey: credentials.pubkey 
              // No privkey stored - user has custody via browser extension
            }
          })
        }
        // β†’ ALWAYS sync profile from Nostr (source of truth)
        await syncUserProfileFromNostr(user.id, credentials.pubkey)
        return user
      }
    })
  ]
}

Identity-Source Architecture Benefits

πŸ”΅ Nostr-First Accounts (NIP07 & Anonymous):

  • Profile Sovereignty: Nostr profile always overrides database values
  • Real-time Sync: Profile changes on Nostr immediately reflect in platform
  • Key Management: Clear separation of user custody (NIP07) vs platform custody (Anonymous)
  • Identity Flow: Nostr β†’ Database (Nostr profile drives platform identity)

🟠 OAuth-First Accounts (Email & GitHub):

  • Familiar Experience: Standard OAuth flow, no Nostr knowledge required
  • Transparent Web3: Background Nostr capabilities without user awareness
  • Profile Stability: OAuth profile data remains authoritative and stable
  • Identity Flow: OAuth Provider β†’ Database (Platform identity drives Nostr keys)

Universal Benefits:

  • 100% Nostr Access: All users can participate in Nostr functionality regardless of login method
  • Appropriate Custody: User-controlled keys for Nostr users, platform-managed for others
  • Future-Ready: Seamless upgrade path to NIP46 remote signing
  • Client-Side Signing: Ephemeral account users can sign Nostr events in browser
  • Complete Profile Data: All users get comprehensive profile information appropriate to their authentication method

πŸ†• Enhanced Profile Collection System

Complete NIP-01 Profile Support

/**
 * COMPREHENSIVE PROFILE COLLECTION
 * ===============================
 * 
 * πŸ”΅ NOSTR-FIRST ACCOUNTS: Complete profile from Nostr relays
 * - Fetches ALL fields from NIP-01 kind 0 events (not just basic fields)
 * - Includes: name, picture, about, nip05, lud16, banner, website, location, 
 *   github, twitter, telegram, mastodon, youtube, linkedin, pronouns, 
 *   occupation, company, skills, interests, and any other custom fields
 * - Real-time sync on every login ensures profile stays current
 * - Stored both in database (key fields) and session (complete profile)
 * 
 * 🟠 OAUTH-FIRST ACCOUNTS: Essential provider data + background Nostr
 * - GitHub: name, email, image (streamlined, no extended fields)
 * - Email: email, name (from provider)
 * - Gets ephemeral Nostr keypair for protocol participation
 * - Can access complete Nostr profile via session.user.nostrProfile if desired
 */

// Enhanced fetchNostrProfile - returns complete profile object
async function fetchNostrProfile(pubkey: string): Promise<Record<string, unknown> | null> {
  const profileEvent = await relayPool.get(
    relays, 
    { kinds: [0], authors: [pubkey] }
  )
  
  if (profileEvent?.kind === 0) {
    // Return ALL fields from Nostr profile (not filtered)
    return JSON.parse(profileEvent.content)
  }
  return null
}

// Enhanced session callback - includes complete profile data
async session({ session, token }) {
  session.user.id = token.userId
  session.user.pubkey = token.pubkey
  session.user.username = token.username
  session.user.email = token.email
  session.user.image = token.avatar
  session.user.name = token.username
  
  // Enhanced profile fields
  Object.assign(session.user, {
    nip05: token.nip05,
    lud16: token.lud16,
    banner: token.banner
  })
  
  // For Nostr-first accounts, fetch complete profile
  if (session.user.pubkey) {
    const completeNostrProfile = await fetchNostrProfile(session.user.pubkey)
    if (completeNostrProfile) {
      session.user.nostrProfile = completeNostrProfile
    }
  }
}

Profile Data Access Patterns

// In your React components
const { data: session } = useSession()

// βœ… Always available (all authentication methods)
session?.user?.name           // Display name
session?.user?.email          // Email address
session?.user?.image          // Avatar/profile picture
session?.user?.pubkey         // Nostr public key (all users get one)

// βœ… Enhanced fields (synced from appropriate source)
session?.user?.nip05          // Nostr address (from Nostr or empty)
session?.user?.lud16          // Lightning address (from Nostr or empty)
session?.user?.banner         // Banner image (from Nostr or empty)

// βœ… Complete Nostr profile (available for all users)
session?.user?.nostrProfile?.about       // Biography/description
session?.user?.nostrProfile?.website     // Personal website URL
session?.user?.nostrProfile?.location    // Geographic location
session?.user?.nostrProfile?.github      // GitHub username
session?.user?.nostrProfile?.twitter     // Twitter handle
session?.user?.nostrProfile?.telegram    // Telegram username
session?.user?.nostrProfile?.mastodon    // Mastodon address
session?.user?.nostrProfile?.youtube     // YouTube channel
session?.user?.nostrProfile?.linkedin    // LinkedIn profile
session?.user?.nostrProfile?.pronouns    // Preferred pronouns
session?.user?.nostrProfile?.occupation  // Job title/occupation
session?.user?.nostrProfile?.company     // Company/organization
session?.user?.nostrProfile?.skills      // Technical skills
session?.user?.nostrProfile?.interests   // Personal interests
// Plus any other custom fields from the user's Nostr profile

// βœ… Authentication context
session?.user?.privkey        // Private key (ephemeral accounts only)
const isNostrFirst = !session?.user?.privkey // True for NIP07 users
const canSignEvents = !!session?.user?.privkey // True for ephemeral accounts

Profile Collection Benefits

πŸ”΅ For Nostr-First Users (NIP07 & Anonymous):

  • Complete Profile Access: Every field from their Nostr profile is available
  • Real-time Sync: Profile updates on Nostr immediately reflect in the platform
  • No Data Loss: Platform preserves all custom fields and metadata
  • Source of Truth: Nostr profile always takes precedence over database values

🟠 For OAuth-First Users (Email & GitHub):

  • Clean Integration: Simple, familiar OAuth flow without Nostr complexity
  • Essential Data: Name, email, image from provider - no unnecessary fields
  • Background Nostr: Transparent access to Nostr protocol features when needed
  • Stable Profiles: OAuth provider data remains consistent and authoritative

Universal Features:

  • Type Safety: Full TypeScript support for all profile fields
  • Flexible Access: Use basic fields or dive deep into complete Nostr profiles
  • Performance: Intelligent caching of profile data with 5-minute refresh
  • Future-Proof: Ready for any new NIP-01 profile fields that emerge

πŸ“š Documentation

Comprehensive documentation is available in the docs directory:

πŸ“ License

This project is licensed under the MIT License - see the LICENSE file for details.


πŸ™ Acknowledgments

  • Vercel for Next.js and deployment platform
  • shadcn for the beautiful UI components
  • Tailwind CSS for the utility-first approach
  • Radix UI for accessible component primitives
  • Zod for runtime validation

Built with πŸ’œ by PlebDevs

From build issues to production-ready in one focused session. This platform demonstrates that proper architecture cleanup and type safety can be achieved while maintaining system functionality and providing immediate value to developers.

πŸš€ Ready to build the next generation of web applications? This platform gives you everything you need to ship fast and scale efficiently with enterprise-grade architecture and zero build errors.


No comments yet.