Documentation

Everything to ship over-the-air updates with Pulsar — from install to multi-deployment testing.

Private bundles

Private bundles encrypt each release on your machine before it is uploaded. Pulsar stores and serves only the encrypted file, the app unlocks it on the phone before installing, and the dashboard unlocks it in your browser for the views that need to look inside, such as what changed and readable crash stacks.

Private bundles are off unless you turn them on, and are available on every plan during the pilot.

What it protects

Anyone who gets hold of a leaked storage bucket, a leaked download link or a backup, anyone intercepting downloads and anyone browsing Pulsar’s storage, Pulsar staff included, sees only ciphertext. Because private bundles also require local signing, a compromised Pulsar can’t release code to your users either.

What it doesn’t protect
  • It is not protection against reverse engineering. The decryption key ships inside your store build, the same way your base JavaScript bundle already does. Anyone who downloads and unpacks your app, Pulsar staff included, can extract it and decrypt that app’s releases. Obfuscation and keeping secrets on your server help with that. Hermes bytecode doesn’t, because every bytecode release also ships a readable JavaScript copy.
  • Attested key delivery isn’t built yet. Pulsar can’t yet hand the key only to genuine installs of your app that the platform vouches for, so the key is in every copy of your store build.
  • Unlocking in the browser means trusting the dashboard. Pulsar serves the dashboard code, so browser unlocking is only as trustworthy as that code. If you’d rather not trust it, unlock with the CLI (pulsar changes), which never sends the key anywhere.
The three options

Each app has one Private bundles setting, next to its Release health gate panel.

  • Off. Releases are stored as uploaded. What changed and readable crash stacks are computed on Pulsar’s server.
  • Manual unlock. Releases are encrypted. Each teammate pastes the bundle key once per browser.
  • Auto unlock. Releases are encrypted. Approved browsers unlock when you sign in.

Manual and Auto unlock produce identical releases and need the same store builds, so switching between them is instant. Switching from Auto to Manual deletes every browser’s wrapped copy of the key, so Pulsar holds none. You can switch back to Off; new releases are then stored plain again, and the panel says so before you confirm.

Before you start
  • Store builds on SDK 1.8 or later that carry the bundle key (below).
  • Local signing set up for the app (Code signing).
  • A bundle key you have backed up.
1. Create a bundle key
bash
pulsar bundle-key create --app MyApp

This generates a random key, writes it to pulsar-bundle-key-MyApp.txt (readable only by you; it won’t overwrite an existing file), registers the key id with Pulsar and prints the Info.plist and strings.xml snippets. Use --out <file> to write it somewhere else. Pulsar receives the key id, not the key.

The file holds one line in the form kid:base64key. Several keys are separated by commas. The same string is used everywhere: the key file, PULSAR_BUNDLE_KEY, and CodePushBundleKeys on iOS and Android.

2. Back it up

Put the key in your password manager or secrets vault before going further. Pulsar can’t recover it. If it’s lost, builds that carry only that key can’t receive OTA updates until you ship a store build with a new key. Turning private bundles on asks you to confirm the key is backed up.

3. Add the key to your store builds
xml
<!-- iOS Info.plist -->
<key>CodePushBundleKeys</key>
<string>KID:BASE64KEY</string>

<!-- Android res/values/strings.xml -->
<string moduleConfig="true" name="CodePushBundleKeys">KID:BASE64KEY</string>

Use the exact string from the key file. The SDK recomputes each key id and rejects one that doesn’t match. This needs SDK 1.8 or later, which tells Pulsar the key ids it carries when it checks for updates (ids only, never the key).

4. Turn it on and check readiness

Open the app’s Private bundles panel, choose Manual unlock or Auto unlock, confirm the backup, register your signing public key if the app doesn’t have one yet, and remove Pulsar’s copy of the signing key if it holds one. The panel shows readiness: for each store build version and OS, how many devices from the last 30 days can open private releases.

Older store builds stay safe

Pulsar never offers an encrypted release to a build that can’t open it. Builds on an older SDK, or without a matching key, get the newest release they can open or stay on their current bundle. Staying behind this way never counts as a failure, never trips the health gate and isn’t added to devices behind.

To get an urgent fix to older builds during the move, release with --plain-copy-for-old-builds. The CLI also uploads the signed, unencrypted zip of the same release, and devices that can’t open the encrypted copy get that one. Pulsar can read the plain copy.

With private bundles on, you can still release unencrypted on purpose. --plain sends a plain release to every build, including those that could open an encrypted one. --only-older-builds sends a plain release only to builds that can’t open private releases, while builds holding the key keep their encrypted one. Pulsar can read both. The dashboard tags every release as private, + plain copy, plain or plain · older builds only, and the Store builds panel shows how many devices on each version can open private releases.

5. Release

When the setting isn’t Off, pulsar release needs the bundle key and your local signing key, and stops with a clear message if either is missing. It never uploads plaintext, apart from a plain copy you ask for.

bash
export PULSAR_BUNDLE_KEY="$(cat pulsar-bundle-key-MyApp.txt)"
export PULSAR_SIGNING_KEY="$(cat pulsar-private.pem)"
pulsar release MyApp Production --platform ios --target 1.0.0

pulsar release MyApp Production --platform ios --target 1.0.0 \
  --bundle-key pulsar-bundle-key-MyApp.txt --private-key pulsar-private.pem

In CI, store the key string as a secret and expose it as PULSAR_BUNDLE_KEY alongside PULSAR_SIGNING_KEY and PULSAR_TOKEN.

  • Each encrypted release prints its reach: how many recent devices will get it and how many stay on their current bundle.
  • A mandatory encrypted release that would reach no device and has no plain copy is blocked.
  • The CLI warns when a key registered for the app is missing from the keys you gave it.
  • Diffs, manifests and source maps are encrypted the same way.
  • With Hermes bytecode, the bytecode and its JavaScript copy are both encrypted, and the plain copy for older builds is the JavaScript build.
Seeing what changed

Pulsar can’t read an encrypted release, so what changed and readable crash stacks are unlocked in your browser. Open a release and choose Unlock.

  • Manual unlock. Paste the key string once per browser. The browser checks the key id and keeps the key for that app in that browser, in a form scripts can use but not export. Forget key on this browser removes it.
  • Auto unlock. Each browser creates its own key pair and registers the public half. A teammate who has the key approves the new browser. Both screens show the new browser’s fingerprint, five groups of four characters: check they match before approving, because that is what stops a substituted key. The approving browser wraps the bundle key for the new browser and sends only that wrapped copy to Pulsar, and the new browser unwraps it when you sign in. The first browser is enrolled by pasting the key once.

Pulsar holds only browsers’ public keys and wrapped copies. Removing a teammate deletes their grants; rotate the key as well, since they could have saved it.

Unlock with the CLI instead
bash
pulsar changes --app MyApp --deployment Production --label v12

This downloads the encrypted manifests and source maps, unlocks them on your machine with PULSAR_BUNDLE_KEY or --bundle-key <file>, and prints what changed. The key never leaves your machine, so this is the route for teams that would rather not trust the dashboard.

Rotating the key
  1. Create and register a new key with pulsar bundle-key create.
  2. Ship store builds that carry both keys, comma separated: OLD_KID:OLD_KEY,NEW_KID:NEW_KEY.
  3. Release with both keys in PULSAR_BUNDLE_KEY. The CLI locks each release for every key it’s given.
  4. Retire the old key once readiness shows no active build reporting it.

pulsar bundle-key id prints the key ids in PULSAR_BUNDLE_KEY or --bundle-key <file>, so you can check which keys a secret holds without printing the keys.

Store review and export compliance

Apple export compliance. If your app declares ITSAppUsesNonExemptEncryption, re-check your answer before the first store build with SDK 1.8. Decrypting your own app’s content is generally treated as exempt, but confirm it for your own app.

Review. Apple and Google Play allow downloaded JavaScript that doesn’t change the app’s purpose. Encrypted downloads are harder for reviewers to inspect, so keep what you ship over the air within what was reviewed.

What Pulsar can still see

Release metadata (labels, sizes, timing, targeting and release notes, which stay readable because the update sheet shows them before download), install telemetry, crash reasons as the SDK reports them, key ids, and any plain copies uploaded for older builds. Pulsar can’t see the code, file names, file hashes or source maps of encrypted releases.