Separate the buyer account API from store APIs
Confirm the data subject and call boundary first
The Customer Account API is for an authenticated buyer's account data; it is not an Admin API permission copied into a browser. Draw the boundary between buyer, shop, application, identity service, and storage. For every request state who it represents, what it may read, and what it may change. Cart, catalog, and other public content can use the Storefront API, while order history, addresses, and profile data must follow the buyer session and protected-data rules.
A boundary table should include caller, token type, GraphQL endpoint, customer subject, permitted fields, error handling, and fallback experience. Never use one shop token for every buyer, and never treat an email address as a stable identity. A Customer Account API response can include both data and errors, and an HTTP success does not prove domain success. Keep identity, authorization, data state, and rendering as separate records so a UI cannot turn an error into an empty account. Related context: Shopify API overview.
A failure case puts an Admin API access token in the browser to read customer data. The problem is not only exposure; the application cannot prove which buyer authorized the call. Recovery revokes the exposed token, keeps public catalog and cart paths available, and asks for authentication before orders or addresses. Hidden fields or cookies must never impersonate a buyer.
Shopify's Customer Account API overview emphasizes customer scope and secure authentication. Use it as the architecture starting point, then verify fields and mutations against their references. When a request has no clear customer session, return the minimum public content and keep account actions inside the protected flow.
Discover authentication and GraphQL endpoints
Use discovery data instead of hard-coded addresses
The Customer Account API exposes OpenID configuration and account API discovery endpoints. At initialization or a controlled cache refresh, read them to obtain authorization, token, and GraphQL addresses. The endpoint belongs to a shop account context and cannot be copied from one shop to another. Record the source, read time, protocol version, and cache-expiry reason. A failure must not silently fall back to an old shop address.
Build an endpoint resolver that accepts a normalized shop domain and environment and returns authorization URL, token URL, GraphQL URL, and issuer validation. Define failures for HTTPS, host matching, certificate errors, redirects, and incomplete discovery data. The GraphQL client accepts only an address that passed issuer and shop-context checks; a page parameter must never supply an arbitrary URL. Related context: Shopify Liquid theme development guide.
A failure case leaves a test shop's account endpoint in a live configuration. Buyers authenticate and receive an account-not-found or cross-shop response. Recovery clears the bad cache, reads discovery for the current shop, keeps public catalog available, and creates a new session. The client must not replace issuer at will or treat discovery failure as anonymous success.
The Customer Account API reference documents discovery paths, client types, and GraphQL constraints. Preserve a non-sensitive digest of discovery data and test healthy, expired, incomplete, and unreachable responses. Discovery is a session prerequisite, not a permanent configuration file; recheck it after a shop or domain context changes.
| Layer | May hold | Required checks | Must not hold |
|---|---|---|---|
| Browser | Session reference and display state | Secure, HttpOnly, SameSite | Client secret or raw token |
| Server | Protected session and token state | Shop, subject, expiry | Unneeded full customer copy |
| Customer API | Buyer-scoped GraphQL data | Scope, schema, errors | Full Admin shop access |
| Anonymous path | Catalog and cart | No customer identity | Orders, addresses, profile |
Choose a public or confidential client with PKCE
Match client capability to where secrets can live
Browsers and mobile apps are public clients and cannot safely store a client secret, so they need an authorization-code flow with PKCE. A server that can protect a secret may use a confidential client. Record code location, secret-storage capability, callback addresses, response types, and token storage before choosing. A backend does not justify placing its secret in browser code.
The PKCE verifier exists only for one authorization transaction, while the challenge travels in the authorization request. On callback, validate state, nonce, issuer, client ID, and redirect URI before exchanging the code. Each browser tab needs its own transaction; a global verifier can be overwritten by concurrent login. Request only the scope the application can explain and use. Related context: Shopify store speed guide.
A failure case checks only the code and not state, so an authorization result from another browser is injected into the current session. Recovery discards the code, clears transaction state, and asks the buyer to authenticate again. It never exchanges a code without its verifier or writes the code to a log.
Every authorization input has a short lifetime and one-use meaning. Use Shopify's customer authentication guide to verify the official flow, then test cross-origin cookies, mobile deep links, and browser return paths. A public key can be visible; a secret, verifier, code, and token require protected storage.
Design callback state
Bind the response to one browser transaction
State binds an authorization response to the browser, shop, locale, client, and callback path that started it. Store state, the PKCE verifier, creation time, and expected issuer in server-side or protected session storage. Compare state in constant time and consume it immediately. State must not contain an email, an order number, or readable profile data.
Multiple tabs, back navigation, network retries, and mobile app switching create old callbacks. Keep transactions independent and give an old state a clear expired result. Use a fixed callback allow-list rather than concatenating a host from return_to. After code exchange, put only the minimum identity result in the application session and delete the original code and verifier. Related context: Shopify page builder comparison.
A failure case stores state in a long-lived cookie readable by third-party script and allows arbitrary return_to. An attacker can steal the binding value and redirect the buyer away from a trusted origin. Recovery clears the cookie and temporary state and returns to a fixed account entry; it does not consume a suspicious code or extend state lifetime.
The callback matrix covers normal return, buyer cancellation, repeated code, state mismatch, issuer mismatch, PKCE mismatch, and authorization timeout. Each branch tells the buyer what to do without revealing tokens or whether an account exists. Logs carry a transaction digest and failure class, never a full query string or authorization code.
| Flow input | Persist | Success condition | Failure fallback |
|---|---|---|---|
| Discovery | Issuer, endpoint digest, read time | Host and shop match | Clear cache and rediscover |
| Authorization | State, PKCE verifier, nonce | One-use match and exchange | Discard code and authenticate |
| Refresh | Expiry, lock, result digest | One controlled refresh | Clear session and ask again |
| Logout | Session, token, cache references | Current and other tabs expire | Return to anonymous state |
Manage access-token lifetime
Separate short access from long recovery
An access token is a short-lived credential for Customer Account API calls. Read expires_in or the equivalent expiry information and arrange refresh before the access window closes. Whether a public client receives a refresh token, and how rotation works, must come from the current official reference rather than another OAuth service. Bind token storage to client, shop, and buyer session; never put it in HTML, analytics events, or ordinary logs.
The request layer checks expiry before sending. If a token is invalid, allow one controlled refresh and retry the original GraphQL operation; if refresh fails, clear the session and request authentication. Use a single-flight lock so tabs cannot refresh concurrently and overwrite each other's new token. Store a non-sensitive token-family digest, result, and time, not the token text. Related context: Shopify Flow automation guide.
A failure case treats an access token as a permanent login credential. A cached page continues requests overnight, receives unauthorized responses, and shows stale order data. Recovery clears token and local account state, keeps public catalog and cart, and opens authentication. It never retries an old token repeatedly or substitutes an Admin token. Suspected exposure triggers session and client revocation plus log review.
Write the confidential/public distinction, expires_in handling, and refresh behavior from the official reference into the session contract. Test clock movement, incomplete refresh responses, rotation, and two tabs refreshing together. Expiry is a normal state; the UI can preserve non-sensitive shopping context while stating that account actions require authentication again.
Scope cookies, logout, and multiple tabs
Keep account state in the current shop and browser
An application session cookie should contain only an opaque session reference or protected minimum state, with appropriate Secure, HttpOnly, SameSite, and path settings. It is not a Customer Account API token and cannot be reused across shops. The server loads shop, customer subject, token state, and creation time from the reference; the browser receives only the non-sensitive result needed to render.
Logout clears or revokes tokens, server session, refresh locks, and local account cache, and tells other tabs to refresh. When an old session receives unauthorized, it becomes anonymous rather than keeping another buyer's result in memory. Cache keys distinguish shop, customer subject, locale, and resource type; a URL alone is not enough.
A failure case lets cookie domain and cache key cover multiple shops, so a buyer moving from shop A to shop B sees shop A's account menu. Recovery invalidates cross-shop cookies and caches, then discovers and authenticates in the current shop. Until isolation is proven, render public products only. Hiding a button cannot hide protected data still present in a network response.
Exercise login, logout, expiry, back navigation, duplicate tabs, private browsing, and shop switching. Compare browser tools, server session records, and GraphQL requests. The Shopify theme implementation practice can inform interaction work, but account isolation must hold at both server and cache layers.
| GraphQL result | Meaning | Page action | Record |
|---|---|---|---|
| Network failure | Delivery unknown | Try later | Trace, time, attempts |
| HTTP error | Transport failure | Class-specific message | Status and endpoint digest |
| 200 plus errors | Domain may have failed | Use error paths | Operation and path |
| Data plus userErrors | Partial result | Keep old value and correct | Safe digest and version |
Minimize permissions and protected customer data
Prove purpose before requesting fields
Customer Account API data is scoped to a buyer identity, and the customer object can carry protected-data requirements. For every page record purpose, fields, retention, and deletion path, then request only what the current action needs. Do not read order history, addresses, name, and contact details because they might be useful later. Fewer fields reduce exposure in logs, caches, and error responses.
Review anonymous pages, authenticated pages, account management, and support tools separately. Name GraphQL fragments after their purpose and forbid one giant fragment from serving every component. Filter fields in the server, logger, and cache; the UI cannot be the privacy boundary. Verify required scope and field status with the customer object reference and keep the review basis beside the query version.
A failure case queries the full Customer object and writes the response to an analytics platform. The buyer closes the page, yet profile data remains in a third-party log. Recovery stops the broad query, removes unnecessary cache and log data, and moves historical support work to a controlled tool. Do not copy data into a new table to avoid a permission review.
The Customer object reference describes fields and access boundaries. Test missing scope, partial scope, empty fields, a deleted buyer, and a rejected request, while the page still shows a clear anonymous state. Privacy deletion, access requests, and retention are part of the account feature rather than a later patch.
Classify GraphQL contract errors
Check HTTP, GraphQL, and domain state together
The Customer Account API uses GraphQL semantics, so the client distinguishes network failure, HTTP failure, GraphQL errors, null data, and a domain field state. HTTP 200 proves transport completion only; it does not prove a mutation succeeded. Each query and mutation documents required and nullable fields, error mapping, and page fallback. An errors array must never become a message that the buyer has no orders.
Keep operation name, variable digest, response status, error path, and trace reference, but never a token or personal field. After a schema change, inspect fragments, enums, input validation, and mutation payloads before running old and expired-session fixtures. An unknown field or type error should stop that feature branch while safe account content remains usable.
A failure case receives HTTP 200 while userErrors reports address validation failure. The UI checks only status and shows saved. Recovery reads payload and userErrors, keeps the old address visible, asks for correction, and avoids a repeated mutation. If data and errors coexist, use field paths to decide which result is safe; partial success must not become global success.
Map classes to action: authenticate again, correct input, retry later, contact support, continue anonymously, or stop writing. Record schema and API version in request context so documentation drift and runtime failure remain distinct. The Shopify page-building guide is page context; account state still comes from an actual GraphQL result.
| Privacy event | Immediate action | Awaited proof | Prohibited action |
|---|---|---|---|
| Withdrawal | Stop nonessential reads and writes | External service result | Copy to a new table |
| Deletion | Clear token, session, cache | Backup retention | Pretend an empty object is complete |
| Access request | Minimum export | Purpose and lifetime | Export full logs |
| App removal | Inventory storage | Audit proof | Keep an unneeded copy |
Make mutations idempotent and recover conflicts
Handle repeats and concurrency safely
Profile updates, address changes, and preference saves need an idempotency key, version condition, and repeated-submit result. Generate one action reference per user action and bind it to customer subject, shop, and resource. A repeat returns the known result or a clear conflict. A browser timestamp alone is not an idempotency key, and a refresh must not create the same resource again.
Two tabs changing one profile need a version, update-time, or domain conflict rule. A stale cache followed by a write can overwrite another change, so read current state before writing and query after writing to confirm. Keep only the necessary digest of an idempotency record: enough to explain a repeat, not another customer-data copy.
A failure case double-clicks Save. The first mutation succeeds but its response times out; the second lacks an idempotency key and creates a second address. Recovery stops automatic retry, queries current addresses, and asks the buyer to choose. It does not delete by creation time because both records may be valid. An unresolvable conflict enters review.
Conflict fixtures include timeout then retry, same action reference, different references for one resource, stale version, input validation, and changed permission. Record non-sensitive payload digest, userErrors, and final resource version. Account mutation needs buyer-readable confirmation and current-state recovery; it is not merely another webhook retry problem.
Keep locale and regional context separate from identity
Let presentation change without changing the subject
Customer identity is defined by the current shop and authenticated subject. Language, currency, market, or region is presentation and business context. When building an authorization URL, use the supported regional context without putting locale into an account endpoint host. Tokens and session keys remain bound to shop and subject; a region change updates labels and allowed fields, not the buyer.
Language switches, multiple domains, country selectors, and browser translation can alter page paths before and after login. Store locale as a safe presentation parameter, then reconstruct the authorization request with fixed issuer, client ID, and callback validation. Apply regional rules to address format and time display while retaining identity. Do not copy data into a new regional account.
A failure case keys cache by customer ID only. Moving from an English path to another locale returns an old fragment with the wrong shop's address label. Recovery clears locale and regional caches and reloads data for the current shop and subject; uncertain identity gets public content only. A market name is not a customer identity key.
Test callback, refresh, logout, account menu, order list, address form, and errors across locales and regional contexts. The Shopify Flow automation practice is operational context only; permissions and tokens still follow the Customer Account API contract.
| Fallback layer | Narrow to | Preserve | Trigger |
|---|---|---|---|
| Endpoint | Rediscover current shop | Issuer digest | Hard-coded or mismatched host |
| Session | Clear and authenticate | Audit and anonymous cart | Cross-shop or expiry loop |
| GraphQL | Stop one mutation | Current resource version | 200 with errors |
| Page | Anonymous catalog and cart | Non-sensitive context | Unknown buyer subject |
Handle privacy deletion, withdrawal, and audit
Make the account data lifetime provable
An account feature must handle consent withdrawal, account deletion, data access, and app removal as well as reads and updates. List data returned by Customer Account API, local sessions, caches, logs, and backups, then define a deletion or de-identification action for each. Audit records keep the minimum proof of an action: time, subject reference, result, and a digest, never a full profile copy.
After deletion or withdrawal starts, block nonessential reads and writes, clear access tokens, session cookies, caches, and pending mutations. Running requests check session state, and queued work filters by customer subject. Results distinguish cleared, waiting for backup retention, awaiting an external service, and unconfirmed. Support staff cannot bypass deletion through an export file.
A failure case matches cleanup by email. The address changed, leaving an old session and address cache, while the job writes an empty profile and overwrites useful audit state. Recovery stops automatic cleanup, inventories by stable subject and session index, and confirms each item. An empty object is not proof that all data was deleted.
Test normal deletion, expired token, network interruption, repeated request, partial service success, backup wait, and a buyer authenticating again. Each result explains current readable scope and next action. Shopify's API versioning reference includes Customer Account API in the version cadence, so the privacy procedure records the API version and schema basis.
Failure cases and availability fallback
Protect the public shopping path when identity is unclear
Case one: a hard-coded GraphQL endpoint fails after a shop context changes and discovery returns a new address. Validate issuer and discovery, clear the stale cache, and keep account UI at reauthentication or try-later while public catalog and cart remain available. Case two: a public client assumes a refresh token always exists and loops after expiry. Clear the session and return to the authentication entry.
Case three: a shared cookie joins account menus across shops. Invalidate session and cache by shop and subject, then inspect whether any cross-shop response was served. Case four: GraphQL returns 200 with userErrors but the UI says saved. Keep the old value, show correctable fields, and avoid another write. Case five: cleanup writes an empty profile. Pause the job and use audit plus current API state to separate true deletion from write failure. A page component should not build authorization URLs, read tokens, or interpret every GraphQL response. Provide narrow server states such as anonymous, authenticated, reauth_required, forbidden, retryable, and conflict, with only the fields needed to render. Authentication, refresh, error mapping, and audit stay in the protected service; the theme displays state, submits user input, and keeps navigation accessible. The interface contract defines cache control, response fields, error codes, session changes, and retry limits. Any account-data response carries an internal shop and subject association so a generic cache cannot reuse it. A form obtains an action reference, the server validates session and fields, and the result is renderable without exposing a raw response. One universal JSON object should not make every component guess permission. A failure case stores an account token in a theme section and puts the full customer object in a data attribute just to show a greeting. Recovery removes token and full object, keeps a server-provided display fragment or anonymous greeting, and routes reauthentication through a fixed entry. If the narrow interface lacks a field, extend its contract and review scope instead of exposing the original payload. Use the Shopify theme and Liquid guide for page collaboration while keeping Customer Account API as the identity boundary. Test network response, cache headers, log association, and keyboard reachability together so safe narrowing does not become an unusable blank region. Whenever Customer Account API version, schema, discovery document, client configuration, cookie policy, token storage, or page component changes, rerun endpoint discovery, PKCE, state, expiry, refresh, logout, shop isolation, scope, GraphQL errors, mutation idempotency, and privacy deletion. Fix the shop, browser, locale, and account fixtures, and keep input digest, response state, and current account fact. Evidence includes discovery digest, issuer check, callback transaction, token-lifetime record, cookie attributes, GraphQL operation, schema error class, cache key, deletion audit, and fallback screenshot. A green login button is not enough; prove where expiry stops, what recovery keeps, and whether anonymous paths remain clear. The Shopify store-speed field guide can supplement page evidence but cannot replace identity and privacy proof. A failure case completes login in one browser and assumes multiple tabs, mobile callback, and expired tokens behave. The issue appears only after a buyer sees stale account data. Recovery closes the private account region, keeps public catalog and cart, and reruns isolation fixtures. If runtime behavior differs from documentation, record API version and discovery result and pause field expansion rather than copying an old schema. Routine review samples anonymous, newly authenticated, nearly expired, expired, logged-out, cross-shop, and deleting sessions. Each has a clear page and server log. Correlate by shop and subject, not email or page title. The final question for every account record is who read or changed what, through which session, under which version, at what time, and with what scope.
Availability fallback does not bypass identity. Anonymous browsing, public products, cart, and a clear login entry can continue; orders, addresses, profile, and mutations stop without a valid session. Cache, components, and server endpoints must share the same anonymous state. Hiding a button while sending a protected request is not a fallback.
Recovery requires a trusted discovery endpoint, matching issuer, readable token lifetime, classified GraphQL errors, isolated caches, inventoried deletion work, and sampled account facts. Validate expiry, logout, shop switching, and partial responses with fixed session fixtures before restoring one account feature at a time. Any response without a provable subject returns to anonymous experience.
Frequently asked questions
Can Customer Account API use an Admin API token?
No. Customer Account API calls bind to a buyer session and its customer scope. An Admin token represents shop-level authority and placing it in a browser breaks the boundary. Public catalog content can remain available through a public API, while account data requires protected authentication.
Why should account endpoints not be hard-coded?
Endpoints belong to a shop context and discovery provides authorization, token, and GraphQL addresses. A shop or domain context can change, making an old address invalid. Validate issuer against the shop, then clear and rediscover or return to anonymous experience after failure.
Should an expired access token refresh forever?
No. Allow one controlled refresh, then clear the session and ask for authentication if refresh fails. A single-flight lock prevents tabs from overwriting each other's tokens. An old access token must never become a shop token.
Does HTTP 200 mean a GraphQL save succeeded?
No. Check errors, userErrors, data, field paths, and domain state. Keep the current value, identify correctable fields, and avoid a repeated mutation. Update the page only when the payload clearly represents success.
How can a broken account feature stay usable?
Narrow the experience to public products, cart, and a fixed authentication entry. Pause orders, addresses, profile, and mutations. Clear stale private state, verify discovery, issuer, token lifetime, and GraphQL error classes, then restore one account feature at a time.