A developer we'll call Marcus picks passkit-generator on a Tuesday afternoon because it's the top result for "apple wallet pass node.js" and the README promises exactly what he wants. Two hours later he has a signed .pkpass downloading from a route handler, a real barcode, and a coupon that opens cleanly in Wallet on his own phone. He closes the ticket and marks the integration done.
It isn't done. It's the first quarter of it. passkit-generator generates a pass and stops there, and everything past that first install, the update service, the push notifications, the certificate handling, a second wallet entirely, is a separate decision Marcus hasn't made yet. That gap is easy to miss because the library is good at the part it covers, and nothing in the getting-started guide tells you how much sits past the edge of it.
What passkit-generator actually promises
passkit-generator is a Node.js library, MIT licensed, that turns a pass model and a set of dynamic fields into a signed .pkpass buffer, stream, or set of raw files. It splits static assets like logos and background images from the fields that change per customer, which is the right instinct for a loyalty card or a boarding pass template you'll issue thousands of times with only the barcode and the name changing. As of this month it's at version 3.6.1, sitting around 1.3k GitHub stars, and pulling roughly 1.2 million npm downloads a month, so it's the default answer for Node developers doing Apple Wallet generation, not a niche pick.
None of that is a knock on the library. It does one job, generation, and does it about as well as that job can be done outside of Apple's own tooling. The problem is what the job description leaves out, and the README says so directly rather than pretending otherwise. Its own docs point developers toward two companion projects from the same author for anything past the initial .pkpass: one for the web service protocol, one for push notifications. That's the author being honest about scope. It's also the tell that "generate a pass" and "run a wallet pass program" are two different projects wearing the same search term.
The three things it hands off to someone else
Look past the quickstart and passkit-generator's own documentation names its gaps for you. Signing a pass needs a Pass Type ID certificate and Apple's WWDR intermediate, both of which you generate yourself with OpenSSL and Keychain Access, and the library takes the resulting .p12 as an input rather than helping you produce one. Keeping an already-issued pass current needs a web service that implements Apple's registration and update endpoints, which the library explicitly does not include, pointing instead to a separate package called passkit-webservice-toolkit from the same maintainer. And getting a device to actually notice an update needs an Apple Push Notification service integration, which the docs hand off again, this time to a small library called hapns.
Three separate concerns, three separate decisions, and that's before anyone mentions Android. passkit-generator doesn't touch Google Wallet at all. Its docs don't claim otherwise, since the project is scoped to iOS and watchOS from the description down.
None of this is hidden. It's genuinely well documented, which is part of why it's easy to underestimate. A README that clearly names its own gaps reads as thorough, not incomplete, and a developer skimming it at 11pm two weeks before launch is more likely to bookmark the two companion links for later than to treat them as blocking work still ahead of them.
Assembling the stack yourself, one package at a time
Here's what that actually looks like in a real build. You start with passkit-generator for the .pkpass itself, which handles the manifest hashing and PKCS#7 signature correctly as long as you feed it a valid certificate. Then you add passkit-webservice-toolkit, a framework-agnostic TypeScript wrapper around the endpoints Apple's web service protocol defines: device registration, a list of passes that changed since a device last checked, and the individual pass fetch itself. You wire that into your own database, because the toolkit wraps the HTTP surface, not the part where you decide which passes actually changed or store a device's push token against a serial number.
Then comes hapns for the APNs half, which sends the actual wake-up push once your server decides a pass needs refreshing. That push carries no data, just a signal, so the device calls back into the web service you just built to ask what changed. Get the apns-topic wrong, or let a push token go stale without handling the failure response, and a pass stops updating with no error visible anywhere in your own logs, because APNs will happily return success for a notification the device never acted on. None of the three libraries surfaces that failure for you. You're the integration layer between them, and the integration layer is where most of the actual engineering time goes.
Add a fourth concern most teams run into only after the first customer complaint: a change tag or version marker on each pass, so your web service can tell a device "nothing new since your last check" instead of resending an unchanged pass on every poll. passkit-generator has no opinion on this, correctly, since it's not a generation problem. It's a data-modeling problem you now own because you picked the generation-only library first and worked outward from there.
A fifth: the authentication token embedded in every pass at signing time has to stay stable across updates, even though almost everything else in the pass is free to change. Roll it when you shouldn't and every device holding an older pass starts failing authentication against your own web service, with a support ticket that reads like a mystery until someone checks the one field nobody thought to compare.
Certificates don't get simpler because a library is involved
The certificate side of this doesn't shrink either. A Pass Type ID certificate still expires once a year, still can't be renewed in place, and still needs a CSR generated and exported as a .p12 with the private key intact, a step the library takes as a given rather than something it manages for you. We've covered the full certificate chain elsewhere on this blog, including the WWDR generation mismatch that produces the exact same unhelpful install error as an expired cert. The short version for this comparison: picking a generation library changes nothing about who tracks that expiration date. It's still you, on a calendar you have to set yourself.
Google Wallet isn't a plugin away
If the Apple half of a wallet pass program is three packages stitched together, the Google half is a separate project built from close to nothing. Google Wallet passes aren't signed .pkpass archives at all. They're JSON objects referenced by a signed JWT that a user's browser or app hands to Google's save endpoint, a completely different signing scheme, a different account model built on Google service accounts and issuer IDs, and a different update mechanism that pushes changes through Google's own infrastructure rather than APNs. passkit-generator was never going to cover this, and it doesn't claim to. What that means in practice is that "add Google Wallet support" isn't a configuration flag on the stack you already built. It's a second integration, from a thinner set of community libraries, that happens to serve a conceptually similar end result to a customer holding a phone.
A team that ships Apple first and tables Google for later usually finds later arrives faster than expected, once a product manager notices half of mobile users are on Android and asks why the loyalty card only works for the other half.
The gap shows up in the tooling, too. Search for a Google Wallet equivalent of passkit-generator and the results thin out fast, mostly thin JWT-signing helpers rather than a maintained library with the same download numbers and companion ecosystem. That asymmetry isn't an accident. Apple Wallet has had a Node community building around it since Passbook shipped in 2012. Google's REST API and object model are newer and less uniformly adopted, so the open-source tooling around it hasn't caught up to the same depth, which means a team building the Google half from scratch is doing more of the work themselves than the Apple side ever required.
What a hosted API collapses into one call
This is the part of the comparison that's actually a trade-off rather than a verdict. A hosted pass API, Passmint among them, takes the same pass definition and returns a signed .pkpass and a Google Wallet save link from one request, because it already owns the certificate handling, the web service protocol, the APNs integration, and the Google JWT signing behind that single endpoint. Changing a loyalty balance or a gate number becomes one PATCH call instead of a write to your own change-tracking table followed by a push you send yourself. The Pass Type ID certificate still belongs to your Apple Developer account, but the private key and the WWDR bundling are handled for you, with the expiration date surfaced instead of buried in a calendar reminder nobody set.
Self-hosting the four-package stack isn't wrong. The two paths solve genuinely different problems, and picking between passkit-generator-plus-supporting-libraries and a hosted API deserves an actual decision, not a formality on the way to the same place.
When assembling it yourself is still the right call
Self-hosting earns its cost in a few specific situations.
- You're a platform issuing passes on behalf of other businesses. Every certificate has to be scoped per tenant, and that's usually easier to reason about when you hold the keys directly rather than through someone else's API surface.
- You're running entirely on an edge runtime with constraints a hosted vendor doesn't fit. A Cloudflare Worker with no filesystem access needs a generation path built for that from the start, not one bolted on afterward.
- You need to audit or fork every dependency in a compliance-sensitive stack. An open-source library lets you read, patch, and vendor the exact code signing your passes. A hosted API, by definition, doesn't.
For exactly that middle case, there's a path between "four separate npm packages" and "hand the whole pipeline to a vendor." The open-source passmint package generates signed passes for both wallets from any Web Crypto runtime, Node, Cloudflare Workers, Deno, or Bun, without polyfills, so a team that wants to self-host can start from a library that already speaks both wallets rather than bolting Google support onto an Apple-only stack after the fact. It still leaves the update service and certificate storage as your problem, the same way passkit-generator does. It just narrows the list of things you're stitching together from four to one, plus whatever web service and push layer you choose to run yourself.
The actual comparison
| passkit-generator alone | passkit-generator + toolkit + hapns | Hosted API | |
|---|---|---|---|
| Apple Wallet generation | Yes | Yes | Yes |
| Google Wallet | No | No | Yes |
| Update web service | No, build it | Yes, wired by you | Yes, included |
| APNs push | No | Yes, via hapns | Yes, included |
| Certificate storage | You hold the key | You hold the key | Managed for you |
| Packages to maintain | 1 | 3+ | 0 |
Every row past the first is a real decision with a real cost on both sides, and no library or API makes that decision for you. What changes is how many separate pieces you're personally responsible for keeping wired together a year from now, after the person who built the integration has moved to a different team and the certificate is three weeks from expiring.
If you'd rather spend that engineering time on your actual product than on a change-tag column and an APNs retry policy, Passmint's API issues both wallets from one request and keeps the certificate, the push, and the update protocol out of your codebase entirely.
Primary sources
Common questions
Technical content writer, Passmint
Julio is a technical content writer at Passmint. He writes about Apple PassKit, the Google Wallet API, and what breaks when wallet passes meet production traffic.
More from Julio Song →