Docs
Wallet Pass

Wallet Pass

This is the self-host app to manage Apple and Google wallet pass

Features

  • Create and Update Apple and Google wallet pass
  • Currently storeCard for Apple and generic pass for Google is supported
  • Dynamic barcode URL proxy without need to update the pass content
  • Push notification to user device

Dependencies

  • Postgres DB
  • S3 storage
  • Google developer account
  • Apple developer account

Configuration

DB provision

The project use drizzle-orm to manage DB migration, to provision DB

  1. Create a Postgres database and set the following env vars accordingly
  • DB_HOST
  • DB_PORT
  • DB_USER
  • DB_PASSWORD
  • DB_DATABASE
  1. Run the migration script
    > npm run migrate

Project configuration

Create a Apple pass and certificate

Follow this instruction

Create a Google pass and key

Follow this instruction

Add a new project configuration

  1. create new project, POST /projects with a name
{
  "project": "your-project"
}

remember the api key returned in the response

  1. update project configuration, PUT /projects with x-stl-key set to the api key from last step, request body
{
  "config": {
    "walletPass": {
      "webhookUrl": "string", // the API endpoint to receive subscribed events
      "subscribedEvents": [
        "string" // pass:register, pass:unregister, when a pass is installed/removed on a device
      ],
      "google": {
        "issuerId": "string", // from your Google wallet pass setup
        "passTypeIdentifier": "string", // from your Google wallet pass setup
        "passType": "string", // "generic" is supported as of now
        "credentials": "string" // json content of secret key from Google, the private key will be encrypted when storing in DB
      },
      "apple": {
        "teamIdentifier": "string", // from your Apple wallet pass setup
        "passTypeIdentifier": "string", // from your Apple wallet pass setup
        "passType": "string", // "storeCard" is supported as of now
        "cert": "string", // certificate content
        "key": "string", // certificate private key content, the key will be encrypted when storing in DB
        "keySecret": "string" // key secret if available
      }
    }
  }

Pass template file

Upload your pass base template file to s3 bucket according to your design, example can be found here

The service will try to find the template under S3_BUCKET/wallet-pass/TEMPLATE_ID/template.{json|zip}, where S3_BUCKET is configurable via env var, and TEMPLATE_ID is provided by the client request when creating the wallet pass

How to use

Create wallet pass

POST /wallet-passes

for Apple

{
  "id": "EXTERNAL_ID", // set an external id to identify a wallet pass, e.g. project-apple-1
  "callbackUrl": "http://127.0.0.1:3006/webhooks/wallet-pass-created", // callback endpoint when wallet pass details once creation is successful, payload will be explained later
  "params": {
    "templateId": "e7e74bbd-5a14-4a43-95a6-cfdf2f8eeb16", // template ID for the template file to use, which is uploaded in previous step
    "platform": "apple", // "apple" or "google"
    "barcode": {
      "redirect": {
        "url": "XXX" // url for the barcode to show on wallet pass card
      },
      "altText": "XXX Pass"
    },
    "externalId": "EXTERNAL_ID", // set an external id to identify a wallet pass, e.g. project-apple-1
    "pass": {
      // this section depends on your wallet pass design, please check the vendor's offical document for details
      "description": "XXX",
      "backFields": [
        {
          "key": "note",
          "value": "XXX"
        },
        {
          "key": "website",
          "label": "Link",
          "attributedValue": "XXX",
          "value": "XXX"
        }
      ],
      "headerFields": [
        {
          "label": "XXX",
          "textAlignment": "PKTextAlignmentRight",
          "key": "XXX",
          "value": "XXX"
        }
      ],
      "primaryFields": [],
      "secondaryFields": [
        {
          "label": "XXX",
          "textAlignment": "PKTextAlignmentLeft",
          "key": "XXX",
          "value": "XXX"
        },
        {
          "label": "XXX",
          "textAlignment": "PKTextAlignmentLeft",
          "key": "XXX",
          "value": "XXX"
        }
      ]
    }
  }
}

for Google

{
  "id": "EXTERNAL_ID", // set an external id to identify a wallet pass, e.g. project-google-1
  "callbackUrl": "http://127.0.0.1:3006/webhooks/wallet-pass-created", // callback endpoint when wallet pass details once creation is successful, payload will be explained
  "params": {
    "templateId": "e7e74bbd-5a14-4a43-95a6-cfdf2f8eeb16", // template ID for the template file to use, which is uploaded in previous step
    "platform": "google", // "apple" or "google"
    "barcode": {
      "redirect": {
        "url": "XXX" // url for the barcode to show on wallet pass card
      },
      "altText": "XXX"
    },
    "externalId": "EXTERNAL_ID", // set an external id to identify a wallet pass, e.g. project-apple-1
    "pass": {
      // this section depends on your wallet pass design, please check the vendor's offical document for details
      "logo": {
        "sourceUri": {
          "uri": "XXX URL"
        }
      },
      "heroImage": {
        "sourceUri": {
          "uri": "XXX URL"
        }
      },
      "linksModuleData": {
        "uris": [
          {
            "description": "XXX",
            "id": "website",
            "uri": "XXX URL"
          }
        ]
      },
      "hexBackgroundColor": "#ffffff",
      "cardTitle": {
        "defaultValue": {
          "language": "en",
          "value": "XXX"
        }
      },
      "subheader": {
        "defaultValue": {
          "language": "en",
          "value": "XXX"
        }
      },
      "header": {
        "defaultValue": {
          "language": "en",
          "value": "XXX"
        }
      },
      "textModulesData": [
        {
          "id": "XXX",
          "header": "XXX",
          "body": "XXX"
        },
        {
          "id": "XXX",
          "header": "XXX",
          "body": "XXX"
        }
      ]
    }
  }
}

for the callbackUrl, it will be called when the wallet pass is successfully created, and the payload is like

{
  "id": "EXTERNAL_ID", // the external id passed during create request
  "result": {
    "id": "XXX", // PRIMARY_KEY in wallet pass db for reference, uuid
    "fileURL": "${env.ROOT_URL}/link/wallet-pass/${primaryKey}", // the url to install the wallet pass
    "platform": "XXX", // "apple" or "google"
    "createdAt": "XXX", // timestamp
    "externalId": "EXTERNAL_ID" // the external id passed during create request
  },
  "signedMessage": "0xXXXXX"
}

the signedMessage is created with ethers wallet crypto signature

const wallet = new ethers.Wallet(env.SIGNATURE_PRIVATE_KEY);
const messageToSign = `${task.id}-${JSON.stringify(result.result)}`;
const signedMessage = await wallet.signMessage(messageToSign);

it's used as an authentication mechanism for client to verify the callback request

Update wallet pass

PUT /wallet-passes/

for Apple

{
  "id": "a409cc64-7c12-4f36-97c2-efc0d1a6e206", // PRIMARY_KEY in wallet pass db for reference, uuid
  "params": {
    "pass": {
      // this section depends on your wallet pass design, please check the vendor's offical document for details
      "backFields": [
        {
          "key": "note",
          "value": "XXX"
        },
        {
          "key": "website",
          "label": "Link",
          "attributedValue": "XXX",
          "value": "XXX"
        },
        {
          // Notification info
          "key": "notification",
          "label": "XXX",
          "value": "XXX",
          "changeMessage": "%@",
          "textAlignment": "PKTextAlignmentNatural"
        }
      ],
      "primaryFields": [],
      "secondaryFields": [
        {
          "label": "XXX",
          "textAlignment": "PKTextAlignmentLeft",
          "key": "XXX",
          "value": "XXX"
        },
        {
          "label": "XXX",
          "textAlignment": "PKTextAlignmentLeft",
          "key": "XXX",
          "value": "XXX"
        }
      ],
      "headerFields": [
        {
          "label": "XXX",
          "textAlignment": "PKTextAlignmentRight",
          "key": "XXX",
          "value": "XXX"
        }
      ]
    }
  }
}

for Google

{
  "id": "daf26924-277c-4037-b6a7-c2b3061bb279", // PRIMARY_KEY in wallet pass db for reference, uuid
  "params": {
    "message": {
      // Notification info
      "header": "XXX",
      "body": "XXX",
      "id": "XXX",
      "message_type": "TEXT_AND_NOTIFY"
    },
    "pass": {
      "logo": {
        "sourceUri": {
          "uri": "XXX URL"
        }
      },
      "heroImage": {
        "sourceUri": {
          "uri": "XXX URL"
        }
      },
      "linksModuleData": {
        "uris": [
          {
            "description": "XXX",
            "id": "website",
            "uri": "XXX URL"
          }
        ]
      },
      "hexBackgroundColor": "#ffffff",
      "cardTitle": {
        "defaultValue": {
          "language": "en",
          "value": "XXX"
        }
      },
      "subheader": {
        "defaultValue": {
          "language": "en",
          "value": "XXX"
        }
      },
      "textModulesData": [
        {
          "id": "XXX",
          "header": "XXX",
          "body": "XXX"
        },
        {
          "id": "XXX",
          "header": "XXX",
          "body": "XXX"
        }
      ],
      "header": {
        "defaultValue": {
          "language": "en",
          "value": "XXX"
        }
      }
    }
  }
}