DEV Community

Cover image for Building a Multi-Tenant Production-Grade Wallet Pass System for Google & Apple: A Complete Architectural & Implementation Guide
George Ikwegbu Chinedu
George Ikwegbu Chinedu

Posted on

Building a Multi-Tenant Production-Grade Wallet Pass System for Google & Apple: A Complete Architectural & Implementation Guide

Introduction

Your customers already carry everything on their phones — and their native digital wallet apps (Apple Wallet on iOS and Google Wallet on Android) are accessed daily. Building a production-grade system that issues real digital passes transforms customer engagement: digital loyalty cards, tickets, and membership cards that users add with a single tap, receive live dynamic updates on their lock screens, and scan at physical locations to redeem rewards or verify admission.

This guide provides an end-to-end, production-tested blueprint for architecting and implementing a dual-platform wallet pass engine. The concepts, cryptographic signing pipelines, API specifications, and database models presented here are deliberately stack-agnostic — applicable whether your backend is built with TypeScript/Node.js, Go, Python, Java, or Rust, and whether your database is PostgreSQL, MySQL, or a document store.

By the end of this guide, you will master:

  1. Google Wallet Architecture: From Google Cloud service account provisioning and Google Pay Console issuer setup to signing JWT passes and handling class inheritance.
  2. Apple Wallet & PassKit Architecture: Managing certificate authorities, .p12 to .pem extraction, WWDR G4 chains, APNs push channels, and strict PassKit validation rules.
  3. Local Development & Mobile Testing via HTTPS Tunneling (ngrok): How to satisfy Apple’s non-negotiable HTTPS requirement and Google’s JWT origin validation when testing on physical smartphones.
  4. Pass Design & Cryptographic QR Redemptions: Structuring fields, image bundles, and tamper-proof, short-lived QR codes.
  5. Multi-Tenant System Design: How a single platform issuer can host hundreds of individual brands with their own custom colors, logos, and points systems without requiring separate Apple or Google developer accounts.
  6. Customer Identity & Junction Data Modeling: Handling customers who belong to multiple businesses simultaneously without data leaks or duplicate pass collisions.

📂 Reference Implementation & Open Source Repository:
A complete, production-ready reference implementation of this architecture — featuring a NestJS backend, PostgreSQL/Prisma persistence, PassKit signing pipeline, and a Vue 3 Tailwind admin dashboard — is available on GitHub:
👉 https://github.com/gikwegbu/add_to_google_apple_wallet

Contributions, issue reports, and pull requests are warmly welcome!


Part 1 — Google Wallet: From Zero to Issuer

Google Wallet passes are powered by the Google Wallet REST API and authenticated using JSON Web Tokens (JWTs) signed with RS256 using an asymmetric Google Service Account key.

┌────────────────────────────────────────────────────────────────────────┐
│                        GOOGLE WALLET ECOSYSTEM                         │
├────────────────────────────────┬───────────────────────────────────────┤
│      GOOGLE CLOUD CONSOLE      │      GOOGLE PAY & WALLET CONSOLE       │
│  - Cloud Project               │  - Issuer Account (Issuer ID)         │
│  - Google Wallet API Enabled   │  - Service Account Authorization      │
│  - Service Account (RSA Key)   │  - Pass Class Definitions             │
└────────────────────────────────┴───────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

1.1 Create a Google Cloud Project

Every Google Wallet integration lives under a Google Cloud Project:

  1. Navigate to the Google Cloud Console.
  2. Create a new project (e.g., wallet-pass-platform). Choose a name reflecting your platform rather than a single client brand.
  3. Ensure billing is enabled for the project.

1.2 Enable the Google Wallet API

  1. In your Cloud Project, open APIs & Services → Library.
  2. Search for Google Wallet API.
  3. Click Enable. This activates the endpoints under walletobjects.googleapis.com.

1.3 Provision a Dedicated Service Account

Your backend server authenticates with Google using an automated Service Account identity:

  1. Open IAM & Admin → Service Accounts.
  2. Click Create Service Account, naming it descriptively (e.g., wallet-issuer-sa).
  3. Click into the newly created service account and navigate to the Keys tab.
  4. Select Add Key → Create new key → JSON.
  5. Download the JSON credential file and store it securely on your server (e.g., core/service-account.json). Never commit this file or its private key to version control.

The downloaded file contains:

  • client_email: The service account email (e.g., wallet-issuer-sa@project-id.iam.gserviceaccount.com).
  • private_key: The RSA 2048-bit private key (PEM format with \n characters).

1.4 Register in the Google Pay & Wallet Console

Having the Cloud Project API enabled is not enough. You must register as an authorized pass issuer:

  1. Go to the Google Pay & Wallet Console.
  2. Complete the business registration profile (Legal Business Name, Website, Point of Contact).
  3. Once approved, note your Issuer ID (a numeric string such as 3388000000023066256). This permanent identifier namespaces every class and object you ever produce.

1.5 Authorize the Service Account in the Wallet Console

The Cloud Console and the Pay & Wallet Console are independent security domains. You must bridge them:

  1. In the Google Pay & Wallet Console, navigate to the Users tab.
  2. Click Invite a user (or Add User).
  3. Paste the full client_email from your service account JSON file.
  4. Set the permission level to Developer or Admin.
  5. Save.

⚠️ Common Trap: If you omit this step, any API call or pass creation will fail with 401 Unauthorized: Request had invalid authentication credentials or 403 Forbidden: Not an issuer, even if the Service Account is Project Owner in Google Cloud.

1.6 Pass Classes vs. Pass Objects

Google Wallet separates templates from individual user passes:

  • Pass Class (Template): Defines the global layout, program name, brand logos, primary colors, review status, and callback URLs. Created once per brand/program.
  • Pass Object (Instance): Represents a single user's pass. References the classId and holds specific values (User's Name, Current Points Balance, Unique Barcode, Account ID).

Google supports several class types:

  • genericClass / genericObject: Best for flexible reward cards, club memberships, vouchers, and event tickets. Highly customizable.
  • loyaltyClass / loyaltyObject: Specifically tailored for retail points programs with structured tier labels.
  • eventTicketClass / eventTicketObject: Tailored for seated venue events with seat/gate numbers.

Class IDs follow the format: <Issuer_ID>.<custom_class_suffix> (e.g., 3388000000023066256.summer_rewards_class).

1.7 Generating the Save Link: The "Skinny" JWT Flow

You do not need to make an upfront REST API call to pre-create every object before a user saves it. Google provides an atomic, high-performance issuance pattern known as the Skinny JWT:

 ┌─────────────────┐       GET /google/pass/:id       ┌─────────────────┐
 │   User Browser  │ ───────────────────────────────> │  Your Backend   │
 │                 │ <─────────────────────────────── │  Signs JWT      │
 └─────────────────┘       302 Redirect to Save URL   └─────────────────┘
          │
          ▼
 ┌─────────────────┐
 │ Google Pay Save │ ──> Pass added to Google Wallet app instantly!
 └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

JWT Structure

The backend constructs an RS256-signed JWT containing the pass object and returns a 302 redirect:

// JWT Header
{
  "alg": "RS256",
  "typ": "JWT"
}

// JWT Claims Payload
{
  "iss": "wallet-issuer-sa@project-id.iam.gserviceaccount.com",
  "aud": "google",
  "typ": "savetowallet",
  "iat": 1729600000,
  "origins": [
    "http://localhost:3000",
    "https://your-domain.ngrok-free.dev"
  ],
  "payload": {
    "genericObjects": [
      {
        "id": "3388000000023066256.user-uuid-1234",
        "classId": "3388000000023066256.summer_rewards_class",
        "state": "ACTIVE",
        "cardTitle": {
          "defaultValue": { "language": "en-US", "value": "Reisty Rewards" }
        },
        "header": {
          "defaultValue": { "language": "en-US", "value": "George Ikwegbu" }
        },
        "textModulesData": [
          {
            "id": "points",
            "header": "POINTS",
            "body": "120"
          }
        ],
        "barcode": {
          "type": "QR_CODE",
          "value": "<signed_qr_payload>",
          "alternateText": "Scan at terminal"
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Critical Requirement: The origins claim is required by Google. It must match the domain or IP from which the user initiates the Save flow. For local and mobile testing, include your HTTPS tunnel domain (see Part 4).

The user is redirected to:
https://pay.google.com/gp/v/save/<signed_jwt>

When clicked on Android or desktop Chrome, Google verifies the cryptographic signature against your Service Account public key, provisions the object on their servers, and adds it to the user's Google Wallet immediately.


Part 2 — Apple Wallet: Developer Setup, PassKit & Certificates

Apple Wallet passes (.pkpass) are signed ZIP archives verified by iOS using Public Key Cryptography Standards (PKCS#7). iOS strictly validates the signature against Apple's Worldwide Developer Relations (WWDR) Certificate Authority.

                      APPLE PASSKIT SIGNING PIPELINE

  pass.json + icon.png + logo.png
               │
               ▼
  Generate manifest.json (SHA-1 checksum of every asset)
               │
               ▼
  Sign manifest.json using Pass Certificate + Private Key + WWDR G4
  (Generates detached PKCS#7 signature)
               │
               ▼
  Compress into ZIP archive named <user_name>_pass.pkpass
Enter fullscreen mode Exit fullscreen mode

2.1 Apple Developer Setup

  1. Enroll in the Apple Developer Program (annual $99 membership).
  2. Locate your Team ID under Account → Membership Details (a 10-character alphanumeric string like A1B2C3D4E5).

2.2 Register a Pass Type Identifier

  1. Go to Certificates, Identifiers & Profiles → Identifiers.
  2. Click +, select Pass Type IDs, and click Continue.
  3. Provide a Description (e.g. Platform Loyalty Passes) and an Identifier formatted as pass.<organization>.<product> (e.g. pass.com.yourcompany.wallet).
  4. Register the identifier.

2.3 Create & Export Signing Certificates

Passes must be signed by a certificate issued specifically for your Pass Type ID.

  1. In Keychain Access on macOS, select Certificate Assistant → Request a Certificate from a Certificate Authority.
    • Enter your email, select Saved to disk, and leave CA Email blank. This creates a CertificateSigningRequest.certSigningRequest (CSR).
  2. In the Apple Developer portal under Identifiers → Pass Type IDs, select your ID and click Create Certificate.
  3. Upload your CSR. Download the resulting pass.cer.
  4. Double-click pass.cer to install it into Keychain Access. > 💡 Keychain Tip: Ensure the certificate is installed in the login keychain, NOT the System keychain; otherwise, macOS will grey out the .p12 export option.
  5. In Keychain Access, right-click the Pass Type ID certificate and choose Export "Pass Type ID: ...".
  6. Export as .p12 format (e.g. pass.p12). Enter an export password — this is your APPLE_CERT_PASSPHRASE.

2.4 Convert to Server-Ready PEM Files

Convert the .p12 file and Apple’s root authority certificate into unencrypted PEM files for your backend:

# 1. Extract the public pass certificate
openssl pkcs12 -in pass.p12 -clcerts -nokeys -out passcertificate.pem

# 2. Extract the unencrypted private key
openssl pkcs12 -in pass.p12 -nocerts -nodes -out passkey.pem

# 3. Download Apple Worldwide Developer Relations (WWDR) G4 Certificate
curl -O https://www.apple.com/certificateauthority/AppleWWDRCAG4.cer

# 4. Convert WWDR from DER to PEM
openssl x509 -inform DER -in AppleWWDRCAG4.cer -out wwdr.pem
Enter fullscreen mode Exit fullscreen mode

Store passcertificate.pem, passkey.pem, and wwdr.pem in core/apple/certs/. Add this folder to .gitignore.

2.5 The .pkpass Package Anatomy & Mandatory Assets

A .pkpass bundle is a standard ZIP file with an exact file hierarchy:

George_Ikwegbu_pass.pkpass
├── pass.json          ← Data structure, text fields, colors, serial number
├── icon.png           ← MANDATORY: 29x29px (shown in iOS notifications & pass list)
├── icon@2x.png        ← MANDATORY: 58x58px
├── icon@3x.png        ← MANDATORY: 87x87px
├── logo.png           ← MANDATORY: 160x50px (pass header face)
├── logo@2x.png        ← MANDATORY: 320x100px
├── manifest.json      ← SHA-1 checksum map of every file above
└── signature          ← Detached PKCS#7 signature of manifest.json
Enter fullscreen mode Exit fullscreen mode

⚠️ Strict Asset Rules:

  • Apple PassKit will fail with SyntaxError: Missing required image icon.png if icon.png is absent from the bundle template.
  • Never include manifest.json or signature inside manifest.json.

2.6 Apple PassKit Validation Rules & Gotchas

When building with libraries such as @walletpass/pass-js (Node), passkit-generator (Python), or passbook (Ruby), you must satisfy two critical Apple requirements:

  1. authenticationToken Length Requirement: Apple PassKit mandates that if a pass defines a webServiceURL, the authenticationToken must be at least 16 characters long.
   // ❌ FAILS: Throws TypeError: authenticationToken must be 16 characters or longer
   pass.authenticationToken = 'auth-' + Math.random().toString(36).substring(7);

  // ✅ CORRECT: Increase it
   pass.authenticationToken = 'auth-' + Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);

   // ✅ CORRECT: Cryptographically random 32-character hex token
   pass.authenticationToken = crypto.randomBytes(16).toString('hex');
Enter fullscreen mode Exit fullscreen mode
  1. Apple Web Service Protocol Requires HTTPS: Apple devices strictly refuse to register passes or fetch background updates if webServiceURL uses plain http://. It must be HTTPS with a valid TLS certificate.

Part 3 — Testing Live Passes Locally via HTTPS Tunneling (ngrok)

Developing wallet passes locally presents a challenge:

  • Apple Wallet on iOS requires a public HTTPS webServiceURL to register passes and receive background push updates.
  • Google Wallet JWT tokens enforce an origins check and require publicly resolvable image URLs.
  • Testing on physical iPhones and Android devices requires your computer’s local dev server to be reachable from mobile devices.

This is resolved using an HTTPS tunnel such as ngrok.

 ┌─────────────────┐       HTTPS Requests       ┌──────────────┐      Port 3000      ┌──────────────┐
 │ Physical Phone  │ ─────────────────────────> │ ngrok Tunnel │ ──────────────────> │ Local Server │
 │ (iOS / Android) │ <───────────────────────── │ Public HTTPS │ <────────────────── │ (Nest / Exp) │
 └─────────────────┘                            └──────────────┘                     └──────────────┘
Enter fullscreen mode Exit fullscreen mode

3.1 Starting the Tunnel

Launch an HTTPS tunnel targeting your local backend server port (e.g. port 3000):

# Standard dynamic tunnel
ngrok http 3000

# Or using a persistent static domain (recommended)
ngrok http --url=https://your-domain.ngrok-free.dev 3000
Enter fullscreen mode Exit fullscreen mode

ngrok will output a secure HTTPS endpoint:
https://your-domain.ngrok-free.dev -> http://localhost:3000

3.2 Configuring the Backend

Update your server configuration (e.g. core/.env):

PORT=3000
API_BASE_URL=https://your-domain.ngrok-free.dev
Enter fullscreen mode Exit fullscreen mode

In your pass generation logic:

  1. Google Wallet: Add process.env.API_BASE_URL to the JWT origins array:
   origins: [this.configService.get('API_BASE_URL'), 'http://localhost:3000']
Enter fullscreen mode Exit fullscreen mode
  1. Apple Wallet: PassKit sets the registration URL dynamically:
   const webServiceURL = `${this.configService.get('API_BASE_URL')}/api/apple`;
   if (webServiceURL.startsWith('https://')) {
       pass.webServiceURL = webServiceURL;
       pass.authenticationToken = crypto.randomBytes(16).toString('hex');
   }
Enter fullscreen mode Exit fullscreen mode

Now, clicking pass generation links on physical devices or in your web dashboard allows iOS and Android to connect directly to your local workstation.


Part 4 — Interactive Pass Endpoints & Frontend UX

A clean user experience offers dedicated chips for both mobile ecosystems:

┌────────────────────────────────────────────────────────────────────────┐
│ USER                POINTS     PASSES                        ACTIONS   │
├────────────────────────────────────────────────────────────────────────┤
│ James Groka         ° 80       [ ● GOOGLE ]   [ ● APPLE ]    [⎘] [🗑] [⋮] │
└────────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

4.1 Server Pass Endpoints

Google Pass Redirect Endpoint

GET /google/pass/:userId

  1. Looks up the user and active pass configuration in the database.
  2. Generates the signed Google JWT.
  3. Responds with 302 Found redirecting directly to https://pay.google.com/gp/v/save/<jwt>.
  4. Clicking this link with target="_blank" on desktop or mobile opens the native Google Wallet save dialogue.

Apple Pass Download Endpoint

GET /apple/pass/:userId

  1. Looks up the user and active pass template.
  2. Injects the user’s name, points, barcode, and unique serial number into pass.json.
  3. Signs and compresses the .pkpass bundle in memory.
  4. Sanitizes the user's name for the download filename (e.g. George_Ikwegbu_pass.pkpass).
  5. Returns binary data with standard PassKit headers:
   HTTP/1.1 200 OK
   Content-Type: application/vnd.apple.pkpass
   Content-Disposition: attachment; filename="George_Ikwegbu_pass.pkpass"
   Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Enter fullscreen mode Exit fullscreen mode

4.2 Frontend Chip Design & Interaction

In your frontend dashboard (Vue, React, or standard HTML), implement two distinct pills matching platform conventions:

  • Google Chip: Light grey/white pill (#F8FAFB), subtle border (border-slate-200), rounded rectangle (rounded-lg), uppercase bold label, and a blue indicator dot (#1A73E8).
  <a 
    :href="`${apiBaseUrl}/google/pass/${user.id}`" 
    target="_blank" 
    rel="noopener noreferrer"
    class="inline-flex items-center px-3 py-1.5 bg-[#F8FAFB] hover:bg-slate-100 border border-slate-200 rounded-lg text-[11px] font-bold text-slate-700 tracking-wider"
  >
    <span class="w-2 h-2 rounded-full bg-[#1A73E8] mr-2"></span>
    GOOGLE
  </a>
Enter fullscreen mode Exit fullscreen mode
  • Apple Chip: Identical pill container with a dark slate/black indicator dot (#0F172B).
  <a 
    :href="`${apiBaseUrl}/apple/pass/${user.id}`" 
    :download="`${user.fullname.replace(/\s+/g, '_')}_pass.pkpass`"
    class="inline-flex items-center px-3 py-1.5 bg-[#F8FAFB] hover:bg-slate-100 border border-slate-200 rounded-lg text-[11px] font-bold text-slate-700 tracking-wider"
  >
    <span class="w-2 h-2 rounded-full bg-[#0F172B] mr-2"></span>
    APPLE
  </a>
Enter fullscreen mode Exit fullscreen mode

When clicked:

  • The Google chip opens a new tab that immediately redirects into Google Wallet's cloud save prompt.
  • The Apple chip triggers a browser download of the .pkpass bundle on desktop, or launches the Apple Wallet installation sheet on iOS Safari.

Part 5 — Designing Passes & Tamper-Proof QR Codes

5.1 Barcodes & Cryptographic Redemptions

A common security vulnerability in loyalty passes is encoding raw customer IDs in the QR code (e.g. {"userId": "123"}). A user can screenshot their QR code or copy another customer’s ID and redeem unauthorized points.

Secure Tokenized QR Architecture:

  1. Server Signs a Short-Lived JWT for the QR:
   {
     "sub": "user-uuid-123",
     "tenantId": "tenant-uuid-456",
     "iat": 1729600000,
     "exp": 1729603600
   }
Enter fullscreen mode Exit fullscreen mode
  1. Scanner Validates Authenticity: When the merchant scans the QR code at the terminal:
    • Validates the cryptographic signature against the platform secret.
    • Verifies exp has not lapsed.
    • Atomically decrements points in the database.
    • Triggers background pass balance updates to both Apple and Google.

5.2 Dynamic Pass Updates

When a customer’s balance updates:

  • Google Wallet: Execute a PATCH request to https://walletobjects.googleapis.com/walletobjects/v1/genericObject/<objectId> updating the text modules. Google pushes an automated lock-screen update.
  • Apple Wallet:
    1. Update the database record.
    2. Send an APNs background push notification containing an empty JSON body {} to all registered device tokens for that pass serial number.
    3. iOS wakes up silently, calls your server's GET /v1/passes/:passTypeIdentifier/:serialNumber, and replaces the .pkpass bundle on device.

Part 6 — Multi-Tenant Architecture: One Platform, Hundreds of Brands

In a multi-tenant platform (SaaS for restaurants, fitness studios, or ticketing agencies), each business wants its own logo, colors, and rewards structure.

You do not need separate Apple Developer accounts or Google Cloud projects for each tenant. A single platform issuer can host hundreds of branded passes.

Your Platform (Issuer)
│   Google Issuer ID: 3388000000023066256
│   Apple Pass Type ID: pass.com.yourplatform.wallet
│
├── Tenant A: Restaurant Rosso
│     Google Class: 3388000000023066256.rosso_loyalty
│     Apple Template: templates/rosso/pass.json + red branding
│     Customers receive Rosso-branded passes
│
└── Tenant B: Café Verde
      Google Class: 3388000000023066256.verde_loyalty
      Apple Template: templates/verde/pass.json + green branding
      Customers receive Verde-branded passes
Enter fullscreen mode Exit fullscreen mode

6.1 Tenant Class ID Convention

Adopt deterministic namespacing tied to your internal database UUIDs:

Google Class ID:    <Issuer_ID>.<tenant_uuid>_loyalty
Apple Template:     storage/tenants/<tenant_uuid>/pass.json
Apple Icons/Logos:  storage/tenants/<tenant_uuid>/logo.png
Enter fullscreen mode Exit fullscreen mode

When a business updates its logo or primary color in your admin portal:

  • Google: Call PATCH /walletobjects/v1/genericClass/<classId> with the new asset URLs. Google propagates the change to all existing customer passes automatically.
  • Apple: Update the tenant's asset directory and trigger APNs push notifications to update devices.

Part 7 — One Customer, Many Tenants: The Junction Model

Customers frequently visit multiple businesses on the same platform. A user might hold a loyalty pass for Restaurant Rosso and another for Café Verde.

7.1 Relational Data Model

Decouple the global user profile from their tenant-specific membership using a Junction Record:

User Account: george@example.com (Global ID: user-101)
│
├── Tenant Membership (Rosso)
│     points: 80
│     googleObjectId: 3388000000023066256.rosso_user_101
│     appleSerialNumber: rosso_user_101
│
└── Tenant Membership (Verde)
      points: 30
      googleObjectId: 3388000000023066256.verde_user_101
      appleSerialNumber: verde_user_101
Enter fullscreen mode Exit fullscreen mode
-- Relational Schema Representation
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    fullname VARCHAR(255),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE tenant_memberships (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    points INTEGER DEFAULT 0,
    google_pass_id VARCHAR(255),
    apple_pass_id VARCHAR(255),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    UNIQUE(user_id, tenant_id)
);
Enter fullscreen mode Exit fullscreen mode

7.2 Idempotent Pass Issuance

To prevent duplicate passes when a user clicks "Add to Wallet" multiple times, employ an upsert pattern:

POST /passes/issue
Body: { userId, tenantId, platform }

1. Upsert tenant_memberships WHERE user_id = userId AND tenant_id = tenantId.
2. If membership already possesses a valid pass ID:
   -> Return existing Google Save URL or stream existing Apple .pkpass.
3. If new membership:
   -> Mint unique pass IDs: `${tenant_id}_${user_id}`
   -> Persist pass ID in membership record.
   -> Return generated Save URL / .pkpass.
Enter fullscreen mode Exit fullscreen mode

Conclusion & Production Checklist

Building a unified wallet pass system bridges digital interaction with real-world physical verification. By structuring your backend around standards-compliant JWTs for Google Wallet, strictly validated PassKit packages for Apple Wallet, and an HTTPS tunnel for live testing, you create a robust, production-ready foundation.

Production Pre-Flight Checklist

  • [ ] Google Service Account: Authorized as Developer/Admin in the Google Pay & Wallet Console.
  • [ ] JWT Origins: Configured with your live production HTTPS domain.
  • [ ] Apple Certificates: Extracted into passcertificate.pem, passkey.pem, and wwdr.pem.
  • [ ] PassKit Asset Validation: Template contains icon.png (29x29) and logo.png (160x50) in all pixel densities.
  • [ ] PassKit Token: authenticationToken is verified to be $\ge 16$ characters.
  • [ ] Security & .gitignore: All .pem, .p12, service account JSON files, and environment files (.env, .env.*) are untracked and excluded from version control, leaving only .env.example and pass.json.example.
  • [ ] Local Testing: Verified live pass installation on physical iOS and Android devices using ngrok HTTPS tunneling.

Open Source Repository & Community Contributions

The complete codebase backing this guide — including the backend pass generation controllers, Apple PassKit signing pipeline, and the Vue 3 Tailwind admin dashboard — is open-source on GitHub:

👉 https://github.com/gikwegbu/add_to_google_apple_wallet

We Welcome Contributions!

This project is open-source and actively evolving. If you would like to contribute:

  • Fork the repository on GitHub.
  • Submit Pull Requests for new features, bug fixes, automated pass push notifications, or documentation improvements.
  • Open Issues to report bugs or suggest enhancements.
  • If this project helped you or your team, consider giving it a ⭐ on GitHub!

Top comments (0)