About freemius-troubleshooting
A diagnostic skill for developers integrating Freemius, organized around common checkout, webhook, SDK, and configuration failures. It maps symptoms to likely causes and targeted reference guides, including links to related Freemius skills.
Key features
- 01Symptom-to-fix troubleshooting index
- 02Webhook and redirect diagnostics
- 03SDK type and event guidance
- 04Checkout and customer portal checks
- 05Cross-references to related skills
Use cases
- 01Diagnose missing access after checkout
- 02Investigate webhook signature failures
- 03Resolve SDK type or event errors
- 04Check portal, upgrade, or entitlement issues
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ freemius/ freemius-ai --skill freemius-troubleshootingAfter installing, type/freemius-troubleshootingin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/checkout-and-portal.mdsdk-types-and-events.mdstack-and-porting.mdwebhooks-and-redirect.mdDetails
From the project READMEFreemius Troubleshooting
When a Freemius integration breaks, the failure is almost always in one of five places: webhooks/redirect plumbing, SDK types/events, the stack/porting layer, checkout/portal wiring, or secrets/dashboard config. This Skill is a diagnostic index — find the symptom, jump to the reference, apply the fix.
Source FAQ: https://freemius.com/help/documentation/ai/lovable/#troubleshooting
How to use this Skill
- Match your symptom in the table below.
- Open the linked reference for the root cause + fix.
- If it is a
freemius-core/freemius-checkout/freemius-customer-portalconcern, the reference points you back to the exact section of those Skills.
Symptom → fix index
| Symptom | Likely cause | Reference |
|---|---|---|
| Checkout completes but the user never gets access | Redirect URL or webhook not configured / not public | references/webhooks-and-redirect.md |
| Webhook signature verification fails | Raw body was parsed before handing to the listener | references/webhooks-and-redirect.md |
TS error: Type '"lifetime"' is not assignable on an entitlement type | Entitlement type is subscription | oneoff, not lifetime | references/sdk-types-and-events.md |
Subscribing to license.quota.changed does nothing / type error | That event does not exist in @freemius/[email protected] | references/sdk-types-and-events.md |
TS error: Object is possibly 'false' on license in a webhook handler | Array-subscription license is a union incl. the deleted shape | references/sdk-types-and-events.md |
prisma generate / client fails wanting a driver adapter; url rejected | Prisma 7 dropped classic datasource url — pin Prisma 6 | references/stack-and-porting.md |
process.env.NEXT_PUBLIC_* is undefined in the browser | Vite uses import.meta.env.VITE_*, not process.env | references/stack-and-porting.md |
@/... or @shared/... import cannot be resolved | Missing alias in vite.config.ts + tsconfig paths | references/stack-and-porting.md |
Installed skills via npx skills add but Claude Code (CLI) doesn't see them / slash commands missing | Claude Code isn't a default install target; skills went to ~/.agents/skills/ with no ~/.claude/skills/ symlink | references/stack-and-porting.md |
Tried the npx skills add … -a claude-code CLI route on Claude Desktop (Claude.ai app) and it doesn't apply | The CLI / -a claude-code flag is a Claude Code concept only; Desktop installs each Skill as a per-Skill .zip via Customize → Skills → "+" → Upload a skill (core first; Code execution enabled under Settings → Capabilities) | references/stack-and-porting.md |
| Secret key / API token leaks to the browser, or 401 from Freemius | A Freemius call was made client-side instead of server-side | references/checkout-and-portal.md |
| Checkout overlay opens empty / unconfigured | Overlay not primed from a server-created CheckoutSerialized | references/checkout-and-portal.md |
| Upgrade route 500s: "Payment method update is not allowed for lifetime licenses" | checkout.create({ licenseId }) calls a payment-method-update endpoint that rejects renewing licenses (expiration: null) — use setLicenseUpgradeByKey | references/checkout-and-portal.md |
| User "upgraded" but now has two subscriptions / double-billed | Upgrade offered as a plain subscribe checkout to an already-subscribed user — use a license-upgrade checkout (setLicenseUpgradeByKey) + entitlement-aware surfaces | references/checkout-and-portal.md |
| Portal actions 404 / token invalid | One /api/portal endpoint must serve data and signed actions | references/checkout-and-portal.md |
| One-off / lifetime purchase doesn't behave like the subscription demo | Different modality — use billing_cycle: 'lifetime', type oneoff | references/checkout-and-portal.md |
| Upgraded to Lifetime but still gated / credits still charged / not shown as active (invoice is correct) | freemius.entitlement.getActive() filters to type: 'subscription' — a one-off Lifetime row is invisible, so getUserEntitlement() returns null | references/sdk-types-and-events.md |
Coupon'd subscription "renews" ~100 years out (USD 0 · renews 2126) | 100%-off coupon scoped to "First payment and renewals" | references/checkout-and-portal.md |
| Re-subscribe blocked ("already on an active subscription") after a cancel | Admin cancelled the license, leaving the subscription orphaned-active | references/checkout-and-portal.md |
Subscribed user still sees the paywall / 402 despite an active subscription; user_fs_entitlement table is empty | Checkout used the bare hosted redirect with no overlay purchaseCompleted sync and no Dashboard checkout-redirect URL — nothing ever wrote the entitlement row | references/checkout-and-portal.md |
user_fs_entitlement empty for every user; sync POST returns 400 "License ID is required in the request body" (frontend .catch() hides it) | The overlay purchaseCompleted POST was routed through checkout.request.createProcessor / processRedirect (or syncEndpoint pointed at /api/checkout/redirect?action=process_purchase). That path expects a signed redirect / license_id in the body the overlay payload lacks. Overlay sync must be a plain POST /api/purchase that reads the license from the payload → syncEntitlementByLicenseId — separate endpoint from the hosted GET /api/checkout/redirect | freemius-checkout → references/checkout-provider.md |
| A gated feature (e.g. image OCR on Premium) shows a generic "OCR failed. Please try another file." | The server's tier/entitlement gate (403/402 with a reason like tier_upgrade_required/image_requires_pro) was swallowed into the catch-all error. A gate is not a failure — branch on status+reason and open the paywall with an upgrade path, not the error toast | freemius-checkout → references/paywall.md |
| Purchase succeeds but there's no pricing page / no way to browse or buy plans without hitting a paywall first | Build shipped the paywall but skipped the standalone pricing route — a dedicated /pricing page (Subscribe + Topup tables from GET /api/checkout/pricing) + nav link is a required deliverable | freemius-checkout → references/pricing-tables.md |
| Success toast says "credits added to your account" after buying a subscription (or wrong copy for Lifetime) | Post-purchase copy didn't branch on the synced purchase type — use the re-fetched entitlement tier: lifetime → "Lifetime access unlocked", subscription → "<tier> unlocked", credits pack → "page credits added" | freemius-checkout → references/checkout-provider.md |
| Credit balance shows the old value after an OCR (metered) run until a manual page refresh | The server debits credits correctly (credit -= pages), but the metered action's success handler never refreshes the displayed balance — the badge is stale until a full reload. Refresh the display without a reload: either read the new balance from the action's response, or await the same refreshEntitlement() (GET /api/entitlement → credit) the purchase flow uses. This is the consumption twin of the post-purchase entitlement refresh | freemius-checkout → references/paywall.md |
/pricing shows nothing for a Lifetime / top-tier holder ("nothing is shown anymore") | Every buy surface is correctly hidden (they own the top tier), but hiding them all leaves a blank page. Render a friendly "you own everything" empty-state instead — never a blank page. (A credits-only viewer must still see Subscribe + Topup.) | freemius-checkout → references/pricing-tables.md |
| Account-page "Change plan" renders as a bare underlined link with no padding, misaligned next to Cancel | "Change plan" is a router <Link> given a button class (e.g. btn-outline) that only inherits padding / radius / text-decoration:none from a base .btn class — applied alone to an <a> it loses all of it. Make the button/outline class self-contained, or put both base+modifier classes on the <Link> | freemius-customer-portal → references/subscription-and-upgrade.md |
| Retention coupon: clicking "Apply discount & keep my plan" just closes the wizard, nothing happens, Dashboard shows 0 redemptions | The apply POST sent an empty {} body — the SDK's renewal-coupon action requires { couponId: <coupons[0].coupon_id> } and 400s otherwise; AND the wizard ignored res.ok, so the failed call closed as if it succeeded. Send the real couponId and check res.ok (show error + keep open on failure) | freemius-customer-portal → references/cancellation-and-coupon.md |
After a hosted checkout/upgrade the browser lands on /checkout-result?success=true showing a blank page with only the header | The server GET redirect handler bounces to /checkout-result, but the SPA router (App.tsx <Routes>) has no matching client route — the redirect target is a real page the build forgot to ship. Add a /checkout-result route that reads success, refreshes entitlement once on mount, shows the confirmation, and routes back into the app | freemius-core → references/purchase-processing.md |
| Entitlement doesn't update after a cancel/renew; webhook seems dead — but handler, URL and signature all check out | Lifecycle event fired before the webhook URL was active/propagated in the Dashboard — never delivered (not retro-sent) | references/webhooks-and-redirect.md |
First-resort checks (the Lovable FAQ baseline)
The official FAQ's top recommendations, applied to this stack:
- Ask the agent to re-run the failing step. These Skills are modular —
re-invoke the specific reference (e.g.
freemius-core→purchase-processing.md) rather than re-doing the whole integration. - Webhooks + checkout redirect are the two critical paths. Verify both end to end first; most "no access after paying" reports are one of these misconfigured. Freemius emails you when webhook processing fails — paste that email content into the agent to pinpoint the failure.
- After publishing/deploying, update all URLs. A redirect or webhook URL
still pointing at
localhost(or an old domain) silently breaks the flow.PUBLIC_APP_URLmust be the live domain. - One-off purchases need a different approach than the subscription path —
see the checkout/portal reference for the
lifetimemodality. - Still stuck? Contact Freemius support ([email protected]) with the product id, the failing step, and screenshots.
Capturing new entries
This Skill is meant to grow. When a Skill defect is found during a validation gate, log it in the gate's "Skill defects found" table, fix the offending Skill, then add a troubleshooting entry here (symptom → cause → fix) so the next integrator skips the stumble.