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.comwith 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
- Go to developer.apple.com · Identifiers · Pass Type IDs.
- Click the + button.
- Description:
Frontelio Access. - Identifier:
pass.com.missan.frontline.access. - Save.
Step 2 — Generate a CSR in Keychain Access
- On the Mac, open Keychain Access.
- Menu → Certificate Assistant → Request a Certificate From a Certificate Authority…
- User Email:
support@frontelio.com. Common Name:Frontelio Access — Pass Type. CA Email: leave blank. - Select Saved to disk. Save the
.certSigningRequestfile somewhere safe.
Step 3 — Sign the Pass Type certificate
- Back on developer.apple.com, click the Pass Type ID you just created.
- Click Create Certificate.
- Upload the
.certSigningRequestfrom step 2. - Download the resulting
pass.cerfile when prompted.
Step 4 — Export the cert as a password-protected .p12
- Double-click the downloaded
pass.cer— Keychain Access installs the cert + private key together under your login keychain. - In Keychain Access, find the cert (search
pass.com.missan.frontline.access). Right-click → Export… - File format:
Personal Information Exchange (.p12). Save asfrontelio-pass.p12. - 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:
curl -O https://www.apple.com/certificateauthority/AppleWWDRCAG4.cerConvert from DER to PEM with openssl:
openssl x509 -inform DER -in AppleWWDRCAG4.cer -out AppleWWDRCAG4.pemStep 6 — Base64-encode both files
# .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.txtStep 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:
openssl rand -hex 32Save 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:
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.comLeave 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:
cd /opt/fco/infra/docker
docker compose -f docker-compose.prod.yml up -d --force-recreate apiStep 9 — Test from the staff app
- 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 yetuntil Zones are granted — but does need one, and a QR-scanning reader, to actually open a door. - Tap More → My Access.
- Tap the black Add to Apple Wallet button below the QR card.
- iOS opens the Wallet "Add" sheet. Tap Add in the top-right.
- 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:
- Repeat steps 2–6 with a fresh CSR.
- Set the new
PASS_CERT_P12_BASE64+PASS_CERT_PASSWORDin env. - 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 failedwarnings; the cause follows on the same line.ENOTFOUND gateway.push.apple.commeans the API is still using the retired APNs host — setAPNS_GATEWAY_HOST=api.push.apple.comor remove the variable.APNs returned HTTPmeans 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
webServiceURLis 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 theirpass.json: delete them from Wallet and add them again after the fix. - Verify what the pass looks like — rename the downloaded
.pkpassto.zipand unzip;pass.jsonis human-readable and tells you whether the QR data, fields, and webServiceURL look right. The webServiceURL should end in/access/wallet/.