Freemius 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/sdk@0.3.0 |
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 (support@freemius.com) 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.