Skene
BLOG

JustShip SvelteKit Boilerplate Review: PLG Audit 2026

Is JustShip good for product-led growth? We audited signup, feature gating, analytics, and billing. Score: 2/10 for PLG. Clean auth with PostHog, but Stripe webhooks don't save to database and no feature gating exists.

·bySkene·LinkedIn
Summarize this article with LLMs

JustShip is a free, open-source Svelte 5 + SvelteKit boilerplate that's been gaining traction as a Next.js alternative. It uses Turso (SQLite), Drizzle ORM, Stripe, and PostHog — a modern, lightweight stack.

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: 2/10 — Great authentication foundation with PostHog integrated, but Stripe webhooks are empty shells that don't update the database, and there's zero feature gating.


The full audit

CategoryScoreKey finding
Signup flow7/10Magic link + Google OAuth, rate limited
Feature gating0/10No plans, tiers, or access control
Trial optimization0/10No trial system
Product analytics4/10PostHog integrated, pageviews only
Self-serve motion3/10Stripe checkout works, webhooks are empty
Paywall/upgrade CRO0/10No pricing page or upgrade prompts

1. Signup flow analysis

What's implemented

Email validation with Zod:

// src/lib/components/login/schema.ts
export const loginFormSchema = z.object({
  email: z.string().email()
});

Form fields: 1 (email only for magic link)

Authentication options:

  • Magic link via Postmark email
  • Google OAuth via Arctic library

Google OAuth implementation:

// src/lib/server/auth.ts
export const google = new Google(
  GOOGLE_CLIENT_ID,
  GOOGLE_CLIENT_SECRET,
  `${origin}/login/google/callback`
);

Rate limiting built in:

// src/routes/(login)/login/+page.server.ts
// 20 attempts per hour per IP (configurable via SIGNIN_IP_RATELIMIT)

Magic link tokens expire after 2 hours.

What's missing

  • No signup analytics tracking
  • No onboarding wizard
  • No "Sign Up" CTA (only "Sign In")
  • Only 1 OAuth provider (Google)

2. Feature gating audit

What's implemented

Nothing. The database schema only has basic user info:

CREATE TABLE `user` (
  `id` text PRIMARY KEY NOT NULL,
  `email` text NOT NULL,
  `email_verified` integer
);

What's missing

Everything:

  • No subscription table
  • No plan/tier system
  • No hasFeature() or canAccess() checks
  • No usage limits
  • No feature flags
  • No locked feature UI

This is a completely open application — all features are available to everyone.


3. Trial optimization

What's implemented

Nothing.

What's missing

  • No trial period tracking
  • No trial expiration logic
  • No trial emails
  • No trial countdown UI
  • No freemium model

4. Product analytics

What's implemented

PostHog with automatic pageview tracking:

// src/routes/+layout.ts
posthog.init(PUBLIC_POSTHOG_KEY, {
  api_host: `${PUBLIC_ORIGIN}/ingest`,
  capture_pageview: false,
  capture_pageleave: false
});
// src/routes/+layout.svelte
beforeNavigate(() => posthog.capture('$pageleave'));
afterNavigate(() => posthog.capture('$pageview'));

Custom API host support allows self-hosting or proxying through your domain.

What's missing

  • No posthog.identify() for user identification
  • No custom events (signup, payment, feature usage)
  • No funnel tracking
  • No cohort analysis setup

5. Self-serve motion

What's implemented

Stripe checkout session creation:

// src/routes/stripe/checkout-session/+server.ts
const session = await stripeClient.checkout.sessions.create({
  mode: 'subscription',
  payment_method_types: ['card'],
  line_items: [{
    price: priceId,
    quantity: 1
  }],
  success_url: `${event.url.origin}/?sessionId={CHECKOUT_SESSION_ID}`,
  cancel_url: `${event.url.origin}`
});

Webhook endpoint exists with all the right events:

// src/routes/stripe/webhook/+server.ts
// Events handled:
// - checkout.session.completed
// - invoice.paid
// - invoice.payment_failed
// - customer.subscription.updated
// - customer.subscription.deleted

The critical problem

Webhook handlers are empty shells. They log events but don't update the database:

case 'checkout.session.completed': {
  const session = event.data.object;
  console.log('Checkout session completed:', session.id);
  // NO DATABASE UPDATE
  break;
}

A user can pay, but nothing happens in your system.

What's missing

  • No customer portal integration
  • No billing management UI
  • No subscription storage in database
  • No payment method management

6. Paywall and upgrade CRO

What's implemented

Nothing.

What's missing

  • No pricing page
  • No pricing components
  • No plan comparison table
  • No upgrade prompts
  • No usage meters
  • No "upgrade to unlock" messaging

Database schema

The schema is minimal:

TablePurpose
userBasic user info (id, email, verified)
sessionAuth sessions
email_tokenMagic link tokens

Missing tables:

  • No subscriptions
  • No pricing_plans
  • No usage_metrics
  • No stripe_customers

What you need to add

Priority 1: Complete the Stripe webhooks (critical)

The webhook handlers exist but do nothing. You need to:

case 'checkout.session.completed': {
  const session = event.data.object;
  await db.insert(subscriptions).values({
    userId: session.client_reference_id,
    stripeCustomerId: session.customer,
    stripeSubscriptionId: session.subscription,
    status: 'active',
    priceId: session.line_items.data[0].price.id,
  });
  break;
}

Priority 2: Add subscription storage

Create a subscriptions table:

CREATE TABLE subscription (
  id TEXT PRIMARY KEY,
  user_id TEXT REFERENCES user(id),
  stripe_customer_id TEXT,
  stripe_subscription_id TEXT,
  status TEXT,
  price_id TEXT,
  current_period_end INTEGER
);

Priority 3: Build feature gating

Add plan-based access control:

function canAccess(user, feature) {
  const plan = user.subscription?.priceId;
  return PLAN_FEATURES[plan]?.includes(feature);
}

First steps to PLG

JustShip is minimal — here's the path to a working PLG stack:

Day 1: Fix the Stripe webhooks (critical)

The webhooks exist but do nothing. This is the first thing to fix:

// src/routes/stripe/webhook/+server.ts
case 'checkout.session.completed': {
  const session = event.data.object;

  // Actually save to database
  await db.insert(subscriptions).values({
    userId: session.client_reference_id,
    stripeCustomerId: session.customer as string,
    stripeSubscriptionId: session.subscription as string,
    status: 'active',
    priceId: session.line_items?.data[0]?.price?.id,
    currentPeriodEnd: new Date(session.expires_at * 1000),
  });

  break;
}

Week 1: Create the subscriptions table

Add to your Drizzle schema:

// src/lib/server/database/schema.ts
export const subscriptions = sqliteTable('subscriptions', {
  id: text('id').primaryKey(),
  userId: text('user_id').references(() => users.id),
  stripeCustomerId: text('stripe_customer_id'),
  stripeSubscriptionId: text('stripe_subscription_id'),
  status: text('status'),
  priceId: text('price_id'),
  currentPeriodEnd: integer('current_period_end'),
});

Run migration: npx drizzle-kit push:sqlite

Week 2: Add PostHog events

PostHog is already integrated. Use it:

import posthog from 'posthog-js';

// After successful magic link verification
posthog.identify(userId, { email });
posthog.capture('login_completed', { method: 'magic_link' });

// After Stripe checkout
posthog.capture('subscription_started', { plan: 'pro' });

// Track feature usage
posthog.capture('feature_used', { feature: 'api_call' });

Week 3: Build a pricing page

Create /src/routes/pricing/+page.svelte:

<script>
  const plans = [
    { name: 'Free', price: 0, features: ['Basic features', '100 API calls/month'] },
    { name: 'Pro', price: 29, priceId: 'price_xxx', features: ['All features', 'Unlimited API calls'] },
  ];
</script>

{#each plans as plan}
  <div class="plan-card">
    <h3>{plan.name}</h3>
    <p>${plan.price}/month</p>
    {#if plan.priceId}
      <a href="/stripe/checkout?priceId={plan.priceId}">Get Started</a>
    {:else}
      <a href="/login">Sign Up Free</a>
    {/if}
  </div>
{/each}

Week 4: Add feature gating

Create a subscription check:

// src/lib/server/subscription.ts
export async function getUserSubscription(userId: string) {
  return await db.query.subscriptions.findFirst({
    where: eq(subscriptions.userId, userId),
  });
}

export function isPro(subscription) {
  return subscription?.status === 'active' && subscription?.priceId === 'price_xxx';
}

When to use this template

Good fit:

  • You want SvelteKit instead of Next.js
  • You prefer SQLite/Turso over PostgreSQL
  • You need a minimal starting point
  • You'll build the billing logic yourself

Not ideal:

  • You need working payments out of the box
  • You want PLG features immediately
  • You need feature gating or trials

Conclusion

JustShip scores 2/10 for PLG readiness. It has excellent auth infrastructure (magic link + Google OAuth with rate limiting) and PostHog is integrated, but the Stripe integration is incomplete — webhooks don't update the database.

The template is honest about being minimal. It's a clean starting point for SvelteKit, but you'll need to build the entire monetization layer yourself.

Best for: Developers who want a lightweight SvelteKit foundation and prefer to build their own billing system.


Frequently asked questions

Is JustShip really free?

Yes, it's fully open source under MIT license. All features are included — there's no paid tier.

Why use Turso instead of PostgreSQL?

Turso is SQLite at the edge — simpler to set up, excellent local development experience, and generous free tier. For most SaaS apps, it's sufficient.

Does the Stripe integration work?

Partially. Checkout sessions are created correctly and users can pay. But the webhook handlers don't save anything to the database, so you won't know who paid.

How does it compare to Next.js starters?

It's simpler and more minimal. The auth is solid, but you get less out of the box for billing and PLG. If you want working payments immediately, BoxyHQ or Launch MVP are better choices.


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.