Skip to content

Build1 publisher3 min readPublished

Nakodo routes every Stripe plan change through four functions of its own

Nakodo's developer moved all Stripe plan changes into four in-app functions and pushed every downgrade to the end of the paid month. The timing rule stops prorated-credit refunds; the case against the hosted portal rests on product copy and unbilled plans.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Nakodo routes every Stripe plan change through four functions of its own
Generated illustration

What happened

  • Nakodo's pricing FAQ promises plan changes any time in Settings and says a move to a smaller plan pauses extra campaigns instead of deleting them.
  • The developer argues the hosted portal cannot show product-specific consequences, such as paused campaigns and lost CSV export on a move from Business to Free.
  • Per the post, an immediate downgrade leaves a prorated credit, so a customer can use a month of the big plan in two days and swap down for most of the money back.
  • Upgrade previews call Stripe's invoices.createPreview with an explicit proration date, and that same date is used when the charge is made.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision A team that keeps the hosted portal still has to choose when a downgrade takes effect, because the refund loop described in the post comes from immediate proration.
  • constraint Any product that comps plans by hand needs an in-app plan screen anyway, since those accounts never exist in Stripe for a hosted flow to manage.
  • cost Owning plan changes means owning stale-state recovery and invoice-line parsing, both of which Nakodo's code had to handle explicitly.

Each of Nakodo's plan-change functions sets its own timing [8]. `upgrade` takes effect immediately and charges the difference for the rest of the paid month [8][17]. `scheduleDowngrade` and `cancelSubscription` take effect when the paid month ends [8]. `keepPlan` undoes either booking [8].

Every call first loads the same Current object: the subscription, its single item, the plan that item's price maps to, any schedule, and the customer id [9]. If the item or plan is missing, or Stripe reports the subscription as `canceled` or `incomplete_expired`, the call refuses to continue [9]. It calls `settle()` before it throws [10]. That ordering is careful work. A failed action still records that Stripe and the local row disagreed, so the reload the error message asks for shows the real plan [10].

`previewUpgrade` stamps a `proration_date` from the current clock, asks `invoices.createPreview` for an `always_invoice` proration at that second, and returns the date with the amount [11]. "Showing a number in a dialog and then charging a different one is the sort of thing that generates support email forever," the developer wrote [13]. The second detail is a filter. A preview invoice can also carry the next period's normal charge, so the code sums only lines flagged at `line.parent?.subscription_item_details?.proration` [12]. It then clamps the total at zero [14].

The post gives three reasons for skipping the portal, ordered by how much each cost to learn [18]. They are not equally strong. The credit argument targets "a plain downgrade applied immediately" [4]. Nakodo's answer is a timing rule: the smaller plan starts when the paid month ends and nothing is refunded in between, "because the month was used," the developer wrote [5]. In my view that rule would close the buy-big, swap-down loop no matter which screen holds the button [4][5]. The post does not say whether the portal could be configured to defer a downgrade the same way [4].

The other two reasons are about the portal itself. Nakodo's confirmation copy is specific. A move from Business to Free pauses campaigns above the new limit, trims keyword counts, removes CSV export, and leaves already-unlocked creators unlocked [2]. "The portal does not know your product," the developer wrote, adding that the portal "has no way to say them" [3]. Plans granted by hand have no Stripe customer, subscription or invoice, so for those accounts the portal is a dead end [6].

That last reason, listed as the costliest to learn, is the one I'd weigh most, because it reaches the data model [18][6]. Nakodo splits "paid" from "billed by Stripe" into two flags: `billedByStripe` for a live subscription changed through Stripe, and `billingAccount` for a Stripe customer with invoices and a card [7]. They stay separate because a cancelled subscriber still has invoices worth showing and nothing to change [7]. I think a team with no hand-granted plans and no plan-specific limits has a much weaker case for building all four verbs [6][2].

What to watch

  • How scheduleDowngrade books the end-of-month change in Stripe; the Current object loads any schedule, but the published excerpt stops before that code.
  • Whether Stripe's portal settings can defer a downgrade to period end, which would move the credit argument from the portal column to the policy column.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories