Browse docs
Frontelio Access · iOS

Apple Wallet pass setup.

Turn on the iOS Apple Wallet pass for Frontelio Access. The phone becomes the credential — add the pass to Apple Wallet, double-click the Side Button, and hold the QR code to a QR-scanning reader. Cert provisioning is one-time and takes about 30 minutes.

What the pass is

Apple doesn't let third-party apps emulate NFC cards (Android does — that's the HCE service Frontelio uses on Android). On iOS we use the supported alternative: a PassKit Pass containing a QR code. The worker adds the pass to Apple Wallet once, then opens it (double-click the Side Button) and holds the QR code up to a QR-scanning reader. That reader posts the QR contents to /access/verify, and Frontelio answers GRANT or DENY like it does for any other reader.

The QR code on the pass is a 24-hour credential JWT, the same kind Android broadcasts via HCE. Each time the pass is generated — when the worker adds it, and each time iOS re-fetches it — it carries a freshly minted 24-hour JWT; earlier ones are not revoked, they simply expire. Every night at 00:00 Asia/Dubai we send a silent APNs push to every registered device, and iOS quietly refetches the pass with the new QR. That nightly push is the only trigger: if it fails, or a device never registered, the QR on that pass stops working after 24 hours until the pass is updated or added again.

Prerequisites

  • Apple Developer Program membership for Frontelio (Team ID 55MVF9UKG9).
  • Access to developer.apple.com with sign-in rights to create Pass Type IDs and certificates.
  • A Mac with Keychain Access for the CSR + .p12 export step.
  • SSH access to the production API droplet to set env vars.

Provisioning walkthrough

Step 1 — Create a Pass Type ID

  1. Go to developer.apple.com · Identifiers · Pass Type IDs.
  2. Click the + button.
  3. Description: Frontelio Access.
  4. Identifier: pass.com.missan.frontline.access.
  5. Save.

Step 2 — Generate a CSR in Keychain Access

  1. On the Mac, open Keychain Access.
  2. Menu → Certificate Assistant → Request a Certificate From a Certificate Authority…
  3. User Email: support@frontelio.com. Common Name: Frontelio Access — Pass Type. CA Email: leave blank.
  4. Select Saved to disk. Save the .certSigningRequest file somewhere safe.

Step 3 — Sign the Pass Type certificate

  1. Back on developer.apple.com, click the Pass Type ID you just created.
  2. Click Create Certificate.
  3. Upload the .certSigningRequest from step 2.
  4. Download the resulting pass.cer file when prompted.

Step 4 — Export the cert as a password-protected .p12

  1. Double-click the downloaded pass.cer — Keychain Access installs the cert + private key together under your login keychain.
  2. In Keychain Access, find the cert (search pass.com.missan.frontline.access). Right-click → Export…
  3. File format: Personal Information Exchange (.p12). Save as frontelio-pass.p12.
  4. Set a strong password — keep this safe in 1Password as PASS_CERT_PASSWORD.

Step 5 — Download the Apple WWDR intermediate (G4)

The .pkpass signature chain needs Apple's WWDR Certificate Authority. Apple publishes the latest revision (G4) directly:

bash
curl -O https://www.apple.com/certificateauthority/AppleWWDRCAG4.cer

Convert from DER to PEM with openssl:

bash
openssl x509 -inform DER -in AppleWWDRCAG4.cer -out AppleWWDRCAG4.pem

Step 6 — Base64-encode both files

bash
# .p12 (binary) → single-line base64
base64 -i frontelio-pass.p12 -o pass-cert.b64.txt

# WWDR PEM (text already) → base64 to keep the env var clean
base64 -i AppleWWDRCAG4.pem -o wwdr.b64.txt

Step 7 — Generate a wallet auth secret

The pass embeds an authenticationToken Apple uses on every web-service call back to us. Frontelio computes it as the HMAC-SHA256 of tenantId:userId, keyed by one platform-wide secret: every worker gets a different token, but the whole deployment shares a single secret. Generate it once:

bash
openssl rand -hex 32

Save the output as WALLET_PASS_AUTH_SECRET in 1Password. Do not rotate it casually: changing it makes every pass already in Wallet fail authentication, so those passes stop refreshing until they are removed and added again.

Step 8 — Set env vars on the production droplet

SSH into the API droplet and edit /opt/fco/infra/docker/.env.prod — the env_file of the api service in docker-compose.prod.yml — and add:

bash
PASS_TYPE_ID=pass.com.missan.frontline.access
PASS_CERT_P12_BASE64=<paste contents of pass-cert.b64.txt>
PASS_CERT_PASSWORD=<the password you set in step 4>
WWDR_CERT_PEM_BASE64=<paste contents of wwdr.b64.txt>
APPLE_TEAM_ID=55MVF9UKG9
WALLET_PASS_AUTH_SECRET=<the openssl rand -hex 32 from step 7>

# Optional — the defaults are correct for production:
# WALLET_PASS_WEB_SERVICE_URL=https://api.frontelio.com/api/v1/access/wallet/
# APNS_GATEWAY_HOST=api.push.apple.com

Leave the two optional lines out unless you have a reason to change them. WALLET_PASS_WEB_SERVICE_URL must not end in /v1: Apple appends v1/devices/… to it itself. The APNs host must be api.push.apple.com; the old gateway.push.apple.com no longer exists.

Then recreate the API container. docker compose restart does not re-read env_file, so the new variables would not be applied and the endpoint would keep answering 503. The directory also holds a development docker-compose.yml with no api service, so always name the production file:

bash
cd /opt/fco/infra/docker
docker compose -f docker-compose.prod.yml up -d --force-recreate api

Step 9 — Test from the staff app

  1. Open the Frontelio staff app on an iPhone signed in as a worker in a tenant that has Frontelio Access enabled (Growth plan or higher, or enabled by an override). The worker does not need an access grant to add the pass — the back of the pass lists None yet until Zones are granted — but does need one, and a QR-scanning reader, to actually open a door.
  2. Tap More → My Access.
  3. Tap the black Add to Apple Wallet button below the QR card.
  4. iOS opens the Wallet "Add" sheet. Tap Add in the top-right.
  5. Done — the pass lives in Apple Wallet. Double-click the Side Button to surface it; the QR is a 24-hour credential that refreshes nightly.

Rotating the cert (every year)

Apple Pass Type certs are valid for 1 year. Track the renewal date when you set it up, and roughly two weeks before expiry:

  1. Repeat steps 2–6 with a fresh CSR.
  2. Set the new PASS_CERT_P12_BASE64 + PASS_CERT_PASSWORD in env.
  3. Recreate the API container with the command from step 8 (a plain restart does not re-read the env file) — the next pass issued uses the new cert. Existing devices auto-refresh on the next push.

Troubleshooting

  • "Apple Wallet pass generation is not configured" (503 from /access/wallet/pass.pkpass) — at least one of the env vars is missing. The error message lists each missing key by name.
  • iOS Wallet rejects the pass — usually a stale WWDR. Apple has rotated through G1 → G2 → G3 → G4; using the wrong version makes every modern iPhone refuse to add the pass. Re-download from the link in step 5.
  • Pass adds but daily refresh doesn't arrive — check the API logs for APNs push failed warnings; the cause follows on the same line. ENOTFOUND gateway.push.apple.com means the API is still using the retired APNs host — set APNS_GATEWAY_HOST=api.push.apple.com or remove the variable. APNs returned HTTP means Apple rejected the push. Another common cause: the Pass Type cert and the APNs identity cert have to be the SAME .p12 — we re-use it for both. If you used a separate cert for APNs you'll see TLS handshake failures.
  • Pass adds but never refreshes, and no device ever registers — the pass's webServiceURL is wrong. Apple calls <webServiceURL>v1/devices/…, so the URL must end in /access/wallet/. A URL ending in /wallet/v1/ makes Wallet call …/wallet/v1/v1/devices/…, which does not exist. Passes issued with that URL keep it inside their pass.json: delete them from Wallet and add them again after the fix.
  • Verify what the pass looks like — rename the downloaded .pkpass to .zip and unzip; pass.json is human-readable and tells you whether the QR data, fields, and webServiceURL look right. The webServiceURL should end in /access/wallet/.