Skene
BLOG

Vercel Next.js Subscription Payments Template Review: PLG Audit 2026

Is the Vercel Next.js Subscription Payments template good for SaaS? We audited signup flow, Stripe integration, analytics, and feature gating. Score: 3/10 for PLG. See what's missing and how to fix it.

·bySkene·LinkedIn
Summarize this article with LLMs

The Vercel Next.js Subscription Payments template is the most starred Supabase starter on GitHub with over 7,600 stars. It's a go-to choice for developers building SaaS applications with Next.js, Supabase, and Stripe.

We ran it through a comprehensive PLG skills audit — analyzing signup flow CRO, feature gating, trial optimization, product analytics, self-serve motion, and paywall design.

Overall PLG Score: 3/10 — Solid billing infrastructure, but significant gaps in activation, analytics, and feature gating.


The full audit

CategoryScoreKey finding
Signup flow6/102 fields, GitHub OAuth, magic link — but no analytics
Feature gating1/10No paywalls, no usage limits, no tier-based access
Trial optimization3/10Trial logic exists but disabled by default
Product analytics0/10Zero tracking — no SDK, no events, no identification
Self-serve motion7/10Full Stripe checkout, customer portal integration
Paywall/upgrade CRO0/10No upgrade prompts, no locked features

1. Signup flow analysis

What's implemented

The signup form at /components/ui/AuthForms/Signup.tsx is minimal:

// Lines 28-66
<form noValidate={true}>
  <input type="email" name="email" />
  <input type="password" name="password" />
  <Button type="submit">Sign up</Button>
</form>

Form fields: 2 required (email, password), 0 optional — good for conversion.

Authentication options:

  • Email + password (/components/ui/AuthForms/Signup.tsx)
  • Magic link (/components/ui/AuthForms/EmailSignIn.tsx)
  • GitHub OAuth (/components/ui/AuthForms/OauthSignIn.tsx)

The OAuth implementation is extensible — adding Google or other providers requires minimal code.

What's missing

No form validation library. The only validation is a regex in /utils/auth-helpers/server.ts:

// Lines 9-12
function isValidEmail(email: string) {
  var regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
  return regex.test(email);
}

No Zod, Yup, or client-side validation. The form has noValidate={true}, so validation happens only server-side.

No signup analytics. Zero tracking events on:

  • Signup page viewed
  • Signup started
  • Signup completed
  • Email verified

Without this data, you can't identify where users drop off.


2. Feature gating audit

What's implemented

The database schema includes subscription status tracking:

-- /schema.sql Line 105
create type subscription_status as enum (
  'trialing', 'active', 'canceled',
  'incomplete', 'incomplete_expired',
  'past_due', 'unpaid', 'paused'
);

Subscription data is accessible via getSubscription() in /utils/supabase/queries.ts.

What's missing

No feature-level gating. The subscription status is used only to display the current plan name — not to gate features:

// /components/ui/AccountForms/CustomerPortalForm.tsx Lines 50-53
description={
  subscription
    ? `You are currently on the ${subscription?.prices?.products?.name} plan.`
    : 'You are not currently subscribed to any plan.'
}

Missing components:

  • No Paywall, Upgrade, or Locked components
  • No hasFeature() or canAccess() permission checks
  • No usage limits (limit, quota, usage)
  • No feature flags (no LaunchDarkly, Statsig, or custom implementation)

The entire app is accessible to all authenticated users. Middleware in /middleware.ts only checks authentication, not subscription tier.


3. Trial optimization

What's implemented

Trial calculation exists in /utils/helpers.ts:

// Lines 52-69
export const calculateTrialEndUnixTimestamp = (
  trialPeriodDays: number | null | undefined
) => {
  if (trialPeriodDays === null || trialPeriodDays === undefined || trialPeriodDays < 2) {
    return undefined;
  }
  const currentDate = new Date();
  const trialEnd = new Date(
    currentDate.getTime() + (trialPeriodDays + 1) * 24 * 60 * 60 * 1000
  );
  return Math.floor(trialEnd.getTime() / 1000);
};

Trial period is passed to Stripe during checkout in /utils/stripe/server.ts:

// Lines 67-78
if (price.type === 'recurring') {
  params = {
    ...params,
    mode: 'subscription',
    subscription_data: {
      trial_end: calculateTrialEndUnixTimestamp(price.trial_period_days)
    }
  };
}

What's missing

Trial is disabled by default:

// /utils/supabase/admin.ts Line 11
const TRIAL_PERIOD_DAYS = 0;

No trial-related emails:

  • No trial welcome email
  • No trial reminder (3 days, 1 day before expiry)
  • No trial expired email

All trial emails must be configured in Stripe or implemented separately.

No trial extension logic. Users cannot request more time.


4. Product analytics

What's implemented

Nothing. Zero analytics infrastructure.

What's missing

No analytics SDK:

  • No Mixpanel, Amplitude, PostHog, Segment, or GA4
  • No npm install posthog-js or similar in package.json

No tracking calls:

  • No .track(), .capture(), or logEvent() anywhere in the codebase
  • No user identification via .identify() or .setUserId()

No tracking plan:

  • No events.ts, analytics.ts, or event taxonomy documentation

The only logging is console.log statements for debugging:

// /utils/stripe/server.ts Lines 67-69
console.log(
  'Trial end:',
  calculateTrialEndUnixTimestamp(price.trial_period_days)
);

Impact: You cannot measure:

  • Signup-to-paid conversion rate
  • Time-to-first-payment
  • Feature adoption
  • Churn predictors

5. Self-serve motion

What's implemented

The checkout flow is fully self-serve via Stripe:

// /utils/stripe/server.ts Lines 21-120
export async function checkoutWithStripe(
  price: Price,
  redirectPath: string = '/account'
): Promise<CheckoutResponse> {
  let params: Stripe.Checkout.SessionCreateParams = {
    allow_promotion_codes: true,
    billing_address_collection: 'required',
    customer,
    line_items: [{ price: price.id, quantity: 1 }],
    cancel_url: getURL(),
    success_url: getURL(redirectPath)
  };
  const session = await stripe.checkout.sessions.create(params);
  return { sessionId: session.id };
}

Features:

  • Promotion codes enabled
  • Billing address collection
  • Customer creation/retrieval
  • Trial period support

Billing management via Stripe Customer Portal:

// /components/ui/AccountForms/CustomerPortalForm.tsx Lines 40-45
const handleStripePortalRequest = async () => {
  const redirectUrl = await createStripePortal(currentPath);
  return router.push(redirectUrl);
};

Users can upgrade, downgrade, update payment method, and cancel — all self-serve.

What's missing

No team management:

  • No invite functionality
  • No seats or team-based pricing
  • No role-based access control

No Contact Sales flow:

  • All plans are self-serve only
  • No enterprise inquiry form

6. Paywall & upgrade CRO

What's implemented

The pricing page at /components/ui/Pricing/Pricing.tsx displays products from Stripe:

// Lines 185-193
<Button
  variant="slim"
  onClick={() => handleStripeCheckout(price)}
>
  {subscription ? 'Manage' : 'Subscribe'}
</Button>

What's missing

No paywall components:

  • No "Upgrade to unlock" messages
  • No feature lock icons
  • No usage limit displays

No upgrade triggers:

  • No contextual prompts based on user behavior
  • No "You've used X of Y" messaging
  • No trial expiry countdown

The only upgrade path is the pricing page.


Database schema

The schema in /schema.sql is well-designed for subscriptions:

TablePurpose
usersUser profiles (full_name, avatar_url, billing_address)
customersMaps user UUID to Stripe customer ID
productsSynced from Stripe via webhooks
pricesSupports recurring and one-time pricing
subscriptionsFull subscription lifecycle tracking

Missing tables for PLG:

  • No user_onboarding or activation_state
  • No usage_metrics or feature_usage
  • No teams or team_members

Webhook integration

The webhook handler at /app/api/webhooks/route.ts handles:

const relevantEvents = new Set([
  'product.created', 'product.updated', 'product.deleted',
  'price.created', 'price.updated', 'price.deleted',
  'checkout.session.completed',
  'customer.subscription.created',
  'customer.subscription.updated',
  'customer.subscription.deleted'
]);

This keeps the database in sync with Stripe automatically.


What you need to add

Priority 1: Analytics (critical)

Integrate PostHog, Mixpanel, or Amplitude. Track at minimum:

// Signup funnel
track('signup_page_viewed')
track('signup_started', { method: 'email' | 'oauth' })
track('signup_completed')
track('email_verified')

// Monetization funnel
track('pricing_page_viewed')
track('checkout_started', { plan: 'pro', price: 29 })
track('checkout_completed', { plan: 'pro', price: 29 })

Priority 2: Feature gating

Create a permission system:

// Example implementation
function hasFeature(user, feature) {
  const plan = user.subscription?.prices?.products?.name;
  return PLAN_FEATURES[plan]?.includes(feature);
}

// Usage in components
{hasFeature(user, 'advanced_analytics') ? (
  <AnalyticsDashboard />
) : (
  <UpgradePrompt feature="advanced_analytics" />
)}

Priority 3: Trial activation

  1. Enable trials: Set TRIAL_PERIOD_DAYS = 14 in /utils/supabase/admin.ts
  2. Add trial emails via Resend or SendGrid
  3. Track trial activation events

Priority 4: Onboarding

Add a post-signup flow that guides users to their first "aha moment."


First steps to PLG

If you're using this template and want to add product-led growth, here's how to start:

Week 1: Add analytics

  1. Install PostHog: npm install posthog-js
  2. Initialize in your layout with your project key
  3. Add posthog.identify(userId) after login
  4. Track these 4 events minimum:
    • signup_completed
    • pricing_page_viewed
    • checkout_started
    • subscription_activated

Week 2: Define your activation metric

  1. Identify your "aha moment" — what action correlates with retention?
  2. Create an event for it: posthog.capture('aha_moment_reached')
  3. Build a dashboard showing signup → aha moment conversion
  4. Set a target (e.g., 40% of signups reach aha in 7 days)

Week 3: Add a trial

  1. Set TRIAL_PERIOD_DAYS = 14 in your config
  2. Create 3 emails: welcome, day 7 reminder, day 13 warning
  3. Add a trial countdown banner in your dashboard
  4. Track trial_started and trial_converted events

Week 4: Build your first upgrade prompt

  1. Pick one feature to gate behind a paid plan
  2. Create a simple <UpgradePrompt> component
  3. Show it when free users try to access the feature
  4. Track upgrade_prompt_shown and upgrade_prompt_clicked

Ongoing: Measure and iterate

  • Review your signup → trial → paid funnel weekly
  • A/B test your upgrade prompts
  • Survey churned users to understand why they left
  • Gradually gate more features as you learn what drives upgrades

When to use this template

Good fit:

  • You need Stripe subscription billing quickly
  • Your product is payment-focused (the billing is the product)
  • You'll add analytics and feature gating yourself

Not ideal:

  • You need PLG infrastructure out of the box
  • You want to track activation and retention
  • You need team/seat-based billing

Conclusion

The Vercel Next.js Subscription Payments template scores 3/10 for PLG readiness. It's excellent for what it's designed to do: Stripe billing with Supabase auth.

But PLG requires more than billing:

  • Analytics to measure activation (0/10 currently)
  • Feature gating to drive upgrades (1/10 currently)
  • Onboarding to guide users to value (not present)

The self-serve motion is strong (7/10), so if you add the activation layer, you'll have a solid foundation.


Appendix: Key file locations

ComponentFileLines
Signup form/components/ui/AuthForms/Signup.tsx1-83
OAuth/components/ui/AuthForms/OauthSignIn.tsx1-54
Email validation/utils/auth-helpers/server.ts9-12
Trial calculation/utils/helpers.ts52-69
Trial config/utils/supabase/admin.ts11
Checkout flow/utils/stripe/server.ts21-120
Pricing component/components/ui/Pricing/Pricing.tsx1-204
Webhooks/app/api/webhooks/route.ts1-96
Database schema/schema.sql1-145

This analysis was performed using PLG Skills, an open-source framework for product-led growth audits. Skills used: signup-flow-cro, feature-gating, trial-optimization, product-analytics, self-serve-motion, paywall-upgrade-cro.


Frequently asked questions

Is the Vercel Next.js Subscription Payments template good for SaaS?

It depends on your needs. The template excels at Stripe subscription billing (7/10 self-serve) but lacks PLG infrastructure. It has zero analytics, no feature gating, and no onboarding. Use it if billing is your main concern and you'll add the activation layer yourself.

Does the Vercel subscription template include analytics?

No. The template has zero analytics infrastructure — no Mixpanel, Amplitude, PostHog, Segment, or GA4. There are no .track() or .identify() calls anywhere in the codebase. You'll need to add analytics yourself to measure signup-to-paid conversion.

How do I add feature gating to the Vercel Next.js template?

Create a permission system that checks user subscription status against feature access. The template already tracks subscription status (active, trialing, canceled) in the database — you need to add hasFeature() checks in your components and create UpgradePrompt components for locked features.

Does this template support free trials?

Trial logic exists but is disabled by default. The TRIAL_PERIOD_DAYS constant is set to 0 in /utils/supabase/admin.ts. Set it to 14 (or your preferred trial length) to enable trials. Note: no trial emails are configured — you'll need to set those up via Stripe or a service like Resend.

What's the best alternative to this template for PLG?

If you need PLG infrastructure out of the box, consider Launch MVP (5.9/10 PLG score) which includes a working trial system, onboarding tour component, and PostHog analytics configuration. See our Launch MVP PLG analysis for details.

Can I use this template for team/seat-based pricing?

Not without significant modifications. The template has no team management, invite functionality, or seat-based pricing. It's designed for single-user subscriptions. For team billing, you'll need to add teams and team_members tables and modify the Stripe integration.

Done with this article? Explore more ways to ship real PLG.