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
| Category | Score | Key finding |
|---|---|---|
| Signup flow | 7/10 | Magic link + Google OAuth, rate limited |
| Feature gating | 0/10 | No plans, tiers, or access control |
| Trial optimization | 0/10 | No trial system |
| Product analytics | 4/10 | PostHog integrated, pageviews only |
| Self-serve motion | 3/10 | Stripe checkout works, webhooks are empty |
| Paywall/upgrade CRO | 0/10 | No 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()orcanAccess()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:
| Table | Purpose |
|---|---|
user | Basic user info (id, email, verified) |
session | Auth sessions |
email_token | Magic 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.