The Forward Email backend sends remote notifications through Apple Push Notification service (APNs) for iOS, Firebase Cloud Messaging (FCM) for Google Play Android builds, and UnifiedPush for every Android build. Each alias-scoped event is delivered through WebSocket and push in parallel with the same immutable notification_id; the mail client treats them as two transports for one logical event, prefers WebSocket while foregrounded, and uses a bounded push fallback without duplicate notifications or refresh work.
The native client implementation and build profiles are documented in the mail.forwardemail.net push guide. Configure this repository first, then copy only the explicitly identified public values into the mail repository's build environment.
Event producers call the transport-neutral sendNotification helper. It assigns the immutable notification_id once and explicitly starts both delivery paths from that same payload: sendPushNotification queries and fans out to every active alias token, while Redis publication feeds the API WebSocket subscriber. The subscriber owns only socket delivery and forwards the unchanged envelope to matching connected clients.
An active WebSocket connection is not required for push delivery. Push starts directly inside sendNotification, before and independently of any Redis subscriber or socket lookup. Per-token provider attempts use bounded parallelism, so one slow or failed token cannot prevent the remaining active tokens from being attempted. A short-lived Redis SET NX claim keyed by notification_id suppresses duplicate provider fan-out if the same immutable notification envelope is retried. Global maintenance broadcasts without an alias remain intentionally WebSocket-only because there is no alias-scoped token set to target.
| Variable | Required for | Value source | Secret |
|---|---|---|---|
APPLE_TEAM_ID |
APNs | Apple Developer membership details | No |
APPLE_KEY_ID |
APNs | Identifier shown for the shared Apple services key | No |
APPLE_KEY_PATH |
APNs | Absolute server path to the downloaded shared .p8 key |
Yes: file contents |
APNS_BUNDLE_ID |
APNs | iOS bundle identifier; use net.forwardemail.mail |
No |
APNS_PRODUCTION |
APNs | true for distribution tokens; false for development/sandbox tokens |
No |
FCM_PROJECT_ID |
FCM | Firebase Project settings → General → Project ID | No |
FCM_SERVICE_ACCOUNT_PATH |
FCM | Absolute server path to a Firebase service-account JSON key | Yes: file contents |
VAPID_SUBJECT |
UnifiedPush | Operator contact URI, normally mailto:support@forwardemail.net |
No |
VAPID_PUBLIC_KEY |
UnifiedPush | Public half of the generated VAPID key pair | No |
VAPID_PRIVATE_KEY |
UnifiedPush | Private half of the generated VAPID key pair | Yes |
Reuse the existing Apple credentials. APNs deliberately uses the established
APPLE_KEY_ID,APPLE_TEAM_ID, andAPPLE_KEY_PATHvalues from Sign in with Apple. The same protected.p8file can therefore continue to be distributed by the existing Ansible deployment; do not introduce separate APNs-specific credential variables.
Run the existing certificate playbook from the repository root:
node ansible-playbook ansible/playbooks/certificates.yml --user deployIn addition to the required TLS files, the playbook prompts for two optional local credential paths:
/path/to/AuthKey_00000000000.p8
/path/to/firebase-service-account.json
The playbook validates each non-empty local path and copies both files with mode 0660 and owner deploy to the same /var/www/production directory on the applicable process hosts. The Apple key keeps its local basename, while the Firebase key is installed deterministically as /var/www/production/firebase-service-account.json. Set APPLE_KEY_PATH to /var/www/production/<apple-key-basename> and use the default FCM_SERVICE_ACCOUNT_PATH=/var/www/production/firebase-service-account.json. Leaving either prompt blank skips only that optional credential.
The Apple services key must have Apple Push Notifications service (APNs) enabled. If the existing Sign in with Apple key already has APNs enabled, reuse it without generating or deploying another key. Apple permits an APNs signing key to authenticate multiple apps, and an APNs signing key works with both development and production environments.1
To create or replace the shared key:
- Sign in to Apple Developer as an Account Holder or Admin.
- Open Certificates, Identifiers & Profiles → Keys and create a key.
- Enable and configure Apple Push Notifications service (APNs). Also retain or enable Sign in with Apple because this repository uses the same key for both services.
- Select the APNs environment and key scope required by the Apple account. A team-scoped key is appropriate when the same key serves multiple app topics.
- Confirm and download the
.p8file. Apple permits the private key to be downloaded only once, so store the original securely.2 - Copy the displayed 10-character Key ID into
APPLE_KEY_ID. - Copy the 10-character Team ID from Membership details into
APPLE_TEAM_ID. - Supply the local
.p8path whenansible/playbooks/certificates.ymlprompts for the Apple key. The playbook uploads it to/var/www/production/<local-basename>on every applicable process host. SetAPPLE_KEY_PATHto that absolute server path. - In Certificates, Identifiers & Profiles → Identifiers, open the
net.forwardemail.mailApp ID and enable Push Notifications. Regenerate the development and distribution provisioning profiles consumed by the mail repository.
Configure the backend with values such as:
APPLE_TEAM_ID=TEAM123456
APPLE_KEY_ID=ABC123DEFG
APPLE_KEY_PATH=/var/www/production/AuthKey_ABC123DEFG.p8
APNS_BUNDLE_ID=net.forwardemail.mail
APNS_PRODUCTION=trueAPPLE_KEY_PATH must identify the unencrypted .p8 provider key, not an App Store Connect API key, signing certificate, provisioning profile, or .p12 file. Keep APNS_BUNDLE_ID equal to the client bundle identifier. Use APNS_PRODUCTION=false for development-signed device builds and APNS_PRODUCTION=true for TestFlight, App Store, and other distribution builds.
FCM is optional for Google-free downstream builds, but the mail repository's GitHub release uses one dual-provider Play build containing both FCM and UnifiedPush. That release defaults to FCM at runtime until the user explicitly selects a UnifiedPush distributor. The mail repository's default and F-Droid build commands remain UnifiedPush-only and do not require Firebase or Google Play Services.
To obtain the two backend values:
- Open the Firebase console and create or select the project used by the Forward Email Android application.
- Open Project settings → General and copy the immutable Project ID into
FCM_PROJECT_ID. Do not use the display name, project number, or Android application ID. - Ensure the Firebase Cloud Messaging API is enabled for that project.
- Open Project settings → Service accounts and generate a new private key, or create a dedicated least-privilege service account in Google Cloud IAM and download its JSON key. Firebase documents Firebase Cloud Messaging API Admin as the role that permits sending to a target project.3
- Store the JSON outside the repository and supply its local path when
ansible/playbooks/certificates.ymlprompts for the Firebase service account. The playbook uploads it beside the Apple.p8file as/var/www/production/firebase-service-account.json, regardless of the local filename. - Confirm that the JSON key belongs to a service account authorized to send messages to the project named by
FCM_PROJECT_ID.
FCM_PROJECT_ID=forward-email-production
FCM_SERVICE_ACCOUNT_PATH=/var/www/production/firebase-service-account.jsonThe JSON file is a production credential. Never commit it, paste it into an issue, include it in a client build, or expose it through a public environment variable. The mail repository separately needs google-services.json; that client configuration file is not a substitute for this backend service-account key.
UnifiedPush subscriptions use Web Push-compatible encryption. Generate one stable VAPID key pair from this repository with the web-push CLI:
pnpm exec web-push generate-vapid-keysStore the generated values as follows:
| Generated or chosen value | Backend setting | Mail repository setting |
|---|---|---|
| Public key | VAPID_PUBLIC_KEY |
GitHub Actions variable and local build environment VAPID_PUBLIC_KEY |
| Private key | VAPID_PRIVATE_KEY |
Never copy to the client repository, Actions, APK, AAB, or CI logs |
| Contact URI | VAPID_SUBJECT |
Not required by the client |
VAPID_SUBJECT must be a contact URI controlled by the operator, normally mailto:support@forwardemail.net or an HTTPS URL. The public and private values must remain a matched pair.
VAPID_SUBJECT=mailto:support@forwardemail.net
VAPID_PUBLIC_KEY=BN...
VAPID_PRIVATE_KEY=...Treat the VAPID pair as long-lived application identity. Rotating it requires Android clients to obtain new UnifiedPush subscriptions. The public key is intentionally embedded in Android artifacts; the private key remains backend-only.
# Shared Sign in with Apple and APNs credentials
APPLE_TEAM_ID=TEAM123456
APPLE_KEY_ID=ABC123DEFG
APPLE_KEY_PATH=/var/www/production/AuthKey_ABC123DEFG.p8
# APNs delivery
APNS_BUNDLE_ID=net.forwardemail.mail
APNS_PRODUCTION=true
# Google Play Android delivery
FCM_PROJECT_ID=forward-email-production
FCM_SERVICE_ACCOUNT_PATH=/var/www/production/firebase-service-account.json
# UnifiedPush delivery
VAPID_SUBJECT=mailto:support@forwardemail.net
VAPID_PUBLIC_KEY=BN...
VAPID_PRIVATE_KEY=...Values belong in the deployment environment generated from .env.defaults and validated by .env.schema. File credentials must be deployed separately and referenced by absolute path; do not copy credential contents into the environment file.
After the backend is configured, provide the mail repository maintainers with exactly these non-secret values:
| Value | Mail repository destination |
|---|---|
VAPID_PUBLIC_KEY |
Actions variable and local build variable VAPID_PUBLIC_KEY |
Firebase project's Android google-services.json |
Base64-encode as Actions secret GOOGLE_SERVICES_JSON_BASE64, or point local GOOGLE_SERVICES_JSON to the file |
APPLE_TEAM_ID |
Existing Actions secret APPLE_TEAM_ID used for iOS signing |
Do not hand off APPLE_KEY_PATH contents, the APNs .p8 file, FCM_SERVICE_ACCOUNT_PATH contents, or VAPID_PRIVATE_KEY to the client repository. The iOS release has separate certificate, provisioning-profile, and App Store Connect values documented by the mail repository.
| Check | Expected result |
|---|---|
| Shared Apple credentials | APPLE_KEY_ID, APPLE_TEAM_ID, and APPLE_KEY_PATH are present and reused for APNs |
| Apple key file | APPLE_KEY_PATH is absolute, readable only by the service account, and contains the downloaded services .p8 key |
| APNs topic and environment | APNS_BUNDLE_ID=net.forwardemail.mail; APNS_PRODUCTION matches the client signing profile |
| Firebase project | FCM_PROJECT_ID matches the project_id in the client project's google-services.json |
| Firebase service account | The JSON file exists at FCM_SERVICE_ACCOUNT_PATH and its service account can send FCM HTTP v1 messages |
| VAPID pairing | VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY came from the same generation command |
| Client handoff | Mail Actions variable VAPID_PUBLIC_KEY exactly equals backend VAPID_PUBLIC_KEY |
| Secret boundary | No .p8, service-account JSON, VAPID private key, or base64 secret is tracked by Git |
The delivery helper sends FCM and UnifiedPush requests through the repository's hardened fetch path with the caller-provided Tangerine resolver. UnifiedPush endpoint validation rejects unsafe targets, and permanent provider responses participate in the normal token failure and pruning lifecycle.