Skip to main content

Context

A wallet pass is the loyalty card inside Apple Wallet or Google Wallet: balance, tier and a scannable code at the point of sale. It removes the single biggest friction in the whole category — “download our app” — and replaces it with a link. For Latin America it matters more than elsewhere. Android dominates the device base and Google Wallet ships preinstalled, so the addressable share is close to universal, and the pass updates by push without any app of ours on the device. ADR-020 places this as the middle tier of the mobile strategy. One thing it got wrong, corrected here: wallet passes are always issued under Softcrum’s own accounts, never the client’s. The app needs the client’s developer account because it competes for a store listing and Apple’s guideline 4.3 pushes it there; a pass has no listing, and its branding — logo, colours, organization name — travels inside the pass file itself. A tenant who wants something that goes out entirely under their own name gets the white-label app, which is what that tier is for. Offering a client-account wallet tier would sell the same thing twice and add certificate management for every tenant. That makes one spike blocking rather than tier-shaping: if Apple requires a separate Pass Type ID per brand, passes under our own account may not be possible at all, and this feature changes shape. It is answered before ADR-022 is written.

Scope (normative)

  • Pass generation for Apple Wallet (.pkpass, signed) and Google Wallet (JWT-based objects).
  • Per-tenant branding driven by the same theming configuration as the mobile app.
  • Balance, tier and a scannable member code on the pass face.
  • Push updates on balance and tier change, through the existing notification cascade.
  • loyalty.wallet_pass_registrations tracking which devices hold which pass.
  • A signed, expiring add-to-wallet link suitable for embedding in an email.

Non-scope (normative)

  • Payment. These are loyalty passes, never payment instruments.
  • Location-triggered lock-screen relevance in the first version. It is one of the strongest features of the format, but it needs per-tenant venue coordinates, which is its own data problem.
  • Offline validation at the point of sale. The code is scanned and validated against our API.

Behaviour (normative)

  1. Passes are issued under Softcrum’s own Apple and Google accounts, always. Client-owned certificates are explicitly not offered — a tenant wanting their own channel gets the white-label app (ADR-020). Signing keys are stored as secrets and rotatable without reissuing every pass; a leaked key that cannot be rotated is a full reissue to every member.
  2. The add-to-wallet link is signed and expires. A link that never expires is a permanent credential sitting in an email inbox.
  3. Pass updates go through NotificationChannelPort like any other channel (ADR-019), with its own adapter. Bypassing the cascade here would create a second, unaudited delivery path.
  4. Update pushes are coalesced: several balance changes within a short window produce one push. A member whose phone buzzes on every point earned will remove the pass.
  5. A member may hold the pass on several devices. Every registered device receives updates.
  6. Unregistering a device stops updates for it and never affects the others.
  7. The scannable code is a member identifier that is useless without authentication — scanning it grants nothing on its own. It is an identifier, not a bearer token.
  8. Pass updates are category product, not marketing — they are a state change on something the member deliberately installed.

Data (normative)

API (normative)

The platform callbacks follow Apple’s and Google’s own protocols, which are not ours to design. They are the one place in the module where the path shape is dictated externally.

Events (normative)

loyalty.wallet_pass.issued. Balance and tier changes are consumed from the existing events rather than generating new ones.

Acceptance criteria (normative)

  1. A generated .pkpass validates against Apple’s signature requirements and installs on a device.
  2. A Google Wallet object installs from the same link on Android.
  3. A balance change pushes an update within 60 s to every registered device.
  4. Ten balance changes inside the coalescing window produce exactly one push.
  5. Key rotation issues valid passes without invalidating installed ones.
  6. An expired add-to-wallet link is rejected with a typed error.
  7. Negative: the scannable code alone grants no access to any endpoint — proven by a test that attempts it unauthenticated.

Execution

Blocked on the signing-library ADR (DEC-H5). Asynchronous pipeline: generation and pushes run in backend/workers; the wallet update adapter is a NotificationChannelPort implementation.

Open questions

None outstanding. Every question this spec carried was answered in the consolidated register (../../../design/open-questions-v1.md, v1.1) and folded into the normative sections above. Still blocked, but not by a decision:

Changelog

Delivery record

Not implemented yet.