Skene
BLOG

SvelteKit Supabase SaaS Starter Review: PLG Audit 2026

Is the CMSaasStarter template good for product-led growth? We audited signup, feature gating, analytics, and billing. Score: 3.7/10 for PLG. Best Stripe integration of any SvelteKit starter, but no analytics or feature gating.

·bySkene·LinkedIn
Summarize this article with LLMs

The CMSaasStarter is a SvelteKit + Supabase SaaS template with marketing pages, blog, subscriptions, auth, and a user dashboard. It's one of the more complete SvelteKit options available.

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.7/10 — Has the best Stripe integration of any SvelteKit starter we've reviewed (checkout + billing portal), but zero analytics and no feature gating.


The full audit

CategoryScoreKey finding
Signup flow7/10Supabase Auth UI with GitHub OAuth
Feature gating2/10Static pricing table, no runtime checks
Trial optimization1/10Stripe trial status awareness only
Product analytics0/10Documentation only, nothing installed
Self-serve motion9/10Full Stripe checkout + billing portal
Paywall/upgrade CRO3/10Pricing page exists, no in-app prompts

1. Signup flow analysis

What's implemented

Supabase Auth UI component with GitHub OAuth:

// src/routes/(marketing)/login/login_config.ts
export const oauthProviders = ["github"] as Provider[]
// src/routes/(marketing)/login/sign_up/+page.svelte
<Auth
  supabaseClient={data.supabase}
  view="sign_up"
  redirectTo={`${data.url}/auth/callback`}
  providers={oauthProviders}
  socialLayout="horizontal"
/>

The Supabase Auth UI handles:

  • Email + password signup
  • Email verification
  • Password reset
  • OAuth callbacks

What's missing

  • Only 1 OAuth provider (GitHub) — no Google, Microsoft
  • No custom signup tracking
  • No form validation messaging customization
  • No onboarding flow after signup

2. Feature gating audit

What's implemented

Static pricing table with Free/Pro/Enterprise tiers:

// src/routes/(marketing)/pricing/pricing_plans.ts
{
  id: "pro",
  name: "Pro",
  description: "A plan to test the purchase experience...",
  price: "$5",
  stripe_price_id: "price_1NkdZCHMjzZ8mGZnRSjUm4yA",
  features: [
    "Everything in Free",
    "Support us with fake money",
    "Test the purchase experience",
  ],
}

Feature comparison with limits:

{
  name: "Feature 3",
  freeIncluded: true,
  freeString: "3",        // Free tier: 3 uses
  proIncluded: true,
  proString: "Unlimited", // Pro tier: unlimited
}

What's missing

No runtime enforcement. The pricing table shows limits, but the code never checks them:

  • No hasFeature() or canAccess() functions
  • No middleware that gates routes by plan
  • No usage counting or limit enforcement
  • No locked UI components
  • No "upgrade to access" redirects

All features are available to everyone regardless of plan.


3. Trial optimization

What's implemented

Stripe trial status awareness in subscription handling:

// src/routes/(admin)/account/subscription_helpers.server.ts
const primaryStripeSubscription = stripeSubscriptions.data.find((x) => {
  return (
    x.status === "active" ||
    x.status === "trialing" ||  // Trial support
    x.status === "past_due"
  )
})

What's missing

  • No trial period initialization
  • No trial_days parameter in checkout
  • No trial expiration warnings in UI
  • No trial reminder emails
  • No "upgrade before trial ends" prompts
  • No trial-to-paid conversion tracking

4. Product analytics

What's implemented

Documentation only. The analytics_docs.md file explains how to add PostHog or Google Analytics:

### PostHog
- Install PostHog JS Library: `npm install posthog-js`
- Set up in `src/routes/+layout.svelte`

What's missing

Nothing is actually installed:

  • No analytics package in package.json
  • No tracking calls in code
  • No user identification
  • No conversion tracking
  • No funnel analysis

You cannot measure signup-to-paid conversion without adding analytics yourself.


5. Self-serve motion

What's implemented

This is where the template shines. Full Stripe checkout:

// src/routes/(admin)/account/subscribe/[slug]/+page.server.ts
const stripeSession = await stripe.checkout.sessions.create({
  line_items: [{ price: params.slug, quantity: 1 }],
  customer: customerId,
  mode: "subscription",
  success_url: `${url.origin}/account`,
  cancel_url: `${url.origin}/account/billing`,
})

Automatic Stripe customer creation:

// Uses getOrCreateCustomerId() helper
// Creates Stripe customer on first checkout

Billing portal for self-serve management:

// src/routes/(admin)/account/(menu)/billing/manage/+page.server.ts
const portalSession = await stripe.billingPortal.sessions.create({
  customer: customerId,
  return_url: `${url.origin}/account/billing`,
})

What users can do via portal:

  • Update payment methods
  • View invoices
  • Upgrade/downgrade plans
  • Cancel subscription

What's missing

  • No custom checkout page (redirects to Stripe)
  • No dunning/failed payment recovery
  • No usage-based pricing

6. Paywall and upgrade CRO

What's implemented

Pricing module with current plan indicator:

// src/routes/(marketing)/pricing/pricing_module.svelte
{#if plan.id === currentPlanId}
  <div class="btn btn-outline btn-success">Current Plan</div>
{:else}
  <a href={"/account/subscribe/" + plan?.stripe_price_id}>
    <button class="btn btn-primary">{callToAction}</button>
  </a>
{/if}

Feature comparison table showing what's included in each tier.

What's missing

  • No upgrade prompts inside the app
  • No usage limit warnings
  • No "upgrade to unlock" overlays
  • No annual/monthly pricing toggle
  • No testimonials or social proof
  • No FAQ on pricing page

Database schema

Uses Supabase with minimal schema:

TablePurpose
profilesUser metadata
stripe_customersStripe customer ID mapping
contact_requestsContact form submissions

Missing tables:

  • No trial tracking fields
  • No feature usage metrics
  • No subscription metadata (stored in Stripe only)

What you need to add

Priority 1: Analytics

Install PostHog and track key events:

posthog.capture('signup_completed');
posthog.capture('pricing_page_viewed');
posthog.capture('checkout_started', { plan: 'pro' });
posthog.capture('checkout_completed', { plan: 'pro' });

Priority 2: Feature gating

Create a subscription check helper:

async function getSubscription(userId: string) {
  const customer = await getStripeCustomer(userId);
  const subscriptions = await stripe.subscriptions.list({
    customer: customer.id,
    status: 'active'
  });
  return subscriptions.data[0];
}

function hasFeature(subscription, feature) {
  const plan = getPlanFromPriceId(subscription?.price_id);
  return PLAN_FEATURES[plan]?.includes(feature);
}

Priority 3: Trial system

Add trial period to checkout:

const stripeSession = await stripe.checkout.sessions.create({
  // ... existing config
  subscription_data: {
    trial_period_days: 14,
  },
});

First steps to PLG

CMSaasStarter has the best billing of any SvelteKit template. Add the activation layer:

Week 1: Add analytics

The docs mention PostHog but nothing is installed. Fix that:

npm install posthog-js
<!-- src/routes/+layout.svelte -->
<script>
  import posthog from 'posthog-js';
  import { browser } from '$app/environment';

  if (browser) {
    posthog.init('your_key', { api_host: 'https://app.posthog.com' });
  }
</script>

Track key events:

  • signup_completed
  • pricing_page_viewed
  • checkout_started (with plan name)
  • subscription_activated
  • feature_used (for adoption tracking)

Week 2: Add subscription checking

Create a helper to check plans:

// src/lib/subscription.ts
import { stripe } from '$lib/stripe';

export async function getSubscription(customerId: string) {
  const subscriptions = await stripe.subscriptions.list({
    customer: customerId,
    status: 'active',
  });
  return subscriptions.data[0];
}

export function getPlanFromPriceId(priceId: string) {
  const plans = {
    'price_free': 'free',
    'price_xxx': 'pro',
    'price_yyy': 'enterprise',
  };
  return plans[priceId] || 'free';
}

Week 3: Gate features

Add plan-based access in your routes:

<!-- src/routes/(admin)/pro-feature/+page.svelte -->
<script>
  export let data;
  $: isPro = data.plan === 'pro' || data.plan === 'enterprise';
</script>

{#if isPro}
  <ProFeatureContent />
{:else}
  <UpgradePrompt feature="Pro Feature" />
{/if}

Week 4: Add trial with countdown

  1. Modify checkout to add 14-day trial
  2. Create a trial banner component:
<!-- src/lib/components/TrialBanner.svelte -->
<script>
  export let trialEndsAt;
  $: daysLeft = Math.ceil((new Date(trialEndsAt) - new Date()) / (1000 * 60 * 60 * 24));
</script>

{#if daysLeft > 0 && daysLeft <= 14}
  <div class="alert alert-warning">
    <span>Your trial ends in {daysLeft} days.</span>
    <a href="/account/billing">Upgrade now</a>
  </div>
{/if}

Key metrics to track

Once analytics is added, measure:

  • Signup → first value action (define your "aha moment")
  • Free → trial conversion rate
  • Trial → paid conversion rate
  • Time to upgrade (how long do users stay on free?)

When to use this template

Good fit:

  • You want SvelteKit + Supabase
  • You need working Stripe checkout and billing portal
  • You prefer Supabase Auth over custom auth

Not ideal:

  • You need feature gating out of the box
  • You want analytics pre-configured
  • You need trial optimization

Conclusion

The CMSaasStarter scores 3.7/10 for PLG readiness. It has the strongest self-serve billing of any SvelteKit template — full Stripe checkout with customer creation and billing portal access.

But it's missing the PLG fundamentals: no analytics to measure conversion, no feature gating to drive upgrades, and no trial system to reduce friction. The pricing table shows limits that aren't enforced anywhere in the code.

Best for: Teams who want solid Stripe billing with SvelteKit and will add analytics and feature gating themselves.


Frequently asked questions

How does this compare to JustShip?

CMSaasStarter has better Stripe integration — the webhooks actually update user state, and there's a billing portal. JustShip's Stripe webhooks are empty shells. But JustShip has PostHog integrated, while this template has no analytics.

Does it support multiple OAuth providers?

Only GitHub is configured by default. You can add Google, Microsoft, etc. through Supabase Auth settings, but you'll need to update the oauthProviders config.

Can I use it for a freemium product?

Not without significant work. The pricing page shows Free/Pro/Enterprise, but there's no code that restricts features based on plan. You'll need to build feature gating yourself.

What's the best SvelteKit starter for PLG?

None of them are particularly strong for PLG. CMSaasStarter has the best billing, but you'll need to add analytics and feature gating regardless of which template you choose.


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.

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