Skip to content

PLG Readiness Audit: Saasfly — 4/10 #148

Description

@teekoo5

PLG Readiness Audit: Saasfly — 4/10

We audited this repo for product-led growth readiness using the open-source PLG Skills framework. Here's what we found across 6 categories.

Overall: 4/10 — Clean monorepo architecture with full Stripe checkout and PostHog. But the critical flaw: feature limits are defined in the UI and database, then not enforced in code. The gating logic is literally commented out.

Category Score Finding
Signup flow 7/10 Magic link + GitHub OAuth, multi-language (EN/JP/KO/ZH)
Feature gating 2/10 Plan limits defined but NOT enforced
Trial optimization 0/10 No trial system
Product analytics 3/10 PostHog integrated, pageviews only
Self-serve motion 8/10 Full Stripe checkout + billing portal
Paywall/upgrade CRO 4/10 Clean pricing with annual/monthly toggle

Critical: feature limits exist in UI but not in code

The pricing data defines clear limits per plan:

// apps/nextjs/src/config/price/price-data.ts
{ id: "starter", benefits: ["Up to 1 cluster per month"] },
{ id: "pro", benefits: ["Up to 3 clusters per month"] },
{ id: "business", benefits: ["Up to 10 clusters per month"] },

The database has a SubscriptionPlan enum:

// packages/db/prisma/schema.prisma
enum SubscriptionPlan { FREE  PRO  BUSINESS }
model Customer { plan SubscriptionPlan? }

But the cluster creation code ignores all of this:

// packages/api/src/router/k8s.ts
createCluster: protectedProcedure
  .input(k8sClusterCreateSchema)
  .mutation(async ({ ctx, input }) => {
    // Creates cluster WITHOUT checking plan limits
    const newCluster = await db
      .insertInto("K8sClusterConfig")
      .values({
        name: input.name,
        plan: SubscriptionPlan.FREE, // Always FREE, no limit check
        authUserId: userId,
      })
      .returning("id")
      .executeTakeFirst();
    return { id: newCluster.id, success: true };
  }),

And the UI upgrade toast is commented out:

// apps/nextjs/src/components/k8s/cluster-create-button.tsx
// COMMENTED OUT:
// if (response.status === 402) {
//   return toast({
//     title: "Limit of 1 cluster reached.",
//     description: "Please upgrade to the PROD plan.",
//     variant: "destructive",
//   });
// }

A FREE user can create unlimited clusters. The pricing page promises limits that don't exist.

Self-serve billing: solid

The Stripe integration is well-implemented with smart routing — existing subscribers go to billing portal, new users go to checkout:

// packages/api/src/router/stripe.ts
if (customer && customer.plan !== "FREE") {
  // Existing subscriber → billing portal
  const session = await stripe.billingPortal.sessions.create({ ... });
} else {
  // New subscriber → checkout
  const session = await stripe.checkout.sessions.create({ mode: "subscription", ... });
}

Webhooks properly update the database on checkout.session.completed and invoice.payment_succeeded.

Analytics: PostHog with pageviews only

PostHog is initialized with custom pageview tracking in apps/nextjs/src/config/providers.tsx. But searching the entire codebase for posthog.capture returns exactly 1 result — the pageview. No signup, checkout, cluster creation, or limit-reached events.

What you'd need to add

1. Enforce limits in the API (critical). The plan and limits are defined — just add the check:

createCluster: protectedProcedure.mutation(async ({ ctx }) => {
  const customer = await getCustomer(ctx.userId);
  const clusterCount = await countClusters(ctx.userId);
  const limits = { FREE: 1, PRO: 3, BUSINESS: 10 };
  if (clusterCount >= limits[customer.plan || 'FREE']) {
    throw new TRPCError({
      code: 'FORBIDDEN',
      message: 'Cluster limit reached. Upgrade to create more.',
    });
  }
  // ... create cluster
});

2. Uncomment the upgrade toast. The component exists. Uncomment it and wire up the error handling.

3. Add PostHog custom events:

posthog.capture('signup_completed', { method: 'magic_link' });
posthog.capture('cluster_created', { plan: customer.plan, count: clusterCount });
posthog.capture('limit_reached', { feature: 'clusters', plan: customer.plan });
posthog.capture('plan_upgraded', { from: 'FREE', to: 'PRO' });

4. Add trial support. Stripe integration is solid — add subscription_data: { trial_period_days: 14 } to the checkout session.


Full analysis with monorepo architecture review: Saasfly PLG Audit

Audit methodology: PLG Skills (open source). You can also run uvx skene-growth analyze . (skene-growth) to scan any codebase for PLG gaps.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions