Chapter 26: App Store Compliance
Shopify Requirements
Embedded App Mandate
As of late 2024, Shopify requires all new public apps to be embedded within the Shopify Admin. Standalone apps (opening in a separate browser tab) are no longer approved for the App Store.
AdPriority satisfies this requirement by rendering inside an App Bridge 4.1 iframe. The merchant never leaves the Shopify Admin to use AdPriority.
| Requirement | AdPriority Implementation |
|---|---|
| Embedded in Shopify Admin | App Bridge 4.1 iframe |
| Uses session tokens (not cookies) | JWT validation with client secret |
| Polaris UI components | Polaris v13 exclusively |
| Responsive design | Polaris handles automatically |
| Accessibility (WCAG 2.0 AA) | Polaris built-in a11y |
| Loading states on all async operations | Spinner and SkeletonPage |
| Empty states for all empty lists | EmptyState with CTA |
| Error states with user-friendly messages | Banner with tone="critical" |
GDPR Webhooks
Three mandatory webhook endpoints must be implemented and functional at all times. Shopify tests these during the review process.
| Webhook | Endpoint | Behavior |
|---|---|---|
customers/data_request | POST /api/webhooks/customers-data-request | Return 200 OK. AdPriority stores no customer PII. Response includes a message confirming no personal data is held. |
customers/redact | POST /api/webhooks/customers-redact | Return 200 OK. No customer data to delete. |
shop/redact | POST /api/webhooks/shop-redact | Delete all data for the specified shop within 30 days. Return 200 OK. |
Implementation details for shop/redact:
SHOP DATA DELETION
==================
Webhook received: shop/redact
|
v
Verify HMAC signature
|
v
Look up store by shop domain
|
v
Mark store as is_active = false
|
v
Schedule deletion job (30-day delay)
|
v
After 30 days:
DELETE FROM audit_logs WHERE store_id = ?
DELETE FROM sync_logs WHERE store_id = ?
DELETE FROM season_rules WHERE season_id IN (SELECT id FROM seasons WHERE store_id = ?)
DELETE FROM seasons WHERE store_id = ?
DELETE FROM rule_conditions WHERE rule_id IN (SELECT id FROM rules WHERE store_id = ?)
DELETE FROM rules WHERE store_id = ?
DELETE FROM products WHERE store_id = ?
DELETE FROM subscriptions WHERE store_id = ?
DELETE FROM stores WHERE id = ?
|
v
Log deletion completion
|
v
Return 200 OK
The 30-day grace period allows merchants who accidentally uninstall to reinstall without losing their configuration. After 30 days, all data is permanently and irrecoverably deleted.
Data Deletion on Uninstall
When a merchant uninstalls the app, the app/uninstalled webhook fires. AdPriority handles this by:
- Cancelling any active subscription via the Billing API
- Stopping all scheduled sync jobs for the store
- Marking the store as
is_active = false - Starting the 30-day data retention countdown
- If the merchant reinstalls within 30 days, data is restored and the countdown is cancelled
Session Token Authentication
AdPriority uses Shopify session tokens (not cookies) for authentication. Session tokens are JWTs issued by App Bridge and signed with the app’s client secret.
Validation requirements (enforced on every API request):
| Check | Description |
|---|---|
| Signature | Verify JWT signature using SHOPIFY_CLIENT_SECRET with HS256 |
| Audience | aud claim must match SHOPIFY_CLIENT_ID |
| Expiration | exp claim must be in the future (with 10-second clock tolerance) |
| Not-before | nbf claim must be in the past |
| Issuer | iss claim must be a valid .myshopify.com/admin URL |
| Shop resolution | Extract shop domain from iss, look up store record in database |
If any check fails, the API returns 401 Unauthorized. The frontend (via App Bridge) automatically refreshes the session token and retries.
Polaris UI
Shopify mandates the use of Polaris for all UI components in embedded apps. Custom styling that overrides Polaris defaults may cause rejection.
Compliance checklist:
| Element | Requirement | AdPriority Status |
|---|---|---|
| Page layout | Use Page component | All screens use Page |
| Content containers | Use Card component | All content sections in Card |
| Navigation | Use NavigationMenu from App Bridge | 5 navigation items registered |
| Data tables | Use DataTable or IndexTable | Products list uses IndexTable |
| Forms | Use Polaris form components | All forms use TextField, Select, Checkbox |
| Buttons | Use Button component with correct tones | Primary, secondary, and destructive actions |
| Modals | Use Polaris Modal (not browser window.confirm) | All confirmation dialogs |
| Toast notifications | Use Polaris Toast | Success and error notifications |
| No custom CSS overriding Polaris | Strictly prohibited | Only additive custom styles for chart components |
Google Requirements
OAuth Consent Screen
AdPriority uses Google OAuth to access the Google Sheets API (for supplemental feed management) and, for Pro tier merchants, the Google Ads API.
Consent screen configuration:
| Field | Value |
|---|---|
| App name | AdPriority |
| Support email | support@adpriority.com |
| Application type | Web application |
| Authorized domains | adpriority.com |
| Scopes requested | https://www.googleapis.com/auth/spreadsheets (Sheets) |
| Additional scopes (Pro) | https://www.googleapis.com/auth/adwords.readonly (Ads) |
| User type | External |
| Publishing status | Testing (initially), then Production after verification |
Google OAuth verification: For the Sheets API scope, Google requires app verification if the app will be used by more than 100 users. Submit for verification early in Phase 2 to avoid blocking the App Store launch.
API Quotas
| API | Default Quota | AdPriority Usage | Risk |
|---|---|---|---|
| Google Sheets API | 300 requests/minute per project | ~1-5 requests per sync per store | Low |
| Google Sheets API | 60 requests/minute per user | 1 request per sync per store | Very Low |
| Content API for Shopping | 2 product updates per day per product | Not used in MVP (Sheets approach) | N/A |
| Google Ads API | 15,000 operations per day (basic) | Read-only queries, Pro tier only | Low |
Quota management strategy:
- Batch all Sheet writes into a single
values.updatecall per sync (one API call for entire store) - Monitor quota usage via the Google Cloud Console
- Implement exponential backoff on 429 responses
- Alert if daily quota usage exceeds 80%
Data Handling Policy
Google requires a clear statement of how user data accessed through their APIs is handled.
| Policy Point | AdPriority Practice |
|---|---|
| Data accessed | Google Sheets (write custom label data), Google Ads metrics (read-only, Pro tier) |
| Data stored | Priority scores and sync status stored in AdPriority database. Google Ads performance data cached for 90 days. |
| Data shared | Custom label data is written to a Google Sheet that the merchant has shared with GMC. No data is shared with third parties. |
| Data retention | Deleted within 30 days of app uninstall |
| Limited use | Data is used only for the stated purpose of managing product priority labels |
| Security | OAuth tokens encrypted at rest (AES-256), transmitted only over HTTPS |
Privacy
No Customer PII Stored
AdPriority’s core design principle is that it never stores customer personally identifiable information. The app works exclusively with product data.
Data AdPriority DOES store:
| Data Type | Examples | Source |
|---|---|---|
| Product titles | “Jordan Craig Stacked Jeans” | Shopify product data |
| Product types | “Men-Bottoms-Pants-Jeans” | Shopify product data |
| Vendor names | “Jordan Craig”, “New Era” | Shopify product data |
| Product tags | “NAME BRAND”, “Sale”, “in-stock” | Shopify product data |
| Product IDs | Shopify product ID, variant ID | Shopify product data |
| Priority scores | 0-5 integer | Calculated by AdPriority |
| Custom labels | “priority-5”, “winter”, “jeans-pants” | Generated by AdPriority |
| Sync logs | Timestamps, counts, errors | Generated by AdPriority |
| Shop domain | “nexus-clothes.myshopify.com” | Shopify OAuth |
| Merchant email | “will@nexusclothing.com” (for notifications only) | Shopify OAuth |
Data AdPriority NEVER stores:
| Data Type | Reason |
|---|---|
| Customer names | Not accessed, not needed |
| Customer emails | Not accessed, not needed |
| Customer addresses | Not accessed, not needed |
| Order details | Not accessed, not needed |
| Payment information | Handled by Shopify Billing API |
| Customer browsing data | Not collected |
Encrypted Credentials
All OAuth tokens and API credentials are encrypted at rest using AES-256.
| Credential | Storage | Encryption |
|---|---|---|
| Shopify access token | stores.shopify_access_token | AES-256-GCM |
| Google refresh token | stores.google_refresh_token | AES-256-GCM |
| Google Sheets service account key | Environment variable | Base64-encoded, not in database |
The encryption key is stored as an environment variable (ENCRYPTION_KEY) and is never committed to source control or written to logs.
Privacy Policy Summary
ADPRIORITY PRIVACY SUMMARY
===========================
What we collect:
- Product data (titles, types, tags, vendor, IDs)
- Shop domain and merchant notification email
- Priority scores and sync status
What we DO NOT collect:
- Customer personal information (names, emails, addresses)
- Order data
- Payment data
- Browsing data
How we use data:
- Calculate priority scores for products
- Sync custom labels to Google Merchant Center
- Display dashboard and analytics
Who we share data with:
- Google Merchant Center (custom labels only, via Google Sheets)
- No other third parties
How long we keep data:
- Active while app is installed
- Deleted within 30 days of uninstall
Your rights:
- Export your data at any time (via GDPR data request)
- Delete your data by uninstalling the app
- Contact support@adpriority.com for any privacy questions
Compliance Checklist
Pre-Submission Verification
| # | Requirement | Category | Verified |
|---|---|---|---|
| 1 | HTTPS on all endpoints | Security | |
| 2 | OAuth token exchange flow (not implicit) | Auth | |
| 3 | Session token JWT validation on every API call | Auth | |
| 4 | HMAC verification on all webhook endpoints | Auth | |
| 5 | customers/data_request webhook returns 200 | GDPR | |
| 6 | customers/redact webhook returns 200 | GDPR | |
| 7 | shop/redact webhook deletes all store data | GDPR | |
| 8 | All UI uses Polaris v13 components | UI | |
| 9 | Loading states on all async operations | UI | |
| 10 | Empty states on all empty lists | UI | |
| 11 | Error states with descriptive messages | UI | |
| 12 | Mobile-responsive layout | UI | |
| 13 | WCAG 2.0 AA accessibility | UI | |
| 14 | Each OAuth scope justified | Scopes | |
| 15 | Privacy policy URL accessible | Legal | |
| 16 | Terms of service URL accessible | Legal | |
| 17 | Support URL accessible | Support | |
| 18 | No test or debug data in submission | Quality | |
| 19 | App installs cleanly on fresh store | Quality | |
| 20 | App handles store with 0 products | Quality | |
| 21 | App handles store with 50,000+ products | Quality | |
| 22 | Billing flow completes (test mode) | Billing | |
| 23 | Uninstall cleans up all resources | Lifecycle | |
| 24 | OAuth tokens encrypted at rest | Security | |
| 25 | No customer PII stored | Privacy |