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
| Category | Score | Key finding |
|---|---|---|
| Signup flow | 6/10 | 2 fields, GitHub OAuth, magic link — but no analytics |
| Feature gating | 1/10 | No paywalls, no usage limits, no tier-based access |
| Trial optimization | 3/10 | Trial logic exists but disabled by default |
| Product analytics | 0/10 | Zero tracking — no SDK, no events, no identification |
| Self-serve motion | 7/10 | Full Stripe checkout, customer portal integration |
| Paywall/upgrade CRO | 0/10 | No 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, orLockedcomponents - No
hasFeature()orcanAccess()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-jsor similar in package.json
No tracking calls:
- No
.track(),.capture(), orlogEvent()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
invitefunctionality - 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:
| Table | Purpose |
|---|---|
users | User profiles (full_name, avatar_url, billing_address) |
customers | Maps user UUID to Stripe customer ID |
products | Synced from Stripe via webhooks |
prices | Supports recurring and one-time pricing |
subscriptions | Full subscription lifecycle tracking |
Missing tables for PLG:
- No
user_onboardingoractivation_state - No
usage_metricsorfeature_usage - No
teamsorteam_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
- Enable trials: Set
TRIAL_PERIOD_DAYS = 14in/utils/supabase/admin.ts - Add trial emails via Resend or SendGrid
- 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
- Install PostHog:
npm install posthog-js - Initialize in your layout with your project key
- Add
posthog.identify(userId)after login - Track these 4 events minimum:
signup_completedpricing_page_viewedcheckout_startedsubscription_activated
Week 2: Define your activation metric
- Identify your "aha moment" — what action correlates with retention?
- Create an event for it:
posthog.capture('aha_moment_reached') - Build a dashboard showing signup → aha moment conversion
- Set a target (e.g., 40% of signups reach aha in 7 days)
Week 3: Add a trial
- Set
TRIAL_PERIOD_DAYS = 14in your config - Create 3 emails: welcome, day 7 reminder, day 13 warning
- Add a trial countdown banner in your dashboard
- Track
trial_startedandtrial_convertedevents
Week 4: Build your first upgrade prompt
- Pick one feature to gate behind a paid plan
- Create a simple
<UpgradePrompt>component - Show it when free users try to access the feature
- Track
upgrade_prompt_shownandupgrade_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
| Component | File | Lines |
|---|---|---|
| Signup form | /components/ui/AuthForms/Signup.tsx | 1-83 |
| OAuth | /components/ui/AuthForms/OauthSignIn.tsx | 1-54 |
| Email validation | /utils/auth-helpers/server.ts | 9-12 |
| Trial calculation | /utils/helpers.ts | 52-69 |
| Trial config | /utils/supabase/admin.ts | 11 |
| Checkout flow | /utils/stripe/server.ts | 21-120 |
| Pricing component | /components/ui/Pricing/Pricing.tsx | 1-204 |
| Webhooks | /app/api/webhooks/route.ts | 1-96 |
| Database schema | /schema.sql | 1-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.