Getting Started with Customer Account UI Extensions: From Scaffold to Testing on a Live Store [Tutorial]

The test environment is as of August 2026: Shopify CLI 3.90 and api_version 2026-04. Customer Account UI Extensions is a fast-moving area, so this article distinguishes between the official specifications (with links) and behavior verified in testing as of August 2026 that the official docs do not cover.

Introduction: from generating the extension to testing on a live store

Once you switch to new customer accounts, the account pages become a UI hosted by Shopify. The only mechanism for an app to insert its own UI there is Customer Account UI Extensions (UIE), so if you want to add anything to the account pages, you will be building one of these extensions.

Following the official tutorial takes you through generating an extension and rendering your own UI on a development store's account pages. Once it renders, the next thing you want is to check how it behaves against data from a live store (a store with production-equivalent data). But the official steps all assume a development store, so testing on a live store means switching to a deploy-based development cycle.

Once you have switched, development requires understanding the bundle size limit (64 KB compressed) and analyzing what makes up the bundle. On the environment side, we ran into three kinds of trouble on our test store that the official docs do not cover, so this article also walks through isolating each one from its symptoms and working around it.

What you need

Item Notes Source
Shopify Partner account Required to create and manage apps Official docs
Development store Required as the preview target for shopify app dev Official docs; also confirmed on our test store
Shopify CLI (latest) Install with npm install -g @shopify/cli or similar. Requires Node.js Official docs

The UIE you build is distributed as part of an app and managed in a development workflow separate from theme code. You need the three items above to work within the Shopify CLI app development flow. Section 2 explains why shopify app dev needs a development store as its target, together with why dev preview cannot be used on a live store.

You do not necessarily need your own backend. If your setup can be completed with the data Shopify provides plus processing inside the UIE, you can choose to have no backend at all. That is because Shopify hosts the extension code itself, as stated in the official docs. How to structure things when you deal with external APIs or secrets is covered in Calling External APIs from Customer Account UI Extensions: network_access, CORS, and Testing with Real Data.

Where the extension is inserted is determined by a unit called a target. The order list, order details, profile, and full pages each use different targets, so deciding where to render from the official list of targets before you implement reduces the chance of having to split the extension up again later.

1. Generate the extension: shopify app generate extension

If you do not have an existing app, create a scaffold, then move into that directory and generate the extension.

# Create the app scaffold (skip if you already have an app)
shopify app init

# Generate the extension inside the app directory
cd my-app
shopify app generate extension

Skip shopify app init if you are adding to an existing app. Running shopify app generate extension prompts you for the extension type; choose "Customer account UI extension" and it generates a TOML config file and a Preact-based component file. The steps match the official tutorial, and we confirmed the same generated output on our test store.

The minimal shopify.extension.toml: api_version, targeting, capabilities

The generated config file lives at extensions/<name>/shopify.extension.toml. To begin with, reading it as three elements, api_version, targeting, and capabilities, lets you follow which API the extension uses, where it renders, and what it is allowed to do.

api_version = "2026-04"

[[extensions]]
type = "ui_extension"
name = "My customer account extension"
handle = "customer-account-ui"

  [[extensions.targeting]]
  target = "customer-account.order-status.block.render"
  module = "./src/OrderStatusBlock.jsx"

  [extensions.capabilities]
  api_access = true

api_version is the API version the extension uses; this article assumes 2026-04. targeting pairs an insertion point (target) with the module rendered there; in the sample, ./src/OrderStatusBlock.jsx is assigned to customer-account.order-status.block.render. capabilities are platform-level permissions, and what each capability grants is laid out in the official docs (capabilities). network_access, which you need in order to call external APIs directly, is covered in detail in Calling External APIs from Customer Account UI Extensions: network_access, CORS, and Testing with Real Data.

Since 2025-10, components are written with Preact and the shopify.* globals. The older React style using reactExtension and useApi is no longer used (API reference). During testing, mixing in an older code sample found through search left two generations of syntax coexisting in a single extension, which produced errors that were hard to trace.

import '@shopify/ui-extensions/preact';
import {render} from 'preact';

export default async () => {
  render(<Extension />, document.body);
};

function Extension() {
  const order = shopify.order?.value;
  return (
    <s-text>
      {order ? `Showing order ${order.name}.` : 'Your first block is showing on the account pages.'}
    </s-text>
  );
}

This component reads the order name from shopify.order?.value and still renders the block when there is no order. The skeleton imports @shopify/ui-extensions/preact and calls render(<Extension />, document.body) inside the default-exported async function; only the content of <s-text> changes depending on whether an order exists.

The values available through the shopify.* globals differ by target. Before writing code, check the API reference to see whether the data you want is provided for that target. The details of data access behavior are in Reading and Writing Past Order Data with Customer Account UI Extensions: When to Use Signals, GraphQL, and Metafields.

There are constraints on combining targets. An extension that uses a full-page target such as customer-account.page.render cannot include any other target. Since the official change in October 2024, a configuration that combines them is rejected as a configuration error at deploy time. If your setup includes a full page, decide up front how to split your extensions.

2. Run it locally: shopify app dev and development stores

Connect the generated extension to a development store with the following command to check it.

shopify app dev

When you run it, you are asked to choose a development store as the target. Once it starts, follow the CLI's prompts to open the Dev Console (the p key in the official tutorial) and use the extension's preview link to confirm that your block appears on the account pages. Code changes reload automatically, so you can check the result in the preview after every change. This follows the official tutorial exactly, and we confirmed in August 2026 that the same flow gets you all the way to a rendered block on a development store.

There are four things to verify before adding features.

  1. The extension renders at the specified target
  2. Code changes are reflected in the preview
  3. The data needed for that target can be retrieved from shopify.*
  4. The extension as a whole does not disappear when a value is missing

Check in this order: rendering, data, then external communication. That way you can tell whether you are stuck on configuration, data, or networking without mixing them up.

3. Test with live store data: switching to deploy-based development

As development progresses, you reach a point where you want to verify against real data. Past order history, actual line item properties, and real metafields often cannot be fully reproduced with test data loaded into a development store; the key schemes, empty values, and cancellation states all differ. That is where our test store stopped being enough.

The error when pointing shopify app dev at a live store: Shop is not configured for app development

The official tutorial's steps all assume a development store (official tutorial). Point shopify app dev at a live store (including a Shopify Plus test store) and it is rejected with the following error.

Shop is not configured for app development

When you see this message, check the type of store you are connected to before logging in again or regenerating the extension. If you were pointing at a live store, this is expected behavior, and the fix is to switch to deploy-based development. Our test store hit this same error in August 2026.

Several dev preview problems are also reported in the CLI repository's issues, but before chasing those, checking whether the target is a development store is the faster way to isolate the problem.

The deploy-based development cycle: shopify app deploy --force

While testing on a live store, switch to a development style where you deploy with the following command after every change.

shopify app deploy --force

--force skips the confirmation prompt, so you can run the loop without interaction. You repeat four steps: change the code, run static checks, deploy, and verify on the actual pages. You lose hot reload, but being able to judge against real data often outweighs that, and on our test store the loop felt close to CI.

Before running the loop, check the deploy's scope of impact. Deploying publishes a new version of the app, which affects every store that has the app installed. Test with an app dedicated to testing, or one installed only on a test store, and do not repeat this on an app with real users.

Insert static checks before deploying. The round trip is long, so rework caused by runtime errors is expensive. What to include is covered in the esbuild section of Section 5.

Confirm the result on screen. A successful deploy log does not guarantee that the extension is working on the account pages, so treat the log and what actually appears on screen as two separate things.

4. Manage bundle size: the 64 KB limit and how to analyze the breakdown

UIE bundles have a limit of 64 KB compressed. The official docs explicitly state a "strict 64 KB compressed size limit," and bundles that exceed it are rejected at deploy time.

The same document explains how to analyze the breakdown. With CLI 3.92.0 or later, running shopify app build outputs an esbuild metafile (.metafile.json) in each extension's dist/; load it into esbuild's bundle analyzer and you can see which files and dependencies are inflating the bundle. The test environment for this article is CLI 3.90, so note the caveat that size analysis requires updating to 3.92 or later.

There are five approaches to keeping the size down.

  • Use Polaris Web Components instead of adding a UI library
  • Replace utilities such as date handling with built-ins like Intl.DateTimeFormat
  • Keep large static data out of the bundle and store it in metafields instead
  • Use the localization API for translations instead of bundling an i18n library
  • Check the size difference right after adding a dependency

Reading metafields from a UIE requires declaring them and configuring access. Those steps and the actual data access are covered in detail in Reading and Writing Past Order Data with Customer Account UI Extensions: When to Use Signals, GraphQL, and Metafields.

Even if a setup with no added dependencies stays under 64 KB, adding a single library can sharply shrink the margin to the limit. Checking the gap to 64 KB each time you add a dependency reduces the work of trimming the bundle all at once near the end.

5. Three environment-related problems and how to handle them

Three environment-related issues stopped us. All three actually occurred on our test store, and none of them are covered in the official docs.

When deploy succeeds but nothing renders: what esbuild does and does not detect

Even when a deploy succeeds, the extension can vanish from the account pages without showing any error on screen. There is no clue on the page itself; open the console in the browser's developer tools and you find a ReferenceError. What actually happened during testing was a refactor that removed a function definition along with other code, leaving only the reference behind. Renaming something and forgetting to update the call site is the same kind of undefined reference.

Shopify CLI builds with esbuild, which performs neither type checking nor detection of undefined identifiers. That is why a passing build does not mean working code: the undefined reference in our testing passed straight through the build, and the ReferenceError on initial render made the extension disappear entirely, not just partially. You cannot catch this by watching the deploy success log alone, and with the long round trips of deploy-based development, this kind of rework is especially costly.

Running tsc --noEmit or ESLint (no-undef) before deploying makes it easier to catch the undefined references that esbuild does not.

When [events] suddenly becomes required: where the error comes from and a temporary workaround

A deploy that had passed the day before was suddenly rejected with [events]: Required. This happened during testing, on an app that does not use the Events feature. If you have not changed the app's code or configuration, what is failing is not the extension but server-side validation of the app configuration. The same issue has been reported on the official developer forum, but as of August 24, 2026, there is no reply from staff.

In our testing, placing a dummy declaration in shopify.app.toml let the deploy pass even for an app that does not use Events. The declaration has to take a specific form, though. Three conditions had to be met: a [[events.subscription]] block is also required, only unstable is accepted for api_version, and uri must be an absolute HTTPS URL, not a relative path. In the dummy declaration below, replace the angle-bracketed parts of topics and uri with an accepted topic and an absolute HTTPS URL under your app's control.

[events]
api_version = "unstable"

[[events.subscription]]
topics = ["<an accepted topic>"]
uri = "<an absolute HTTPS URL under your app's control>"

This is strictly a temporary workaround while there is no official answer, and dummy or not, the subscription is actually registered. Record why you made the change, on the assumption that you will revert to the current configuration format once official guidance appears.

The platform's server-side requirements can change without your local CLI knowing. If deploys stop passing even though you have not changed code or configuration, searching the official forum for the error message makes it easier to tell whether a server-side requirement has changed.

403 when working with multiple organizations: the unit of CLI authentication

shopify app commands that had worked the day before suddenly started returning 403. If you recently ran shopify auth login against a different organization, that switch of target is what causes the 403. Shopify CLI's Partner authentication is a single session for the whole machine, so logging in to another organization moves the target at that moment. We confirmed this behavior on our test store as well.

A 403 can have other causes, such as insufficient permissions. Check which organization you are currently in together with your permissions on the target app; if the organization is wrong, the fix is to log in again and select the correct one.

For theme commands, a Theme Access app token (SHOPIFY_CLI_THEME_TOKEN) lets you set the target per command. We could not find an equivalent way to separate app commands. If you work with multiple organizations in parallel, the practical countermeasure is to make a habit of checking the target organization before any app command.

6. Where to get the latest steps and templates

The steps in this article are current as of August 2026. The CLI's interactive flow and the generated scaffold will change over time, so start from the official materials available at the time you begin.

Start building for customer accounts (official tutorial) has tutorials for each type of target. Shopify/customer-account-tutorials (official sample repository) contains complete sample code, such as wishlists and loyalty programs, that you can clone and use.

Once you have the official template, use the generated code's imports, render(), and shopify.* usage as your baseline, and add only the targets and capabilities you need.

What to read next

Once the extension renders, you need to check how the available data differs from target to target. Reading and Writing Past Order Data with Customer Account UI Extensions: When to Use Signals, GraphQL, and Metafields lays out, based on verified testing, the data access behavior you cannot learn from the API reference alone: whether line item properties on past orders can be read as-is, and why metafields sometimes cannot be read.

WRITTEN BY

Kitsune

Shopify Developer

I work across design and engineering, building Shopify stores and providing technical support. I have been building e-commerce on Shopify since around 2019 and have worked with more than 100 stores to date. What matters to me is giving shape, through design and development, to the specific requirements that existing apps alone can't reach. I share what I learn on the job.

Talk to us
BACK TO TECH INSIGHTS