Skip to content

App Store and Google Play subscriptions ​

The native app uses expo-iap with StoreKit 2 and Google Play Billing. Verification runs on the API. Neither a client plan nor client expiry is trusted. Account UUIDs bind Apple appAccountToken and Google obfuscatedAccountId. Only pseudonymous billing identifiers and receipts go to the stores; no family content is sent.

API secrets ​

Configure secrets on the Coolify API service, never in an Expo public variable:

VariableValue
BILLING_SECRET_KEY32-byte base64 key, generated with openssl rand -base64 32
APP_STORE_PLUS_PRODUCT_IDOptional Plus monthly subscription product ID (69 NOK)
APP_STORE_PRODUCT_IDMonthly auto-renewable subscription product ID
APP_STORE_KEY_IDApp Store Connect In-App Purchase key ID
APP_STORE_ISSUER_IDIssuer ID
APP_STORE_PRIVATE_KEYPrivate .p8 contents; literal \n or real newlines supported
APP_STORE_APP_IDNumeric Apple app ID
APP_STORE_ENVIRONMENTProduction (default) or Sandbox
PLAY_PRODUCT_IDGoogle Play monthly subscription ID
PLAY_SERVICE_ACCOUNT_JSONService-account JSON with client_email and private_key
PLAY_NOTIFICATION_AUDIENCEExact aud value configured for authenticated Pub/Sub push
PLAY_NOTIFICATION_SERVICE_ACCOUNTEmail used by Pub/Sub to mint its OIDC push token
PLAY_NOTIFICATION_SUBSCRIPTIONExact Pub/Sub resource, e.g. projects/PROJECT/subscriptions/NAME

Leave the product ID empty to disable its platform. If a product ID is set, incomplete credentials fail startup. A billing encryption key is required when either store is enabled. Keep this key across restarts and backup restoration; losing it prevents rechecking stored receipts. Rotate by decrypting with the old key and resealing with the new one before switching. Google notification authentication is disabled unless all three PLAY_NOTIFICATION_* values are set. The Google notification endpoint also requires the configured BILLING_SECRET_KEY so the API can decrypt only receipts already bound to accounts.

Retained receipts are AES-256-GCM sealed in the account's subscription and persisted in Norway. Apple stores the signed transaction; Android stores the purchase token. API responses and user exports exclude these credentials. Account erasure removes them. Do not log request bodies, tokens, or credentials in proxies or error reporting.

Apple ​

  1. Register no.fellesly.app, complete paid-app agreements, tax and banking details in App Store Connect.
  2. Create two monthly auto-renewable subscriptions in the same group: With assistant at 39 NOK/month and Plus at 69 NOK/month. Rank Plus above With assistant for upgrades. Set APP_STORE_PRODUCT_ID and APP_STORE_PLUS_PRODUCT_ID to the corresponding IDs. Set the localized prices and product descriptions. Do not enable Family Sharing: access is through Fellesly membership.
  3. Create an In-App Purchase API key and supply the key ID, issuer, .p8, numeric app ID and subscription product ID to the API.
  4. Build a new signed app including the expo-iap plugin (pnpm --filter @fellesly/app exec eas build --platform ios).
  5. Create sandbox testers and test against an isolated API with NODE_ENV=development and APP_STORE_ENVIRONMENT=Sandbox. Production deployments reject Sandbox configuration. TestFlight uses sandbox transactions too; point test builds at the isolated API.

Apple's official server library verifies the certificate chain, JWS, bundle, environment and app ID. Apple root certificates bundled from Apple PKI are public trust anchors, not secrets. Current status is fetched from the App Store Server API; grace-period renewal information is also signature-verified.

Google Play ​

  1. Register the app with package no.fellesly.app in Play Console.
  2. Create a monthly auto-renewing base plan under the configured subscription product and activate it. Keep the initial product limited to one monthly base plan; alternate plans and paid credit packs are not implemented.
  3. Enable the Google Play Android Developer API in the service account's Cloud project. Grant the service account access only to this app in Play Console, including the permissions needed to view orders/subscriptions. The backend only performs subscription reads; native finishTransaction acknowledges the verified purchase.
  4. Create a dedicated service-account JSON key and save it as PLAY_SERVICE_ACCOUNT_JSON.
  5. Upload a signed Android build to an internal test track. Add license testers and install through Google Play. Use an isolated nonproduction API: production rejects test purchases.

The backend obtains a cached OAuth token for androidpublisher, calls purchases.subscriptionsv2.get, validates the product and account binding, and reads the expiry from the store. Active, grace-period and canceled-but-unexpired subscriptions remain entitled. Pending, paused, on-hold and expired subscriptions do not.

Store notification endpoints ​

Register these callback URLs on the API host:

  • Apple App Store Server Notifications V2: https://API_HOST/v1/billing/notifications/apple. Configure the production and sandbox URLs as appropriate in App Store Connect.
  • Google Play RTDN: https://API_HOST/v1/billing/notifications/google. Publish RTDN to a Google Cloud Pub/Sub topic, then configure an authenticated push subscription to this URL. Set its OIDC audience to PLAY_NOTIFICATION_AUDIENCE, its service account email to PLAY_NOTIFICATION_SERVICE_ACCOUNT, and PLAY_NOTIFICATION_SUBSCRIPTION to the exact Pub/Sub subscription resource name. Grant only the required Pub/Sub delivery permissions.

Apple notifications are verified by the App Store Server Library against the configured Apple roots, bundle, app ID and environment; the nested transaction JWS is verified by that same verifier. Google push requests require an RS256 OIDC token from Google with an accepted Google issuer, the exact audience and service-account email, email_verified=true, and an issue time within the previous hour. The Pub/Sub resource and RTDN app package must match configuration.

Webhook bodies are invalidation hints only. Expiry and entitlement claims are never applied from the callback. The API matches a verified transaction/token identifier to a single existing account subscription, opens that account's sealed receipt in memory, and asks the existing purchase verifier for current provider state. Unknown subscriptions, unrelated Google products and provider test notifications are acknowledged without changing entitlements; mismatched Apple products are rejected. Duplicate or out-of-order events cause an authoritative re-fetch; concurrent refreshes for one subscription are coalesced.

Successful and authenticated no-op events return HTTP 204 only after the common persistence hook has flushed. If the provider refresh fails, the API retains the previous state and returns HTTP 503 so the delivery can be retried. Invalid authentication or malformed notifications are rejected. Apple and Pub/Sub retry behavior follows each provider's delivery policy; configure and monitor those policies rather than treating webhook delivery as the only reconciliation mechanism.

The API continues to poll sealed subscriptions at startup and hourly. This fallback works when notifications are not configured and reconciles provider changes after delivery outages; a delay of up to an hour is possible.

Test notifications ​

  1. For Apple, send a V2 test notification from App Store Connect to the sandbox callback and confirm the API acknowledges it without changing any account subscription.
  2. For Google, use Play Console's test RTDN delivery after the authenticated Pub/Sub push subscription is configured. Confirm the test event receives HTTP 204 and causes no purchase verification. Then exercise a real license-tester subscription event in an internal test track against an isolated nonproduction API.
  3. Temporarily make the provider status endpoint unavailable in an isolated test environment and confirm a recognized event receives HTTP 503 while the stored subscription remains unchanged.

Do not use production purchases or mutate real subscriptions as part of local verification.

After family creator transfer the store subscription stays on the original account. The new creator must subscribe separately before accepting a paying family, or receive an operator grant.

Release checklist ​

  • Replace placeholder Terms and Privacy copy before submitting to either store.
  • Rebuild native apps after adding the plugin; Expo Go cannot run IAP.
  • Complete real sandbox tests: new purchase, cancel, renewal, expiry, grace period, refund, restore after reinstall, replay after API outage, and restore on another Fellesly account.
  • Confirm canceled-but-unexpired access and loss of access after refund/expiry.
  • Confirm only the creator (or nominated replacement) can initiate a subscription.
  • Verify no receipt/token appears in logs, exports, public API or MCP.

Unit tests exercise store responses, generated Google RS256/JWKS keys and an Apple SDK verification seam; they do not prove live Apple signatures, live Google Pub/Sub delivery or real payments. Sandbox/native evidence is still required: use signed native builds, provider test notifications, license testers, restore/refund/expiry scenarios, and confirm delivery retries against the isolated API before release.

Official references:

Fellesly: the family, in one place.