Define the storefront proxy contract first
Treat the proxy as a verifiable request path
Shopify App Proxy is useful when an app needs to present a bounded feature inside the online store: a member summary, a preference panel, a service status, or another controlled storefront view. It is not a reason to move an entire administration console into a theme and it does not bypass Shopify authorization. Before writing business code, specify the proxy prefix, subpath, allowed methods, response type, store context, time budgets, cache posture, and shutdown behavior. Each item should be traceable between the app configuration, rendered storefront link, request log, and server response.
Start with a read-only path that cannot change an order or payment. Exercise a normal request, a missing signature, a store where the app is unavailable, a slow dependency, a repeated submit, a cache hit, and a failed theme script. A shopper should still see a useful explanation, ordinary navigation, product context, and a contact or return path. The engineering record should identify the store context, route version, latency, outcome, and failure category without exposing secrets.
The official App Proxy configuration documentation explains the relationship between the configurable prefix, subpath, and storefront route. Keep this implementation focused on the storefront proxy boundary; a general administration page, payment flow, and data synchronization job need different contracts.
Design the URL and response contract
Fix the route shape before adding features
A maintainable route makes its feature domain clear, such as /apps/profile/status or /tools/loyalty/summary. Once a path appears in theme links, bookmarks, campaigns, and monitoring, changing it creates a chain of old links, cache keys, and dashboards. Register the path, method, parameters, response type, and version policy before implementation. If a name must change, leave a short-lived explanation or controlled compatibility route rather than returning a silent blank screen.
Keep sensitive values out of the query string. Every allowed parameter needs a type, length, enumeration, and default rule; unknown values should be logged safely and ignored or rejected according to the contract. For JSON, define required fields, empty arrays, status values, and user-readable errors. For HTML, define the allowed fragment, cache headers, security policy, and theme dependencies. A server should not emit a large page fragment that only works with one theme’s private class names.
| Design item | Recommended constraint | Failure symptom | Acceptance evidence |
|---|---|---|---|
| Prefix and subpath | One stable path per feature domain | Links and metrics are fragmented | App settings, theme link, request log |
| HTTP method | Separate reads from state changes | A GET accidentally changes state | Method matrix and replay test |
| Parameters | Type, length, enum, and defaults are explicit | Bad input consumes backend time | Validation log and response |
| Response type | Choose a primary HTML or JSON contract | Browser sees a blank success | Header, status, and body |
| Version policy | Old paths have an explanation and end date | Theme update creates silent failure | Link inventory and recovery test |
Place the finished route in a simple Liquid implementation pattern. The theme owns presentation and submission; it should not reproduce the signing algorithm or place a long-lived credential in browser code.
Verify the proxy signature before business parsing
Preserve the original query representation
Signature verification must happen before business code trusts the query fields. Preserve the original query representation, remove only the documented signature field from the signing set, apply Shopify’s required sorting and concatenation rules, compute the server-side HMAC, and compare the result in constant time. Do not casually decode plus signs, percent escapes, arrays, or Unicode values and then rebuild the string. Two layers that normalize in a different order will calculate different values.
Keep signature fields and business fields distinct. Reject a missing digest, an invalid timestamp or an impossible shop value with one consistent external response. Do not reveal whether a store exists, which secret was used, or how the expected digest differed. A secret belongs in server configuration only; it must never be placed in theme settings, HTML, browser storage, or an error payload.
| Verification stage | Input | Required condition | Rejection behavior |
|---|---|---|---|
| Capture | Original query and method | Encoding and repeated keys remain available | Log a safe request fingerprint |
| Normalize | All fields except the signature | One deterministic rule for every locale | Return a uniform 4xx |
| Compute | Server secret and normalized value | HMAC plus constant-time compare | Do not disclose expected digest |
| Context | shop, path, and install state | Store format and route are allowed | Avoid store enumeration |
| Business gate | Verified values only | Type, permission, and status checks run again | Track auth and business errors separately |
Use the official public app proxy authentication method to understand the framework context and failure behavior, then verify it with fixed request samples. A valid signature proves request context, not permission to perform every business action.
Bind store context and short-lived sessions
Make every read belong to the right tenant
The verified shop context should be the first condition in every database, cache, and downstream lookup. A browser-submitted shop value must never replace it. Normalize host casing, a trailing dot, Unicode hostnames, and empty values before business code runs; reject values that cannot be normalized safely. When the app is unavailable for a store or authorization has expired, return a comprehensible install or return path rather than a detailed account diagnostic.
Create a short session only when the feature needs one, and bind it to the store, user or session subject, expiry, and revocation state. Do not place a long-lived access token in a cookie, URL, or HTML. If an Admin read is needed, use a server-side token appropriate to the access model and request only the fields required by the page. Clear related cache entries when authorization or identity changes so a browser cannot receive an earlier user’s state.
The Shopify authentication and authorization guide is a useful reference for separating authentication context from permission checks. Store context answers which shop made the request; user identity and role answer who may perform the action. Logging those concepts separately makes an incident easier to diagnose without logging personal data.
Set cache keys and invalidation boundaries
Never let a fast response cross stores
Whether a proxy response can be shared depends on whether it contains store, user, or session data. A public explanatory fragment can have a short TTL. A response containing store settings, membership state, addresses, or choices should not be shared across users or stores by default. A cache key should include normalized shop, proxy path, allowed business parameters, locale, market context when relevant, and a data version. A path-only key is not sufficient.
Define hit, miss, stale, and explicit invalidation behavior for each response type. Installation state, uninstallation, permission changes, store settings, and theme changes can make prior data unusable. If invalidation cannot be proven, reduce the TTL or turn off shared caching. Write responses should carry non-cacheable behavior and use an application idempotency key. Cache records should expose only a safe key digest, age, version, and outcome in logs.
| Response class | Sharing rule | Cache key | Invalidation event | Safe fallback |
|---|---|---|---|---|
| Public explanation | Short shared TTL is acceptable | Path, locale, content version | Content or version change | Static fragment |
| Store setting | Store scope only | Shop, path, setting version | Setting save or app removal | Default setting |
| User state | Usually private | Shop, session subject, path | Sign-out or permission change | Ask for verification |
| Write result | Never shared | Idempotency key and request fingerprint | Completion or expiry | Return retry state |
| Error response | Very short or disabled | Route and status class | Service recovery | Ordinary error page |
Test two stores and two browser sessions before measuring hit rate. An improved hit rate is not a reason to remove a store or session dimension from the key; an accidental cross-store response is more damaging than one additional origin read.
Budget slow dependencies and timeouts
Turn waiting into a clear state
Give the proxy a total deadline, connection deadline, and downstream read deadline. Each dependency needs a smaller budget so signature verification, cache lookup, database work, remote calls, and rendering fit inside the total. On timeout, return an explicit status and a readable message. An HTML response should retain store navigation; JSON should expose a stable error category and retry guidance. Never keep retrying after the browser has gone away, and never use unbounded retries that turn one click into a traffic spike.
For a read-only summary, a briefly stale value may be acceptable when its age is visible. For a membership entitlement, price, inventory decision, or other state that changes a shopper’s action, it is safer to say that the value is temporarily unavailable than to present an old value as current. Retry only a safe idempotent read, with backoff and a strict cap. For a write, check whether the prior operation completed before offering another attempt.
The storefront should offer one retry and an ordinary alternative, such as returning to a product page, contacting support, or continuing to browse. Error text must not include a stack trace, database name, token, signature digest, or dependency address. Group timeout evidence by route, store, dependency class, and region so a single store problem is not confused with a platform slowdown.
Make writes idempotent and replay-safe
Decide whether an action can be repeated
Proxy write actions might save a preference, register a reminder, submit a controlled request, or create a result record. Each action needs a business key and an idempotency key bound to the store, user or session subject, action type, and validity period. Inside a transaction, check for an existing completed result before creating a new record. Use a unique constraint or lock for concurrent requests. A response should let the browser query a known result after a timeout instead of blindly creating a second record.
A browser-generated random value is useful for correlation, not authorization. Authorization comes from the verified store context, server-side session, and field-level permission checks. For an action that cannot be repeated, return the original state on a second request. For a preference that can merge, apply the latest valid version and record the conflict. Disabling a button is not sufficient because refreshes, network retries, and multiple tabs can bypass it.
| Action | Idempotency key | Duplicate result | User action after failure | Evidence |
|---|---|---|---|---|
| Save preference | Shop, subject, preference, version | Return saved version | Reload current version | Request fingerprint and version |
| Register reminder | Shop, subject, product | Return existing registration | Show already registered | Unique constraint and state |
| Submit request | Shop, subject, request key | Return original state | Query result state | Transaction record |
| Generate result | Shop, subject, operation key | Return same result address | Open result address | Result version and time |
| Cancel action | Original key and cancel version | Return final state | Do not resend | State-machine record |
Use refresh, double-click, two tabs, timeout, and concurrent requests in the replay suite. The Shopify access token guidance helps keep server token scope and lifetime separate from the lifetime of a single storefront action.
Make HTML, JSON, and errors recoverable
Preserve basic storefront usability
An HTML proxy fragment should depend on as little theme-specific styling as possible and should remain readable when a script does not load. A success response needs a title, status explanation, primary action, and return link. An error uses the same navigation and accessible landmarks. A JSON response needs a stable status, error category, user message, and correlation number; a frontend should never infer an error from a 200 response with an empty body.
Escape content for its context and reject unprocessed HTML, URLs, and user input in templates. If controlled rich text is necessary, sanitize it with an allowlist of tags, attributes, and protocols. Store settings must not become scripts or styles. Match response headers to the content, set a deliberate cache policy, and confirm the actual browser result rather than trusting a server unit test.
Test keyboard focus, visible error text, retry feedback, and long messages on a narrow viewport. The mobile conversion practice provides useful page context for this check. A failed proxy should not trap a shopper inside an empty drawer or a scroll-locked modal.
Minimize permissions, tokens, and sensitive data
Supply only what the storefront feature needs
List every field a proxy feature reads and writes, then map it to app permissions, store settings, and the user role. A store-level task should not send personal data to the browser. A server-side query should not require a token in a page. Use redacted identifiers in logs, and set a retention period for addresses, phone numbers, email addresses, and custom fields when troubleshooting genuinely needs them.
Separate credentials by purpose: a server token for controlled server-to-Shopify reads, a short session for storefront state, and a correlation number for support. Secrets, tokens, signature material, and complete cookies should never appear in HTML, query parameters, analytics events, or exception traces. During rotation, verify the new credential before revoking the old one. After removal or revocation, the proxy should recognize the invalid state and stop further access.
Review the Shopify secure token recommendations. Security is not one extra check; it is a set of boundaries that prevents data, permissions, caching, logs, and recovery behavior from widening exposure together.
Prove the request with safe observability
Build a useful timeline without secrets
Generate a correlation number for every request. Record a safe store digest, proxy path, method, status, total time, phase timings, cache result, dependency category, and failure class. Never record the full signature, token, personal data, or a query string that can be reconstructed into sensitive information. Return the correlation number in a user-facing error so support can find the timeline without asking the shopper for a secret.
The test matrix should cover an invalid signature, absent shop, unavailable app, missing permission, parameter boundaries, cache separation, timeouts, duplicate writes, downstream 4xx and 5xx responses, failed HTML scripts, and a narrow mobile view. Save the request sample, status, visible text, log events, and recovery result for each case. Monitor by route, store, region, and response class; alert on signature rejection, timeout, duplicate conflict, and cache-isolation failures.
| Scenario | Key telemetry | Shopper result | Passing condition |
|---|---|---|---|
| Invalid signature | Path, status, correlation number | Uniform rejection explanation | No sensitive detail leaks |
| App unavailable | Store digest and status class | Install or return path | Store existence is not disclosed |
| Cache hit | Key digest, age, version | Correct content | Two stores never share state |
| Downstream timeout | Phase times and dependency class | Retry or ordinary link | Request ends within budget |
| Duplicate write | Key digest and final state | Original result or completed state | No duplicate record |
Use the Shopify page-building trade-off guide to review whether the proxy link, theme script, and ordinary navigation remain understandable. A backend 200 count is not proof that shoppers received a usable page.
Failure cases and a practical recovery boundary
Stop exposure before investigating the root cause
Failure case one: middleware decodes the query string before the signature routine, and a plus sign is later reconstructed using a different rule. Some valid requests are rejected, so the storefront appears to have no app. Compare a fixed raw request, the normalized signing value, and the computed digest. The narrow repair is to restore one normalization path, not to weaken verification or change the business route. Repeat samples with alternate encodings, repeated keys, and empty values.
Failure case two: the cache key contains only /apps/profile/status and omits the store and session subject. After one browser warms the cache, another store sees an old status summary. Immediately disable shared caching, clear affected keys, and keep the correlation numbers. Add store, session, and data version dimensions, then test two stores, two browsers, and concurrent requests. If the historical response range cannot be demonstrated, keep shared caching off and notify the operations owner.
The recovery boundary includes proxy configuration, the theme entry point, the cache switch, the response template, and the related middleware. It does not include deleting products, orders, customers, or unrelated theme pages. Prepare a read-only status page or ordinary link as a safe entrance. After recovery, verify navigation, product context, and contact access, then repair one signature, cache, or response rule at a time.
Release, pause, and re-enable deliberately
Make the integration an operable capability
Before release, check the prefix, subpath, methods, parameters, signature verification, store context, session expiry, cache key, timeout budget, idempotency key, error response, redacted logs, and ordinary entrance. Exercise one store without the app, one installed store, one slow network, and one narrow device. Confirm that a theme update preserves the link and that removal or permission loss stops access.
After enabling, watch fixed stores and the read-only feature before allowing writes. Group signature rejection, timeout, cache isolation, duplicate conflict, script failure, and support feedback by route, store, region, and response class. During a pause, close writes or shared caching first while keeping the explanation and ordinary navigation available. If the root cause is unclear, switch to a static status page and re-enable one function only after evidence is complete.
The store speed field guide and the media experience guide help position a proxy page within the full storefront experience. Keep the proxy as a bounded storefront capability with clear ownership for theme, permissions, data, and recovery.
For a release record, capture the configured prefix and subpath, the rendered link, HTTP method, request body class, raw query sample, normalized signing value, timestamp decision, store-context digest, session state, cache key digest, downstream budgets, response content type, status, visible error text, and correlation number. A browser screenshot alone cannot reveal whether the wrong store entered a cache or whether a middleware layer changed the signed representation. Keep one trace for an anonymous visitor and another for an authorized session, with secrets and personal data removed.
Exercise the route through a normal link, a refresh, a browser back action, two tabs, a slow dependency, an unavailable dependency, a repeated write, an app removal state, and a changed prefix or subpath. Check that every branch ends within its budget, returns a predictable content type, preserves ordinary navigation, and leaves enough evidence for support to reproduce the symptom. When the feature is paused, verify the same links again so a disabled route produces an explanation instead of a blank success. Re-enable one read path first, then one write path, and compare the stored timeline with the earlier baseline before widening traffic.
Frequently asked questions
Can an App Proxy replace backend authorization?
No. The signed request provides Shopify request context, but every business action still needs app permission, session, identity, and field-level checks. Use a server-side token for required Shopify reads and request the smallest useful scope.
Why can signature validation pass locally and fail online?
A proxy, server framework, or middleware may change query encoding, repeated keys, or sorting in the online path. Preserve a raw request sample and compare the normalized signing value and each calculation step instead of comparing only the final URL.
Can a proxy response be cached for a long time?
Only a response with no store, user, or session data is a good shared-cache subject, and it still needs a version and invalidation rule. Personalized state, write results, and permission-related data should be isolated by store or session; when isolation cannot be proven, do not share it.
What should a shopper do after a backend timeout?
Show a clear temporary-unavailable or retry message while keeping an ordinary navigation path. Retry only a safe idempotent read with a cap. For a write, let the shopper query the original result so refresh does not create a duplicate.
What should be paused first after cross-store content appears?
Disable shared caching or writes and switch to a read-only status page or ordinary link while preserving request evidence. Do not delete commerce data. Verify the core entrance, then repair the signature, cache key, or response template that caused the exposure.