Why You Need the Freemium Model?

If you directly sell a course priced at NT$3,999 online without offering any preview, the likelihood of customers making a purchase approaches zero.
This is why all streaming platforms and SaaS products now adopt the Freemium (Free + Premium) model.
By providing high-quality free content first to build trust, when users think "Wow, the free content is already so valuable, the paid version must be even better," they'll willingly pull out their magic credit cards.

In Vibe Tutor, how do we implement this content firewall that automatically distinguishes between "free preview" and "paid lock"?


First Line of Defense: Route Classification

First, the system needs to know "what type of course" the user is currently viewing.
In src/app/courses/[...slug]/page.tsx, we determine this by parsing the URL's slug array.

const courseId = slug[0];  // e.g., "vibe-tutor-web" or "line-bot-basics"
const chapterSlug = slug.length > 1 ? slug[1] : null; // e.g., "01-intro"

Next, we define a course classification matrix in the code:

// Always free-to-experience courses
const freeCourses = ["line-bot-basics", "gas-sheet-automation", "react-tailwind-portfolio", "github-vercel-deploy", "postgres-sql-basics"];

// Beginner courses (first two chapters free)
const beginnerCourses = ["coding-101", "github-basics", "langchain-rag-basics", "nextjs-app-router", "api-postman-basics", "python-data-analysis"];

Using these classifications, we can write flexible logic:

let isFreePreview = false;

if (freeCourses.includes(courseId)) {
  // 1. Fully free projects, green light
  isFreePreview = true;
} else if (chapterSlug) {
  if (beginnerCourses.includes(courseId)) {
    // 2. Beginner courses: allow if filename starts with "01-" or "02-"
    if (chapterSlug.startsWith("01-") || chapterSlug.startsWith("02-")) {
      isFreePreview = true;
    }
  } else {
    // 3. Premium commercial projects: only allow Chapter 01 for free preview
    if (chapterSlug.startsWith("01-")) {
      isFreePreview = true;
    }
  }
}

With this isFreePreview variable, we solve the "which chapters are free" problem. But if a chapter isn't free, we need to further check the user's "database permissions."


Second Line of Defense: Database Permission Verification

If isFreePreview is false, it means this is a paid article.
The system shouldn't block access immediately—the user might have already purchased it! We need to check the Supabase database.

let hasAccess = isFreePreview;

if (!hasAccess) {
  // 1. Get logged-in user info
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (user) {
    // 2. Query all purchase records for this user
    const { data: purchases } = await supabase
      .from("vt_purchases")
      .select("item_id")
      .eq("user_id", user.id);
    
    if (purchases) {
      const itemIds = purchases.map(p => p.item_id);
      
      // 3. Ultimate permission check
      if (
        itemIds.includes("all-courses") ||       // Legacy VIP tag
        itemIds.includes("vip-all-access") ||    // Ultimate NT$9,999 VIP pass
        itemIds.includes(courseId) ||            // Single-course text license
        itemIds.includes(`${courseId}-source`)   // Single-course with source code
      ) {
        hasAccess = true; // Permission granted!
      }
    }
  }
}

Notably, this code runs in a Server Component, meaning:

  1. Absolute Security: Hackers can't bypass access by modifying client-side JavaScript variables (e.g., changing hasAccess to true). If the server denies permission, no HTML is even prepared for you.
  2. Zero Latency: Database queries happen instantly on the server side, and results are returned directly—no annoying "Loading spinner" for users.

Visual and Psychological Tricks: Blur Effect

When hasAccess is false, the brute-force approach would be to show a blank screen saying "Please pay." But that's boring and doesn't drive sales.
Instead, we use a more enticing tactic: Let them see it but not touch it.

We still render the article but add Tailwind's blur effect and height restriction to the container:

<article className={`prose max-w-none ${!hasAccess ? 'max-h-[250px] overflow-hidden blur-[6px] select-none pointer-events-none opacity-40' : ''}`}>
  <div dangerouslySetInnerHTML={{ __html: contentHtml }} />
</article>

This shows a shadow of the first few paragraphs, gradually blurred.
Then, we overlay a large, beautifully designed "Unlock Full Tutorial" prompt:

{!hasAccess && (
  <div className="absolute top-0 left-0 w-full h-[600px] flex flex-col items-center pt-[100px] px-6">
    <div className="glass p-8 rounded-3xl border border-primary/20 ...">
      <Lock className="w-8 h-8 text-primary" />
      <h3 className="text-2xl font-bold">Unlock Full Tutorial</h3>
      <p>This chapter is premium. Join now to unlock 5000+ words of in-depth analysis...</p>
      <Link href="/pricing" className="...">View Pricing Plans</Link>
    </div>
  </div>
)}

This "tease-and-trap" design leverages Loss Aversion and Curiosity Gap from psychology. Users see the outline but hit a blur at the most exciting part—dramatically boosting conversions for the "View Pricing Plans" button!

Now that you've learned how to build an impenetrable content firewall, the next step is user account creation. In the next chapter, we'll unveil the mysteries of Supabase authentication.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!