# pass-js turns a template folder into a signed Apple Wallet pass

> @walletpass/pass-js is a TypeScript library for Node that generates Apple Wallet passes with localizations, NFC personalization and web service push updates, with a template layer that keeps the shared assets in one place and a clear warning about loading untrusted bundles.

**tinovyatkin/pass-js** — Apple Wallet Passes generating library for Node.JS

- Repository: https://github.com/tinovyatkin/pass-js
- Stars: 741 · Forks: 86
- Language: TypeScript
- License: AGPL-3.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/tinovyatkin-pass-js

## A Node library for the signed pass bundle

@walletpass/pass-js is described as a Node.js library for generating Apple Wallet passes with localizations, NFC and web-service push updates, written in TypeScript, and the package description adds that it also pushes updates from Node.js. The problem it solves is not trivial. A Wallet pass on the device is a signed bundle of JSON, images and localisation resources, and the signing key is tied to a Pass Type ID issued by Apple, so producing one involves certificates, a pass.json that matches a schema, a directory of images at several densities and a set of localised .lproj folders. Doing that by hand for every pass type, and every campaign, is repetitive work, and the library's job is to make the shared part a template and the individual part a stamped copy. The package is published under the @walletpass scope at version 7.2.1, with the author being Konstantin Vyatkin, and the repository is funded through OpenCollective under the walletpass collective. It is an ESM only package with sideEffects set to false, and it declares a Node engine floor of 24.12.0, which is a recent and deliberate requirement rather than an accident of a stale manifest.

## Certificates come first, one per Pass Type ID

The certificate step comes before any code, and the documentation is explicit that you need a certificate issued by the iOS Provisioning Portal, one per Pass Type ID. The flow is the familiar Apple one. You add the certificate to your Keychain, export it as a .p12 file, and convert it into a .pem file. The package ships a helper for that, the passkit-keys command, which is also installed as a binary:

```sh
./bin/passkit-keys ./pathToKeysFolder
```

If you would rather not use the helper, the equivalent openssl invocation is given:

```sh
openssl pkcs12 -in <exported_cert_and_private_key>.p12 -clcerts -out com.example.passbook.pem -passin pass:<private_key_password>
```

The resulting .pem file goes into your keys directory, and the file name matters because the library looks for a key named com.example.passbook.pem inside a template folder. One piece of friction is removed for you, since the Apple Worldwide Developer Relations Certification Authority certificate is bundled with the package and you do not need to supply it. The key itself is loaded into the template either from a path or from strings:

```js
await template.loadCertificate(
  '/etc/passbook/certificate_and_key.pem',
  'secret',
);
// …or set them directly from strings:
template.setCertificate(pemEncodedPassCertificate);
template.setPrivateKey(pemEncodedPrivateKey, optionalKeyPassword);
```

Two fields are required on every pass, and the documentation names them: passTypeIdentifier, which is the Apple Pass Type ID and carries the pass. prefix, and teamIdentifier, your Apple Developer team identifier. Everything else is optional and settable directly on the template or the pass, including backgroundColor, foregroundColor, labelColor, logoText, organizationName, suppressStripShine and webServiceURL.

## A template holds what every pass of that type shares

The template layer is the core abstraction. A template carries the fields, images and localizations shared between your passes, and you stamp individual passes out of it. There are three ways to get one. Template.load reads a local folder, taking all fields from pass.json, all images, the com.example.passbook.pem private key and any .lproj localization folders. Template.fromBuffer takes a ZIP compressed pass bundle, which the documentation illustrates with a .pkpass fetched from S3. And the constructor builds one by hand with a pass style and an object of fields:

```js
const template = new Template('coupon', {
  passTypeIdentifier: 'pass.com.example.passbook',
  teamIdentifier: 'MXL',
  backgroundColor: 'red',
  sharingProhibited: true,
});
await template.images.add('icon', iconPngFileBuffer);
await template.images.add('logo', pathToLogoPNGfile);
```

The first argument is the pass style, coupon or eventTicket among the options, and the second is an optional object with any fields you want set. Images can be added one at a time with a density and locale, or loaded in bulk:

```js
// Add a single image with a specific density and locale
await template.images.add('icon', iconFilename, '2x', 'ru');
// Or load all images in all densities and locales at once
await template.images.load('./images');
```

Image input may be a file path or a Buffer, and the format is enforced, with only PNG accepted. Once the template exists, a pass is created from it and given its own fields:

```js
const pass = template.createPass({
  serialNumber: '123456',
  description: '20% off',
});
```

One ergonomic detail is worth noting. Apple's JSON schema represents structure fields such as primary and secondary fields as arrays whose items must have distinct key properties, and the library exposes a Map-like API with an additional add method that does the logical thing, so pass.primaryFields.add({ key: 'time', label: 'Time', value: '10:00AM' }) works, and the same collection can be read with get, iterated with entries, and pruned with delete and clear.

## The trust boundary on template loading is stated plainly

The README contains a warning that deserves to be repeated, because the library does something that is easy to get wrong. Template.load and Template.fromBuffer are intended for trusted template assets, and you should not pass attacker-controlled folders, ZIP buffers or .pkpass bundles to these APIs, because loading untrusted bundles can consume excessive memory and may crash or stall the process. That is a decompression and parsing hazard rather than a signing hazard, and the fix is architectural. A service that generates passes on behalf of untrusted tenants must not accept a template from the tenant, and the template should come from your own storage, your own repository or a bundle you fetched from a location you control. The documentation's own example makes the point in passing, since the fromBuffer path is illustrated with a .pkpass fetched from S3, which is safe only if that bucket and those keys are yours. A related development affordance is the third argument to Template.load, where allowHttp enables a webServiceURL that points at an HTTP rather than an HTTPS server, which the documentation marks as useful for development. That flag has no business in production, since the pass update channel is where the server identity matters.

## NFC and personalization files have a strict emit condition

For reward card signup flows, the library supports NFC personalization, and the shape of it is a message identifier plus a personalization block:

```js
const template = new Template('storeCard', {
  passTypeIdentifier: 'pass.com.example.passbook',
  teamIdentifier: 'MXL',
});

template.nfc.message = 'member-id';
template.personalization = {
  description: 'Join Acme Rewards',
  requiredPersonalizationFields: [
    'PKPassPersonalizationFieldName',
    'PKPassPersonalizationFieldEmailAddress',
  ],
  termsAndConditions: '<a href="https://example.com/terms">Terms</a>',
};
await template.images.add('personalizationLogo', personalizationLogoPng);
```

The rule that governs what actually lands in the bundle is spelled out, and it is a conjunction rather than a list. The personalization.json file and the personalizationLogo*.png files are emitted only when the final pass has NFC data, personalization metadata, and at least one personalization logo. If any piece is missing, the personalization files are left out of the generated bundle. That is a sensible design, since a pass with a personalization section but no logo, or an NFC payload with nothing to sign the user up for, would produce a broken experience, but it does mean a partially configured template fails quietly in the output rather than loudly at generation time. The personalization logo has a stated size of 150 by 40 points, and the same PNG only rule applies to it as to every other image.

## AGPL-3.0, Node 24 and a native TypeScript toolchain

The licence is AGPL-3.0-or-later, with the manifest carrying the same terms and a LICENSE file in the tree, and the package is funded through OpenCollective. For a library that many teams will embed in a service that generates passes for customers, strong copyleft is the term to read carefully before you adopt it, and the right move is to settle it with whoever owns your licensing. The technical requirements are also unusually strict. The manifest requires Node 24.12.0 or newer, which rules out a lot of existing production images, and the build uses tsgo, the native preview of the TypeScript compiler, rather than the classic compiler. Linting and formatting are done with oxlint in type-aware mode and oxfmt, and the test script builds first and then runs the node test runner over the TypeScript test files with source maps enabled, so the tests execute the sources rather than a compiled copy. The tree carries a CHANGELOG.md and a release-please configuration, and tagged releases include v7.1.0 and v7.2.0 on 2026-05-11 and v7.2.1 on 2026-05-12, with the last push on 2026-08-07. There is also an AGENTS.md and a CLAUDE.md at the root, which tells you that contributions are expected to be made with an agent in the loop rather than by hand.

## Conclusion

Adopt pass-js if you ship Wallet passes from a Node backend and want template reuse, localization folders and NFC personalization without hand building the signed bundle. Do not adopt it in a service that accepts template folders, ZIP buffers or pkpass bundles from untrusted callers, because the documentation says loading untrusted bundles can consume excessive memory and may crash or stall the process. Before you integrate, get the signing certificates sorted per Pass Type ID, confirm your Node version clears the 24.12.0 floor the package declares, and read the AGPL-3.0 terms, since the licence is strong copyleft for a library you may be embedding in a service.

## FAQ

### How do I install pass-js?

The package is published on npm under the @walletpass scope, so npm install @walletpass/pass-js or yarn add @walletpass/pass-js adds it. The manifest requires Node 24.12.0 or newer and the package is ESM only.

### Which certificates do I need to generate a Wallet pass?

You need a certificate issued by the iOS Provisioning Portal, one per Pass Type ID. Export it from your Keychain as a .p12 file and convert it to a .pem file, either with the bundled passkit-keys command or with the openssl pkcs12 invocation in the README. The Apple Worldwide Developer Relations Certification Authority certificate ships with the package.

### Can I load a pass template that came from an untrusted source?

The documentation says not to. Template.load and Template.fromBuffer are intended for trusted template assets, and passing attacker-controlled folders, ZIP buffers or .pkpass bundles can consume excessive memory and may crash or stall the process. Keep templates in storage you control.

### Which fields are required on a Wallet pass?

Two, and the README names them: passTypeIdentifier, the Apple Pass Type ID with the pass. prefix, and teamIdentifier, your Apple Developer team identifier. Any other documented pass field can be set directly on the template or the pass instance.

### Which image formats does pass-js accept?

Only PNG, and the format is enforced by the library. Image input can be a file path or a Buffer, and you can add a single image with a density and locale or load a whole directory of images at once.

## Sources

- [Issues](https://github.com/tinovyatkin/pass-js/issues)
- [License: AGPL-3.0](https://github.com/tinovyatkin/pass-js/blob/master/LICENSE)
- [README](https://github.com/tinovyatkin/pass-js/blob/master/README.md)
- [Releases](https://github.com/tinovyatkin/pass-js/releases)
- [tinovyatkin/pass-js on GitHub](https://github.com/tinovyatkin/pass-js)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tinovyatkin-pass-js
