aporat/laravel-appstore-purchases
Laravel package for verifying Apple App Store in-app purchase receipts and subscriptions server-side. Validate transactions, inspect purchase data, and integrate iOS payments into your app with a simple, framework-friendly API.
Architecture fit. The package remains a Laravel-native integration (service provider, publishable config, publishable migrations, events, queued listeners), so it fits idiomatically inside an existing Laravel app with no new runtime. The material architectural change is scope: 5.0.0 adds a Subscriptions\ persistence and reconciliation layer (ManagesStoreSubscriptions, StoreSubscription, SubscriptionRecorder, notification listeners, VerifyStoreSubscriptions). The package shifts from "receipt validator you call" to "owner of subscription state you query." That is a defensible direction — it centralises the webhook/worker-authoritative model — but it is a real coupling increase: the package now defines a schema, a model contract on the account, and a source of truth for entitlements.
Integration feasibility. High for new adopters (publish migrations, add trait, wire routes). For existing installs, feasibility is high but the upgrade is genuinely breaking: key_path is removed in favour of private_key (path or inline PEM), and a config still carrying key_path fails with an explicit message rather than degrading. Fail-fast is the right choice; it makes the migration a config edit, not a silent incident.
Technical risk.
key_path → private_key, APPLE_APPSTORE_KEY_PATH no longer read) — every deployment's secret wiring must change in the same release.aporat/store-receipt-validator ^11.1 and the removed 11.0.0 compatibility shim — anyone pinned below 11.1 is blocked.phpseclib pinned to ^3.0.36 — necessary (v4 silently broke Google RTDN verification with a blanket 401), but it can now conflict with applications or sibling packages that require phpseclib 4. This is the highest-probability install-time failure.array driver degrades to the old per-process behaviour. Graceful fallback exists, so this is a performance risk, not an availability risk.subscriptions row per store means an account with both Apple and Google subscriptions has multiple rows; entitlement aggregation logic must live somewhere, and that must be decided explicitly.Key questions.
subscriptions table, or the application's existing entitlement model? Is the package's table read-only projection or authoritative?SubscriptionRecorder dedupe window documented and configurable, and does it survive client retries across the window boundary?VerifyStoreSubscriptions (worker) and webhooks when they disagree?UnhandledNotification types (and future Apple types) expected to be handled by the app, or is a no-op acceptable?Stack fit. Laravel with a queue worker and a shared cache store (Redis recommended: cache locks plus cross-worker token sharing). Package requires PHP with phpseclib 3, psr/cache ^2.0 || ^3.0. The CI fix matters here: prior lanes installed composer.lock, so "Laravel 12" lanes actually ran Laravel 13 — the supported-version matrix was effectively untested before this release. The new per-lane fresh resolution and lowest-dependency lane mean 5.0.0's compatibility claims are the first ones backed by the matrix they advertise. Treat pre-5.0 compatibility claims with reduced confidence.
Migration path (ordered).
composer why-not phpseclib/phpseclib ^3.0 before touching anything.aporat/store-receipt-validator to ^11.1.key_path → private_key in config/appstore.php; move the value out of APPLE_APPSTORE_KEY_PATH into APPLE_APPSTORE_PRIVATE_KEY (inline PEM from the secrets manager is now the supported pattern). Verify PEM line breaks survive your secret store — the package repairs spaces/\n, but confirm in staging.google_cache block if the config is already published (absent block uses defaults, so this is optional); confirm store resolves to the shared cache, not array.VerifyStoreSubscriptions to seed state from the stores.ManagesStoreSubscriptions to the account model; wire notification listeners to the configured subscribers.UnhandledNotification listener for observability before routing production traffic.Compatibility and sequencing. Ship config + secrets change first in isolation (it fails loudly, so it can be validated without new features). Then migrations and the model trait. Then the recorder endpoint. Then webhook listeners and the verification worker. Do not couple the key rename to the new subscription layer in one change set — a rollback should be possible without reverting schema.
Maintenance. The package now owns schema, so migrations become upgrade obligations — this is a durable, recurring cost, not a one-off. Backup/restore and staging refresh flows must include the new tables. Pin the version in composer.json and treat 11.x validator bumps as coupled releases.
Support. Net positive: the phpseclib fix eliminates a total RTDN verification outage (401 on every push), and unknown-but-valid Apple notification types now return 2xx and dispatch UnhandledNotification instead of 400, which stops Apple retry storms. Log lines now name the notification type as Apple sent it, which shortens triage. The residual support surface is the new subscription model itself — "client credential only extends" and "webhooks/worker are authoritative" are counter-intuitive to front-end teams and will generate questions.
Scaling. Two concrete wins: Google Play access tokens are cached per service account across processes (previously every PHP process minted its own token on first call), and OIDC signing certificates are cached for Google's max-age (previously fetched on every push). At push volume the certificate cache is the larger saving. Both need a shared store to matter; on array the release is a no-op.
Failure modes and mitigations.
key_path left in config → hard failure with an explicit message. Contained to deployment, caught in staging.UnhandledNotification with no listener → silent no-op. Assign an owner before go-live.Ramp-up. Modest engineering effort (config + migrations + trait), but real product/support education: the lifecycle semantics (start / offer start / renewal reporting), the one-row-per-store model, and the authoritative-webhook contract need to be written down. Budget roughly a sprint for a team unfamiliar with store subscription mechanics, and gate the rollout behind staging validation of the key loading path and a synthetic RTDN push. Testability improved: injectable TokenVerifier on the Apple controller and optional PSR-6/httpHandler on GoogleOidcPushVerifier let you exercise the notification path without Apple's or Google's real chains — worth using as the acceptance test.
How can I help you explore Laravel packages today?