Yeah, I got scooped. Was busy crying. It's fine though, I wrote this for therapeutic benefit, not social media clout.
HN user
IsaacSchlueter
I'm isaacs from Node and npm and JavaScript and stuff.
I don't look at this site much.
http://blog.izs.me/
https://bsky.app/profile/isaacs.bsky.social
https://www.reddit.com/user/isaacs_/
Mikeal Rogers was a prolific Node.js developer and community leader. He created the popular "request" library, ran the initial NodeConf events, helped design the npm registry, and provided a strong leadership that helped get Node.js into a foundation structure with open governance.
That trip is the origin of many of my happiest memories. It was amazing to meet you all, and to share that adventure with him. The playnode.io team and the Korean JavaScript community treated us with such incredible warmth and kindness, we talked often over the years about what a wonderful experience that was.
100%. The main reason we created https://priceops.org is that there's a real mind-shift that has to happen for someone to realize that there's even a "there" there. Most people who've been through it understand all too well, but even there, it's tempting to think "Oh, if only we'd just committed ignorantly to a better pricing model..." not realizing that you'll always get it wrong and the context will always be changing, so it's critical to have an implementation that can respond to new information.
5. Pray that no one ever has to touch it again. (Or failing that, hope you've got a new job somewhere else before that happens.)
So many otherwise smart people make this foolish bet. It is an exceptionally long shot.
Of course I don't wish them too much success, since I'd like to have some customers too.
Feeling is mutual, I'm sure <3 I think it's potentially a very big space with a lot of work to be done, so there's room for plenty of players in it.
written entierly by Isaac
I see what you did there ;)
I enjoy tutorials like this, in the same way and for the same reasons that I enjoy videos where people make cutting tools out of obsidian that they chip by hand.
Personally, though, when I have to actually cut something, I use stainless steel knives or scissors, which are cheap and readily available and do a much better job. Same with pull requests. But historical reenactment can be a fun pastime.
Many startups will go out of business because they don't address this concern.
Startups have extremely little information to guide their pricing and packaging decisions, and often find right away that they've just picked the wrong way to monetize.
An optimal solution makes it easy to build a flexible system, so that it can change rapidly as new information becomes available.
Take the data point for what it's worth (ie, not much). I have never found these chat popovers useful, I have always found them obstructive, and I have always been seething with resentment when forced to use one for an interaction with a company. It's just slightly less painful than being forced to use ye olde telephone, and I will only use live chat if that is the only other option. Most likely, I will simply give up and use a competitor instead.
In any chat I am forced to use, I guarantee I will do my absolute best to radicalize the chat employee against your company and try to convince them to get a different job, or quietly and safely sabotage their employer if they cannot leave. I will make the interaction take as long as possible while doing this, in an effort to drive up costs as much as possible.
I am not alone.
Just to clarify:
This is reasonably fast, but if it's uncached, it is an API call to Stripe
It will be cached after the first request you make with the sidecar/SDK, and unless you have a truly impressive number of users, it'll be able to fit your entire data set in local cache and update only when needed, even on a pretty tiny VM.
I just tend to be probably over-cautious when relying on caches to be fast. Fast 99% of the time is slow 1% of the time. Still good to do it, of course, but always a good idea to at least consider what your worst-case will be.
I just played around with the NextJS starter demo app, and it seems like you can `import tier from 'tier'` and call `tier.subscribe()`, `tier.report()`, and `tier.limits()` without any issue in a NextJS API route.
I am not super familiar with NextJS, but I'll take a look.
The Node SDK is a pretty standard Node library (not clientside JS, for probably obvious reasons). It does spawn a child process if it isn't given a TIER_SIDECAR environment variable, though, which is going to be pretty slow if you have stateless route handlers at the edge.
I'll poke at it today, but I think the way to go is to spin up a sidecar running somewhere with `tier serve`, and then give your NextJS API handlers an environment variable to know where to hit it.
You see a hundred blog posts of "why you should experiment with your pricing", but actually doing that is often such a pain that nobody ends up doing that (and admittedly the reason why I would have shied away from such projects in the past).
Yes, exactly. Especially when those same blog posts tell you to plan to basically have your whole team focus on it for 3 months, as if you have nothing else to do!
- What are your plans like regarding the integration of entitlement checking in the service and SDK? As far as I can tell, that's still a missing piece? I really love how oso[0] has managed to separate the authorization checking from the authorization definition via their DSL and SDKs. Ever since I saw that, I was wondering if a very similar system could work for entitlement checking.
The SDK does expose a `tier.limit(org, feature)` method[1]. This is reasonably fast, but if it's uncached, it is an API call to Stripe, so it can be beneficial to have that drive a proper feature-flagging system like Launch Darkly, or whatever else you use to manage authorization and feature availability.
- I think where your system could provide a lot of benefit would be in help tracking usage metrics that can be harder to calculate (and with that more of a pain to use). It's already nice not having to implement a simpler "number of action N taken the last billing period" metric. However something like a "N number of active users last billing period", where you now would also have to keep track of which users you have already seen before to prevent double-counting them becomes increasingly annoying to implement.
For seats you could use an `"aggregate": "perpetual"` setting on the feature. See: https://www.tier.run/docs/recipes/#simple-per-seat-pricing (that page also has some other examples that might be helpful.) With that, you'd just report the increase or decrease every time the seat count changes, and the counter would never be reset. (There's a few other ways to approach it, but that's what I'd do, as it's probably the simplest.)
You're right, though, if you want to charge based on active users, then it gets a bit more tricky, because you do have to avoid double-counting. And the precise definition of "active" becomes really important. I'd probably approach it by putting a "lastSeen" timestamp on each user account, and then periodically calling `tier.report(org, "feature:activeusers", numberActiveSinceFirstOfMonth, Date.now(), true)` to clobber any previous value. (`clobber` is not the default, since usually you want Tier to count it for you.) I'll add an "active users" example to that recipes page.
[1] https://www.npmjs.com/package/tier#user-content-limitsorg
I completely agree. Whoever[1] made that page should probably not be allowed to make web pages without the assistance of a real designer.
[1]: me. I'm talking about me.
I don't think that's 100% accurate implementation-wise, but the gist is definitely correct. Basically, yes, owing to the way that these things have to get mapped into Stripe's objects, having a plan without any features would make it really costly to reconstruct the model later, and we wouldn't be able to create a subscription to it anyway.
You can create a plan like:
"plan:nofeatures@0": {
"feature:donotuse": {}
}
and then the customer subscribed to that plan will have a single $0 item in that subscription phase. (Tier could in theory do something like this automatically if there's a plan with no features, but figured since an empty plan is likely a mistake anyway, better to just let it be explicit.)To the parent comment, there's nothing in PriceOps that theoretically says a plan must have features, this is just an implementation detail that's somewhat unavoidable.
Ah, I somewhat misunderstood your previous comment, my apologies. This could certainly be clearer in documentation and the priceops.org presentation, I'll take the note on that and think about how to make it more clear.
When we say "change prices", we're not necessarily suggesting "change prices for each customer/transaction, based on demand/time/demographics/etc" in an aggressive yield management approach. In fact, doing this too aggressively in SaaS products (even being too blatant or careless with A/B testing prices and plans) can lead to customer backlash. See for example common reactions to online games selling virtual goods for different prices in different locales. I'm not saying it's never a viable tactic, but it is not a silver bullet, as you point out.
However, it is the case that:
1. Your product will change over time (you keep adding features, decide to focus on some or abandon others, etc.)
2. Your target market may change (features formerly thought of as "pro" become expected at the "basic" level, for example.)
3. Your competitors will change (new entrants to the space, existing players changing their offerings, thus changing customer expectations of your product.)
4. Thus, you're very unlikely to pick the "correct" price and packaging on day 1, and it's virtually impossible to predict what will be "correct" in the future.
So, being able to adjust prices without refactoring your entire application and company is really essential, as many SaaS companies learn the hard way, and as is shown in the resources I shared in my previous comment. Eventually, your prices and packaging will most likely have to change (or at least, changing them will be beneficial, even if not existentially necessary), and making that easy is very valuable.
Might be wrong, but brief review of this appears to show the “thesis” of PriceOps is yield-based metered feature pricing
The "thesis" of PriceOps is less "you must have metered feature pricing", and more "you should design your infrastructure so that you can learn from and adjust pricing in the future". In other words, expect that your product, customers, and landscape will change, and understand that the pricing scheme (ie, what you charge for, how much you charge, and how you bundle prices into a packaged plan) will almost certainly need to change as well.
Do you have an related research to link to?
Here's a selection of links to check out if you're interested in learning more about this subject. I haven't carefully reviewed each one of these to make sure they're 100% what you're asking for, but they're all things I've read and found helpful studying this topic, and many have links to more research and primary sources. Hope you find it helpful :)
On unlocking growth with pricing:
https://techcrunch.com/2022/07/11/turn-your-startups-pricing...
https://www.nfx.com/post/the-hidden-world-of-pricing
https://www.lennysnewsletter.com/p/saas-pricing-strategy
https://www.bvp.com/atlas/why-pricing-deserves-as-much-itera...
More from BVP, this pricing course is amazing btw, I highly recommend checking it out:
https://www.bvp.com/pricing-course
https://www.bvp.com/assets/media/the-startup-pricing-journey...
https://www.bvp.com/atlas/five-pros-and-four-cons-of-usage-b...
(They led npm's series A, I saw first hand the Bessemer brain trust is really impressive.)
"How to price" resources:
https://databox.com/how-to-price-saas-product
https://www2.deloitte.com/xe/en/insights/focus/industry-4-0/...
https://a16z.com/2021/03/11/bottom-up-pricing-packaging-let-...
On benefits/pitfalls of usage-based pricing specifically:
https://review.firstround.com/dont-let-growth-hurt-your-marg...
https://openviewpartners.com/blog/usage-based-pricing-playbo...
https://www.scalevp.com/blog/the-opportunity-in-usage-based-...
https://openviewpartners.com/blog/saas-pricing-and-packaging...
https://adilaijaz.medium.com/6-questions-to-ask-before-adopt...
Some more lessons and case studies:
https://zimtik.com/en/posts/lessons-learned-on-saas-pricing
https://twitter.com/Suhail/status/1418457605437943811
https://arnon.dk/5-things-i-learned-developing-billing-syste...
Grandparenting in old accounts is baked into the system. Plans are immutable[1] once pushed live, so if you add a new plan and put it on your signup page, new customers will use the new plan, but existing customers will keep on with the plan they signed up with. You can of course upgrade them at any time using `tier.subscribe()`
Scheduling rollouts of new pricing can be done by modifying the effective date of a `tier.subscribe()` call. So you could do something like `tier.subscribe('org:' + customerID, 'plan:whatever@123', someFutureDate)`.
For communicating it to customers, yeah, you'll probably want to tell them their price is going to change ;) It might even be good to have an email campaign where they can check out the new price and decide to upgrade/downgrade/whatever with some time to make a decision. Customers usually don't love being forced into things without notice. Tier doesn't handle this part of it for you, but it does give you the tools to make the actual subscription change pretty easily.
[1] "Mostly" immutable. You can of course go into your Stripe account and edit anything they let you. But Tier makes it easier to just add new plans and leave the old ones there.
True story, the npm registry was once taken down (not maliciously, just by accident) by a ReDOS in node-semver. That was extra fun to debug because the failure happened inside of CouchDB.
I've been in the node scene since 0.10. That's around 10 years. My packages have billions of downloads annually. My viewpoint here carries the weight of hours of debug time and frustration and confused users of my code, as well as meeting and knowing the npm staff at the time quite personally
So when you say "npm staff at the time", do you mean at the time of node 0.10?
and knowing under which circumstances package lock files were implemented.
Package lock files were designed in a few short days and pushed out prematurely without much review by a single Npm employee (at the time)
The amusing thing about your comment here is the parts which are accidentally correct.
`package-lock.json` files use the same file format as `npm-shrinkwrap.json` files. Always have, although of course the format of this file has changed significantly over the years, most dramatically with npm 7.
The "design" of the shrinkwrap/package-lock file was done rather quickly, since it was initially just a JSON dump of (most of) the data structure that npm was already using for dependency tree building. However, as far as I know, the days were the standard length of 24 hours, so while that may be "short", certainly shorter than I'd often prefer, they were (as far as I know) no shorter than any other days.
This was indeed shipped without any review by even a single "npm employee", which should not surprising, as "npm" was not at that time a legal entity capable of hiring employees. The initial work was done by Dave Pacheco, and reviewed by npm's author (at that time its sole committer and entire development staff), both of whom were Joyent employees at the time.
The use of a shrinkwrap as a non-published normal-use way to snapshot the tree at build time and produce reproducible builds across machines and time was not implemented by default until npm v5, but there wasn't really much to rush, on that particular feature. You could argue that npm 5 itself was rushed, and that's probably a fair claim, since there was some urgency to ship it along with node version 8, so as not to wait a year or more to go out with node v10.
So this change got pushed out, had an absolute mountain of bugs that took ages to fix...
Idk, I think calling it a "mountain" is relative, actually ;)
They're super, super broken by design.
I know you're using this phrase "broken by design" in the same sense as the author of the OP means it, but... has language just changed on me here, and I didn't notice?
As I've always heard the term used, something is "broken by design" when the actual intent is for a system to fail in some way, to achieve some goal. For example, a legislative or administrative process that is intentionally slow-moving and unable to accomplish its goals in a reasonable time frame, with the hope that this leaves room for independent innovation. Or a product that requires some minor upgrade or repair to continue working, so that the seller can keep tabs on their customers more easily. That kind of thing.
I think what you mean is not that it's "broken by design", but rather it's "a broken design". Unless this is like "begging the question", and I should just accept that I'm gradually coming to speak a language of the past, while the future moves on. It's certainly not intended to cause problems, as far as I'm aware.
If you really do mean "broken by design" (in the sense of a tail light that goes out after 50k miles so that you will visit the dealership and they can sell you more stuff), I'm super curious what you think npm gets out of it.
Yet npm tells you you need them ("please commit this to your repository") and refuses to do basic security things without them (npm audit).
As of npm v7, there's no longer any practical reason why it can only audit the lockfile, rather than the actual tree on disk. Just haven't gotten around to implementing that functionality. If you want it changed, I suggest posting an issue https://github.com/npm/cli/issues. There's some question as to whether to prioritize the virtual tree or the actual tree, since prioritizing the actual tree would be a breaking change, but no reason why it can't fall back to that if there's no lockfile present.
But even approaching build reproducibility is impossible without lockfiles. If a new version of a transitive dependency is published between my install and yours, we'll get different package trees. If we both install from the same lockfile, we'll get the same package tree. (Not necessarily the same bytes on disk, since install scripts can change things, but at least we'll fetch the same bytes, or the build will fail.)
So why were they added? IIRC it was because the version resolution was a massive strain on npm's servers, so lockfiles removed the need to fetch tons of version information each time you added another dependency.
You do not recall correctly, sorry. (Or maybe you correctly recall an incorrect explanation?) The answer is reproducible builds. Using a lockfile does reduce network utilization in builds, but not very significantly.
Oh, and don't even begin to whine about them on Twitter (at the time), lest you be yelled at by the implementor for being ignorant or something.
I hope my tone is civil and playful enough in this message to not consider my response "yelling".
Your take on the installer in npm v6 is not wrong. It got that way by a process of gradual iterative evolution, and it has lots of warts.
npm v7 features a ground-up rewrite of the tree resolution and deification logic in the @npmcli/arborist module. I recommend checking it out, or at least staying tuned for the beta coming soon.
Yes, this has always been possible. Just specify the tarball url instead of a version or range.
You might be surprised (or maybe not) to learn that many service providers are far more willing to spend money on predictably large bandwidth bills than on less predictable changes in their infrastructure which require human time and attention to implement.
If you run `npm install` with an argument, then you're saying "get me this thing, and update the lock file", so it'll do that. `npm install` with no argument will only add new things if they're required by package.json, and not already satisfied, or if they don't match the package-lock.json.
In the bug linked, they wanted to install a specific package (not matching what was in the lockfile), without updating the lockfile. That's what `--no-save` will do.
The SO link is from almost 2 years ago, and a whole major version back. So I honestly don't know. Maybe a bug that was fixed? If this is still a problem for you on the latest release, maybe take it up on https://npm.community or a GitHub issue?
If you run `npm install` with no arguments, and you have a lockfile, it will make the node_modules folder match the lockfile. Try it.
$ json dependencies.esm < package.json
^3.2.5
# package.json would allow any esm 3.x >=3.2.5
$ npm ls esm
tap@12.5.3 /Users/isaacs/dev/js/tap
└── esm@3.2.5
# currently have 3.2.5 installed
$ npm view esm version
3.2.10
# latest version on the registry is 3.2.10
$ npm install
audited 590 packages in 1.515s
found 0 vulnerabilities
# npm install runs the audit, but updates nothing
# already matches package-lock.json
$ npm ls esm
tap@12.5.3 /Users/isaacs/dev/js/tap
└── esm@3.2.5
# esm is still 3.2.5
$ rm -rf node_modules/esm/
# remove it from node_modules
$ npm i
added 1 package from 1 contributor and audited 590 packages in 1.647s
found 0 vulnerabilities
# it updated one package this time
$ npm ls esm
tap@12.5.3 /Users/isaacs/dev/js/tap
└── esm@3.2.5
# oh look, matches package-lock.json! what do you know.
Now, if you do `npm install esm` or some other _explicit choice to pull in a package by name_, then yes, it'll update it, and update the package-lock.json as well. But that's not what we're talking about.I often don't know what I'm talking about in general, but I do usually know what I'm talking about re npm.
It's been a few years since I was directly involved in engineering, but my fairly educated understanding is that it's more around reading of possibly-private packages than publishing.
Publishing is a relatively rare event compared with reading, but in a world of private packages, orgs, and teams, the "can {user} read {object}" gets more complicated. It probably wouldn't be CPU bound if not for the sheer scale we're dealing with, but once all the IO bottlenecks are resolved, you still have to check to make sure that a login token is valid, then get the user associated with it, then check the teams/orgs/users with access to a thing (which might be public, in which case, the problem is a lot simpler, but you still have to get that info and check it), and then whether the user is in any of those groups. So there's a straightforward but relevant bit of CPU work to be done, and that's where Rust shines.
This highlights the problem of averages. Most (99.87% or so) humans download zero npm packages. But those that do, often download them in the thousands at a time. And yes, clean-room CI servers are a big part of that.
I mean, we still write the overwhelming majority of our code in JavaScript. We port to Rust when CPU-heavy task becomes a bottleneck to the rest of the system. It's not as if this paper is saying (nor is it the case) that we've ported the whole registry to Rust. JS has lots of advantages we appreciate.
That is incorrect. Both `npm install` and `npm ci` respect the lock file, and if a lock file is present, will make the `node_modules` tree match the lock file exactly.
`npm ci` is optimized for a cold start, like on a CI server, where it's expected that `node_modules` will not be present. So, it doesn't bother looking in `node_modules` to see what's already installed. So, _in that cold start case_, it's faster, but if you have a mostly-full and up to date `node_modules` folder, then `npm install` may be faster, because it won't download things unnecessarily.
Another difference is that `npm ci` also won't work _without_ a `package-lock.json` file, which means it doesn't even bother to look at your `package.json` dependencies.
That's great to hear!
Reliability sure was tough in those days. npm wasn't secondary or tertiary or any-ary to Joyent. It was my nights and weekends project the whole time I was there, and IrisCouch generously donated infrastructure, which we pushed to its very limit once Nodejitsu acquired them.
It's more accurate to say that npm v5 : npm :: io.js : node.
Same team, performing a big refactoring and significant improvement and modernization of some bits that were outdated and difficult to improve without a controlled demolition and rebuild.
The analogy breaks down, of course, because the governance didn't change, the project's relationship to its corporate backer wasn't standing in the way of progress, etc. (Except, I guess, that it might have gone faster if we'd had more money to hire more devs? But some of this was just a slog through a very old codebase with a lot of scar tissue, and it's hard to speed that up by putting more hands on it.)
Everything in npm 5 was literally planned years in advance. When we have this many people depending on a thing, we have to be careful about how we make drastic changes. Yarn was a strong signal from the community that we were on the right track, but it only seems like a "catalyst" when seen from the outside. Correlation is not causation, even when it lands first.