All Tutorials

Create a secure customer-facing subscription portal

All Tutorials

Create a secure customer-facing subscription portal

Create a secure customer-facing subscription portal

You can use the GraphQL Admin API to manage selling plans and subscription contracts from a merchant perspective. However, customers must also be able to manage their subscription information, including:

  • updating shipping and billing addresses
  • skipping, pausing, or unpausing a current subscription
  • cancelling a subscription

To allow customers to manage their subscriptions, we recommend using an app proxy. In this tutorial, you’ll learn how to set up a secure customer-facing subscription portal using an app proxy.

Why an app proxy is useful

Subscription portal app proxy diagram

An app proxy fetches data from an app proxy server to display on a page of the online store. By using an app proxy, customers will have a seamless experience without having to leave the shop's domain.

For example, let’s say the shop domain is coffee-roasters.com and the shop sells a coffee subscription. Shopify handles the storefront and the checkout through the coffee-roasters.com domain.

If the app managing the subscriptions provides its own customer-facing page, then that page may redirect the customer to a different domain. This redirection could confuse the customer. By using an app proxy, you can ensure the entire experience stays under the coffee-roasters.com domain.

To learn how to set up an app proxy, refer to our application proxy app extension tutorial.

Considerations

When setting up your customer-facing subscription portal, consider the following:

Cookie headers Cookie headers are stripped on app proxies. If you need to persist information between requests, such as authentication tokens, then use query string parameters.

Assets paths Apps run behind the app proxy in the storefront. You might need to adjust how assets are served from your app, since the URLs will use the app proxy's prefix.

Rendering an app proxy

You can render an app proxy in two ways:

  • Full page rendering (recommended)
  • Liquid code in the shop’s theme

Full page rendering

Full page rendering replaces the shop's theme entirely. Shopify recommends using the full page approach because apps and themes can execute untrusted Javascript code on the storefront.

Liquid code in the shop’s theme

If the HTTP response from the proxy URL has Content-Type: application/liquid set in the header, then Shopify renders any Liquid code in the request body in the context of the shop using the shop's theme.

Securely authenticate a customer into a subscription portal using an email

This section describes the steps for implementing an automated and secure customer portal that doesn’t require merchant interaction.

For this example, we’ll use the following app proxy configuration:

Subscription portal app proxy configuration

1. Expose an action to send an email

The first step is to provide a link or button that triggers an email to be sent to a customer. You should place a link or button in an area that’s accessible to the customer.

You can provide a link or button in one of the following areas:

  • on the customer account page on the storefront
  • on the order thank you page
  • on the order status page, by customizing the order status page to expose a link or button
  • in an email, sent directly to the customer after the app receives a subscription contract creation webhook. The email should contain a secure login link for the customer. For example: https://example-merchant.com/apps/my-subscriptions-app/customer_area?customer_id=X&shop_id=Y&authentication_token=Z

2. Authenticate and access the customer portal

After you’ve exposed an action to send an email, the customer can use an email link to access the secure subscription portal. At this point, we recommend using an authentication scheme of your choice, with two considerations:

  • Cookies are not available on app proxies, so any tokens or session IDs should be sent as query string parameters.
  • The login link should be revocable. Emails can be forwarded, so a fixed URL is not safe to use. Simply verifying the authenticity of the URL is also not a secure method. Ideally, a tuple that includes the shop, customer, and token should be stored in the database. The token should be sent as a query string parameter and deleted whenever convenient. For example, you can expire the token after X minutes or when the customer signs off. In the case of a sign off, the customer can start the process again with another email that contains a new login URL.

3. Call the selling plan and subscription contract APIs

After clicking the link, the customer is redirected to an endpoint predetermined by the URL.

For example, given the following URL: https://example-merchant.com/apps/my-subscriptions-app/customer_area?customer_id=X&shop_id=Y&authentication_token=Z

Shopify will call the following endpoint: https://my-subscriptions-app.com/app_proxy/customer_area?customer_id=X&shop_id=Y&authentication_token=Z

The app can then implement the relevant views and forms, such as:

  • billing and shipping addresses
  • pausing, unpausing, skipping, and cancelling subscriptions

All endpoints are forwarded from the app proxy into your app, as long as the form actions and links start with https://example-merchant.com/apps/my-subscriptions-app/, as shown in the example.

When your app receives the request, you can issue API calls using Shopify APIs, such as selling plans and subscription contracts mutations.

4. Update a customer's payment method

For security reasons, customer payment methods, such as credit cards, can't be updated using the API. Customers must request a second email that redirects them to a secure flow where credit card information can be safely updated.

After a customer has updated their payment method, the information is available to the app using the same customer payment method ID.

In this example, an email is sent to the email address associated with the specified token:

POST /admin/api/2021-01/graphql.json

mutation {
 customerPaymentMethodSendUpdateEmail(customerPaymentMethodToken: "b0761d88b49eba7568f3bcf795402080") {
   customer {
     id
   }
   userErrors {
     field
     message
   }
 }
}

JSON response:

{
 "data": {
   "customerPaymentMethodSendUpdateEmail": {
     "customer": {
       "id": "gid://shopify/Customer/2"
     },
     "userErrors": []
   }
 },
 "extensions": {
   "cost": {
     "requestedQueryCost": 10,
     "actualQueryCost": 10
   }
 }
}