Shopify public app deployment
This runbook covers the operator steps for deploying, configuring, installing, validating, uninstalling, and reinstalling the Parcel Pilot Shopify public app.
Use this document for operational setup and live validation.
For the architectural rationale, rollout policy, and migration history, see:
1) Architecture summary
Parcel Pilot's current Shopify public-app model is:
- a non-embedded external Shopify app
- one shared global public-app client ID and secret by default
- one Shopify shop access token stored per Parcel Pilot
ClientIntegration - optional encrypted per-integration app credential overrides for temporary custom-distribution or pilot apps
- GraphQL or Bulk for Shopify business-data operations on the public-app path
- REST kept only for explicitly retained legacy or custom-app compatibility modes
- declarative Shopify app configuration for mandatory compliance topics
- per-shop GraphQL reconciliation for lifecycle and operational topics
Important boundaries:
- do not treat temporary custom-distribution app overrides as a replacement for the future shared public app
- keep compliance topics declarative in
shopify.app.toml - do not declare lifecycle or operational topics in
shopify.app.tomlwhile Parcel Pilot owns them through per-shop GraphQL reconciliation - keep architectural design decisions in Shopify Central App Architecture and Shopify Migration Coexistence Strategy
2) Shopify Dev Dashboard configuration
Use this baseline for the shared public app.
Replace https://parcelpilot.co.uk with the actual public hostname if APP_URL differs in the target environment.
App name: Parcel Pilot
App URL: https://parcelpilot.co.uk/shopify/app
Embed app in Shopify Admin: No
Preferences URL: https://parcelpilot.co.uk/shopify/preferences
Allowed redirection URL: https://parcelpilot.co.uk/shopify/auth/callback
Webhooks API version: 2026-04
Required scopes: read_orders,read_products,read_inventory,write_inventory,read_locations,read_merchant_managed_fulfillment_orders,write_merchant_managed_fulfillment_orders
Optional scopes: read_all_orders
Use legacy install flow: Enabled
Embed in Shopify POS: No
App proxy: not configured
Notes:
- The public app remains non-embedded in the current rollout slice.
shopify.app.tomlis now the source of truth for mandatory compliance topics only.- Lifecycle and operational topics must not also be declared in Shopify app configuration while Parcel Pilot reconciles them per shop through the GraphQL Admin API.
- If you deploy to staging or another non-production host, every App URL, Preferences URL, redirect URL, and webhook endpoint in Shopify must match that environment exactly.
App configuration deploy workflow
The repository now includes a committed Shopify app config at shopify.app.toml.
Use it for compliance-topic deployment:
shopify app config link
shopify app deploy
Operator rules:
- replace
client_idand URLs in the linked app config when targeting a non-production app or host - deploy the app config before first install so the compliance topics exist before Shopify starts delivering them
- keep compliance topics in TOML only; do not recreate them through Parcel Pilot runtime code
3) Webhook endpoints and topics
Current mandatory privacy endpoint:
https://parcelpilot.co.uk/api/webhooks/gdpr
Mandatory privacy topics:
customers/data_requestcustomers/redactshop/redact
Current lifecycle and operational endpoint:
https://parcelpilot.co.uk/api/webhooks/shopify
Topics currently reconciled after OAuth:
app/uninstalledapp/scopes_updateorders/createorders/updatedorders/cancelledfulfillments/createfulfillments/updateproducts/createproducts/updateproducts/deleteinventory_levels/updateinventory_items/update
Ownership model:
- Compliance topics are deployed from
shopify.app.tomlwithshopify app deploy. - Parcel Pilot reconciles lifecycle and operational subscriptions after successful OAuth activation through the GraphQL Admin API.
- Matching lifecycle and operational subscriptions are reused rather than recreated.
- Duplicate lifecycle and operational subscriptions for the same topic are cleaned up during reconciliation.
- Per-shop runtime reconciliation is authoritative only for lifecycle and operational topics.
Operator rule:
- do not add lifecycle or operational topics to declarative app config unless the code and runbook are intentionally changed together
4) Environment and configuration inventory
Use placeholders only. Never commit or paste live secrets into documentation.
| Key | Purpose | Required | Example format | Rebuild config cache | Restart queue workers |
|---|---|---|---|---|---|
APP_URL |
Canonical external base URL used for OAuth callbacks, app launch URLs, and webhook addresses | Yes | https://app.example.com |
Yes | Yes |
APP_KEY |
Laravel encryption key for encrypted secrets and privacy artifacts | Yes | base64:<app-key> |
Yes | Yes |
APP_PREVIOUS_KEYS |
Previous Laravel encryption keys kept during controlled key rotation | Optional | base64:<old-key-1>,base64:<old-key-2> |
Yes | Yes |
SHOPIFY_APP_KEY |
Legacy shared global Shopify app client ID used by existing global/default integrations | Required only when legacy global integrations still exist | <legacy-shopify-client-id> |
Yes | Yes |
SHOPIFY_APP_SECRET |
Legacy shared global Shopify app client secret used by existing global/default integrations | Required only when legacy global integrations still exist | <legacy-shopify-client-secret> |
Yes | Yes |
SHOPIFY_PUBLIC_APP_KEY |
Global shared Shopify public-app client ID | Yes for the shared public app | <shopify-public-client-id> |
Yes | Yes |
SHOPIFY_PUBLIC_APP_SECRET |
Global shared Shopify public-app client secret | Yes for the shared public app | <shopify-public-client-secret> |
Yes | Yes |
SHOPIFY_WEBHOOK_SECRET |
Shared legacy compatibility webhook HMAC override when a deployment intentionally uses a value different from the shared app Client Secret | Optional legacy compatibility only | <legacy-webhook-secret> |
Yes | Yes |
SHOPIFY_PUBLIC_APP_API_VERSION |
API version used by the public-app path | Yes | 2026-04 |
Yes | Yes |
SHOPIFY_PUBLIC_APP_WEBHOOK_API_VERSION |
Webhook subscription API version | Recommended | 2026-04 |
Yes | Yes |
QUEUE_CONNECTION |
Queue backend for webhook and sync jobs | Yes | database |
Yes | Yes |
SHOPIFY_IMPORT_QUEUE |
Queue name used for Shopify import work where configured | Optional but operationally relevant | imports |
Yes | Yes |
CACHE_STORE |
Shared cache backend for OAuth state and operational cache usage | Yes | database or redis |
Yes | Yes |
SESSION_DRIVER |
Session storage used for login and operator flows | Yes | database |
Yes | No |
SESSION_DOMAIN |
Cookie domain for authenticated operator flows when needed | Optional | .example.com |
Yes | No |
SESSION_SECURE_COOKIE |
Forces secure cookies on HTTPS deployments | Recommended | true |
Yes | No |
SHOPIFY_PRIVACY_ARTIFACT_DISK |
Storage disk for encrypted privacy export artifacts | Optional | local |
Yes | Yes |
| company website config | Used where Shopify-facing metadata or support context needs a canonical business site | Optional but recommended | https://www.example.com |
Yes | No |
| company support config | Used where support contact details are surfaced operationally | Optional but recommended | support@example.com |
Yes | No |
Environment rules:
- OAuth state is stored in cache, expires quickly, and is consumed once.
- Cache must be shared across all web nodes. A non-shared cache can break OAuth callbacks when the callback lands on a different node than the launch request.
- Privacy artifacts are encrypted before storage and should remain on a non-public disk.
- Shopify normally signs app webhooks with the app Client Secret. Normal public-app and custom-distribution app onboarding should not require a separate webhook secret.
- Parcel Pilot resolves public-app installs and reconnects against
SHOPIFY_PUBLIC_APP_KEY/SHOPIFY_PUBLIC_APP_SECRET. - Existing global/default integrations continue using
SHOPIFY_APP_KEY/SHOPIFY_APP_SECRETunless they are explicitly migrated or switched to an integration-specific override. - Changing
APP_KEYrequires a planned key-rotation process. UseAPP_PREVIOUS_KEYSso existing encrypted records can still be read during transition. - Temporary custom-distribution apps use encrypted per-integration overrides. Do not replace the global public-app credentials in
.envjust to onboard one temporary merchant.
When a value changes:
- rebuild config cache for every key in the table above
- restart queue workers for queue-processed webhook and sync paths when the effective runtime config changes
5) Deployment procedure
Use the repository-approved deployment sequence from the application root:
composer install --no-dev --prefer-dist --optimize-autoloader
npm ci
npm run build
php artisan migrate --force
php artisan optimize:clear
php artisan config:cache
php artisan view:cache
php artisan queue:restart
Do not add php artisan route:cache to this runbook.
Reason:
- the repository currently contains closure routes, so route caching is not a safe default
If production uses the documented systemd queue workers, restart and inspect them after deploy:
sudo systemctl restart parcelpilot-queue-default parcelpilot-queue-imports
sudo systemctl status parcelpilot-queue-default --no-pager
sudo systemctl status parcelpilot-queue-imports --no-pager
sudo journalctl -u parcelpilot-queue-default -n 200 --no-pager
sudo journalctl -u parcelpilot-queue-imports -n 200 --no-pager
Scheduler verification:
php artisan schedule:list
php artisan schedule:run
Related worker setup guide:
6) Pre-install validation
Run the focused Shopify regression suites and require them to pass before the first live development-store installation.
Public-app foundation and webhook validation:
php artisan test tests/Feature/ShopifyWebhookCredentialVerificationTest.php tests/Feature/ShopifyLifecycleAndPrivacyWebhookTest.php tests/Feature/ShopifyPublicAppLaunchTest.php tests/Feature/ShopifyOAuthCredentialOverrideTest.php tests/Feature/ShopifyPublicAppConfigTest.php
GraphQL and Bulk transport validation:
php artisan test tests/Feature/ShopifyGraphqlTransportTest.php tests/Feature/ShopifyGraphqlMutationTransportTest.php tests/Feature/ShopifyBulkProductCatalogTransportTest.php tests/Feature/ShopifyLocationGraphqlTransportTest.php tests/Feature/ShopifyInventoryLevelGraphqlTransportTest.php tests/Feature/ShopifyInventoryItemGraphqlTransportTest.php
Fulfillment and tracking transport validation:
php artisan test tests/Feature/ShopifyFulfillmentGraphqlTransportTest.php tests/Feature/ShopifyFulfillmentCreateGraphqlTransportTest.php tests/Feature/ShopifyFulfillmentTrackingUpdateGraphqlTransportTest.php tests/Feature/ShopifyFulfillmentListGraphqlTransportTest.php tests/Feature/ShopifyFulfillmentTrackingDiscoveryTest.php tests/Feature/ShopifyFulfillmentMultiTrackingTest.php tests/Feature/ShopifyFulfillmentIdentityPersistenceTest.php
Smoke checks:
php artisan route:list --except-vendor | grep -E 'shopify/(app|preferences|auth/callback|auth/redirect)|webhooks/(shopify|gdpr)'
curl -I https://parcelpilot.co.uk/up
Manual reachability checks:
- Confirm the App URL, Preferences URL, callback URL, and webhook URLs are externally reachable over HTTPS.
- Confirm Shopify can hit the callback and webhook URLs without a Parcel Pilot operator session.
- Confirm the hostname used in Shopify exactly matches
APP_URLfor the target environment.
7) Development-store fixture setup
Use synthetic test data only.
Minimum fixture set:
- one primary merchant-managed Shopify location
- preferably one secondary location so inactive or non-primary inventory behavior can be checked
- at least one tracked inventory product
- at least one untracked inventory product
- one recent unfulfilled order
- one partially fulfilled order
- one already-fulfilled upstream order
- one order with multiple fulfillments
- one order or shipment path with multiple tracking numbers
- synthetic customer contact and address data only
- no production customer data
Recommended setup notes:
- create the fixtures directly in a Shopify development store
- keep one store dedicated to public-app rollout testing where possible
- if testing historical-order behavior, document whether
read_all_orderswas granted and whether the store actually contains old-enough orders to prove the boundary
8) First installation checklist
- Release the current Shopify app version in the Shopify Dev Dashboard or confirm the target draft version is the one being installed.
- Confirm the App URL, Preferences URL, redirect URL, API version, and scope set match this runbook.
- Open the install URL from Shopify or
https://parcelpilot.co.uk/shopify/app?shop=<dev-store>.myshopify.com. - Complete OAuth and confirm Parcel Pilot redirects back into the expected integration settings flow.
- Confirm the Shopify access token is stored on the correct
ClientIntegrationonly. - Confirm the granted scopes match the required scope set and any intentionally approved optional scopes.
- Run
shopify app deployfor the linked app config and confirm the compliance topics point at/api/webhooks/gdpr. - Confirm lifecycle and operational webhook subscriptions exist after OAuth.
- Open the app again from Shopify Admin and confirm the flow does not loop back into OAuth unnecessarily.
- Open Preferences from Shopify Admin and confirm the operator reaches the intended integration settings page after authentication.
- Uninstall the app and confirm the integration is disabled and marked uninstalled.
- Reinstall the app and confirm the integration reactivates cleanly.
- Confirm reinstall or reauthorize does not create duplicate webhook subscriptions.
- Check queue workers, webhook processing, and relevant audit entries after install, uninstall, and reinstall.
9) Business workflow test checklist
Keep this section operational. Use it as a compact smoke-test list rather than a full engineering test script.
- Confirm shop metadata can be read successfully with the installed token.
- Confirm recent orders can be imported or resolved for the development store.
- Confirm product sync and Bulk product sync complete successfully.
- Confirm inventory items, inventory levels, and locations can be read.
- Confirm an inventory quantity update succeeds through the supported Parcel Pilot flow.
- Confirm merchant-managed fulfillment-order reads succeed.
- Confirm fulfillment creation succeeds for a valid test order.
- Confirm tracking updates succeed, including a case with multiple tracking numbers when supported by the selected scenario.
- Confirm missing-identity GraphQL discovery can recover the fulfillment identity needed for downstream updates.
- Confirm the public-app path stays on the GraphQL or Bulk route for the business operations it owns.
- Confirm endpoint-mode rollback behavior is used only for explicit legacy or custom-app compatibility integrations, not for the normal public-app baseline.
10) Security and isolation checks
Run these checks during rollout validation:
- invalid shop input is rejected
- OAuth callback fails on shop and integration mismatch
- expired or replayed OAuth state is rejected
- webhook delivery signed with the wrong secret is rejected
- the global secret is rejected for an override-bound integration when that integration is bound to a different app credential source
- Preferences access is blocked across tenants
- disabled or uninstalled integrations do not continue normal Shopify API behavior
- two integrations bound to different Shopify app credentials stay isolated for OAuth and webhook verification
11) Temporary custom-distribution app procedure
Use this only when a merchant must temporarily connect through a custom-distribution Shopify app before the approved shared public app is the right path.
Procedure:
- Keep the future shared public-app credentials in global environment configuration.
- Configure the temporary custom app client ID and client secret as encrypted integration-level overrides on the specific
ClientIntegration. - Use those same integration-specific credentials for both OAuth and webhook HMAC verification on that integration.
- Store the resulting shop access token only on that integration.
- Do not overwrite global
.envShopify credentials to support one temporary merchant. - When the merchant is ready for the approved public app, install the public app, reauthorize, verify scopes and webhooks, then remove the temporary overrides.
Operator warning:
- temporary custom-distribution app overrides are additive compatibility tooling, not the long-term global public-app configuration model
12) Approval and rollout boundaries
The following can be tested before broader Shopify approval:
- OAuth on Shopify development stores
- webhook mechanics
- uninstall and reinstall behavior
- GraphQL business operations using synthetic development-store data
- privacy webhook processing using test deliveries
- credential isolation between global and override-bound integrations
The following requires or may require Shopify approval before public production rollout:
read_all_orders- protected customer-data access for public production stores
- App Store publication
Additional note:
- Built for Shopify status is not required for initial App Store approval
13) Troubleshooting
Common failures and first checks:
Invalid installation link
Check that the install URL was generated from the correct app client ID for that integration and that the shop parameter uses the real *.myshopify.com domain.
Callback URL mismatch
Check that Shopify's allowed redirect URL exactly matches APP_URL plus /shopify/auth/callback for the deployed environment.
Incorrect client ID or secret source
Check whether the integration is meant to use:
- the shared global public-app credentials
- an integration override
- a preserved legacy or manual compatibility path
Missing OAuth state caused by non-shared cache
Check that CACHE_STORE is shared across every web node serving the public app.
Invalid webhook HMAC
Check that Shopify is signing with the secret that matches the integration's active credential source.
Duplicate webhook subscriptions
Check whether lifecycle or operational topics are being written by a second source. Parcel Pilot already reconciles those topics through GraphQL, so repeated duplicates usually mean another runtime or manual installer is still writing them.
For operator-safe inspection and repair from the Parcel Pilot app environment that owns the live integration record:
- Inspect only:
php artisan shopify:public-app-webhooks <integration_id> - Reconcile with the current public-app policy:
php artisan shopify:public-app-webhooks <integration_id> --apply
This command uses the current config/shopify_public_app.php topic set, SHOPIFY_PUBLIC_APP_WEBHOOK_API_VERSION or its fallback, and the shared /api/webhooks/shopify callback for lifecycle and operational topics. Compliance topics are expected from shopify.app.toml and are reported separately rather than counted as per-shop missing subscriptions.
Missing required scope
Check the granted scopes stored on the integration. If a required scope is missing, reauthorize with the required set and confirm the merchant approved the missing scope.
Queued work continues after deploy
Check whether workers were restarted or received php artisan queue:restart after the deploy.
Wrong APP_URL
Check every Shopify-facing URL in the dashboard, then confirm the deployed environment's APP_URL, reverse-proxy configuration, and HTTPS termination are aligned.
App redirect loop
Check whether the integration already has a valid token and required scopes, and whether the callback or post-auth redirect hostname differs from APP_URL.
No trusted fulfillment identity or manual-review tracking outcome
Check the GraphQL fulfillment-identity discovery path, the persisted fulfillment identifiers on the order or shipment context, and whether the integration has fallen back to a legacy compatibility mode intentionally.