Developer guide
Common Theme App Extension failures in Shopify and how to debug them
A failure-pattern guide for Shopify Theme App Extensions covering the issues that appear most often in production and the shortest debugging path for each one.
The block is installed but nothing renders
This is the most common fake runtime bug in Theme App Extensions. The extension package is fine, the deploy is fine, your Rails app is innocent for once, and the block still shows nothing because the merchant added it to a surface that cannot render it.
Shopify is explicit about the support rules. App blocks are not inserted into themes
automatically after install, and they only work on JSON templates in sections that support
blocks of type @app. If the merchant is on a theme surface that does not meet
those conditions, the block can feel "broken" even though nothing is wrong with the shipped
extension code.
“By default, themes don't include app blocks after an app is installed.”
“Blocks of type
@apparen't supported in statically rendered sections.”
The first ugly truth
Do not debug Liquid, JavaScript, or Rails until you have proved the target surface supports app blocks. Otherwise you are debugging a ghost story.
Fastest path to isolate the failure
- Confirm the merchant is using a JSON template, not a Liquid template.
Confirm the target section actually supports blocks of type
@app.Confirm the merchant added the block to the template you think they did, not some other product, page, or alternate template.
Confirm the block is not hidden by your own
available_ifcondition.Only after all of that, inspect console errors, missing assets, or bad data assumptions.
| Symptom | Most likely cause | Fastest proof | Fix |
|---|---|---|---|
| Block picker exists, but block never appears where expected | Wrong template or wrong section placement | Open the exact template in the theme editor and inspect placement | Guide merchant to the correct template or deep link into it |
| Block cannot be added to a section | Section does not support @app | Inspect section schema or verify theme support via theme files | Use another supported section or fallback onboarding path |
| Block appears on Dawn, fails on an older theme | Theme is not Online Store 2.0 compatible for your target | Check JSON template support and section compatibility | Show explicit unsupported-theme onboarding inside the app |
| Block is selectable but storefront stays empty | Your own visibility gate or missing required data | Temporarily remove conditional render path or inspect synced data | Repair the data contract, not the block shell |
Shopify also documents that only the main section of the default product template is required to support app blocks. That matters because developers often assume that if the theme is "OS 2.0" then every product surface will behave nicely. It will not. A theme can be modern and still have very selective support in the exact place you care about.
A Rails helper that improves support tickets immediately
Do not make merchants hunt for the right surface manually. Generate a precise deep link from your embedded app so support tickets start with the right editor state.
# app/services/shopify/theme_editor_links.rb
class Shopify::ThemeEditorLinks
def initialize(shop:)
@shop = shop
end
def add_product_block(handle:)
query = {
template: "product",
addAppBlockId: "#{@shop.shopify_api_key}/#{handle}",
target: "mainSection"
}.to_query
"https://#{@shop.shopify_domain}/admin/themes/current/editor?#{query}"
end
endThat helper does not magically solve compatibility. It just gets the merchant to the right battlefield faster. Shopify notes that app block deep links can target only sections that support app blocks, and they add only one app block at a time. That is a product constraint, not a suggestion.
The embed is deployed but never runs
App embeds create a special kind of confusion because they often look technically finished. The extension is deployed. The block file exists. The JavaScript bundle exists. Your logs are quiet. Then nothing runs because the merchant never enabled the embed.
Shopify could not be clearer here: app embed blocks are deactivated by default after install. If your onboarding assumes they are live immediately, you are manufacturing support work for future you, who will deserve better than that.
“By default, app embed blocks are deactivated after an app is installed.”
The good news is that app embeds are supported much more broadly than app blocks. Shopify documents them as working in both vintage and Online Store 2.0 themes because they do not depend on section-based app block support. So when an embed never runs, the first suspicion is usually activation state, not theme compatibility.
What to check before you touch code
Is the embed actually enabled in
Theme settings > App embedsfor the live theme?Does your embed schema limit templates with
enabled_onordisabled_onso the merchant is testing a page where it cannot run?Are you looking at the theme that is actually published, not the theme the onboarding flow happened to open?
Are you using a deep link with the right
api_keyand block handle, instead of olderuuidassumptions?
Shopify's embedded app surface now gives you a better way to expose status than asking the merchant to send screenshots from the theme editor. The App Home API can report extension information and activation records for theme app extensions. Use it as a support hint, not as divine truth.
// app/routes/app.onboarding.tsx or equivalent embedded admin surface
async function loadThemeExtensionStatus() {
const extensions = await shopify.app.extensions();
const themeExtensions = extensions.filter(
(extension) => extension.type === "theme_app_extension"
);
return themeExtensions.flatMap((extension) =>
extension.activations.map((activation) => ({
extensionHandle: extension.handle,
blockHandle: activation.handle,
name: activation.name,
target: activation.target,
status: activation.status,
placements: activation.activations,
}))
);
}This lets you render a sane onboarding card: embed available, embed active, or embed unavailable. It also lets you offer a one-click activation path instead of that timeless support classic, "Can you please click around in the editor until fate smiles on us?"
# app/services/shopify/theme_editor_links.rb
class Shopify::ThemeEditorLinks
def activate_app_embed(handle:, template: "product")
query = {
context: "apps",
template: template,
activateAppId: "#{@shop.shopify_api_key}/#{handle}"
}.to_query
"https://#{@shop.shopify_domain}/admin/themes/current/editor?#{query}"
end
endShopify documents this deep-link shape directly. Use api_key, not deprecated
uuid, and make sure the handle matches the embed file name exactly.
It works in the editor but not live
This one causes a lot of emotional damage because it tricks developers into thinking they have a caching bug, a CDN bug, or a "Shopify is broken today" bug. Usually they have a theme state bug.
Shopify's own UX guidance says merchants can preview edits in the theme editor before saving and publishing changes. That means the editor can absolutely show your block or embed working while the live storefront remains unchanged. The editor is not lying. You are just looking at a draft reality.
“Preview edits before saving and publishing changes.”
The usual mismatch matrix
| Editor behavior | Live storefront behavior | Real cause | What to verify |
|---|---|---|---|
| Feature works in preview | Feature missing live | Changes were never saved or published | Published theme, save action, publish action |
| Feature works on product A in editor | Feature missing on product B live | Different template assignment | Resource template suffix and actual live template |
| Feature works on draft theme | Feature missing on storefront | Wrong theme is published | Theme ID used in onboarding versus current live theme |
| Feature works only in editor | Feature disappears live | Your code depends on editor-only branch logic | Theme-editor detection logic and conditional script paths |
The shortest debugging move here is brutally simple. Stop reproducing in the editor for a moment. Identify the published theme, the exact live URL, the resource's actual template, and whether the feature exists without any editor context. Until those four facts are pinned down, everything else is theatre.
A logging pattern that saves real time
When you deep link merchants into the editor from your Rails app, log the theme assumptions you made at that moment. Support becomes much faster when you can compare what the app opened with what is live now.
# app/models/theme_extension_event.rb could persist something like this
payload = {
shop_id: shop.id,
action: "activate_app_embed",
theme_editor_target: {
template: "product",
handle: "app-embed",
theme_reference: "current"
},
initiated_at: Time.current.iso8601
}
Rails.logger.info(payload.to_json)That looks boring, which is exactly why it works. Boring structured logs beat mystical Slack threads every single time.
The extension loads but the feature still fails
Once you have proved that the block or embed is actually present, you have crossed an important line. This is no longer a placement problem. It is a data contract problem.
In practice, most of these failures come from one of five places: missing metafields,
missing metaobjects, stale synchronized data, a false available_if condition,
or a backend endpoint that was designed like a tiny accidental public API and then reacted
badly when the storefront used it like one.
Shopify documents available_if as a storefront and editor visibility gate backed
by an app-data metafield. That means your extension can look valid in code review while still
disappearing because the boolean state in Shopify never got written, or got written for the
wrong shop, or got written after the merchant tested.
“The condition can be included in the block's schema with the
available_ifattribute, and the state of the condition is stored in an app-data metafield.”
The Rails-backed failure modes that keep recurring
A sync job writes metafields or metaobjects asynchronously, but your extension expects them immediately after onboarding.
Your extension fetches app-owned data through an app proxy, but the endpoint expects cookies even though Shopify strips them for app proxies.
You trust a storefront parameter that should have been derived from a verified Shopify proxy request.
You changed the extension asset contract, but older theme settings or older synced state do not satisfy the new expectations.
Your block shell renders, but the real feature silently exits because required data is absent and you never surfaced that state in the DOM or logs.
The app proxy point is especially important for Rails apps. Shopify documents that proxy requests include a signed querystring and explicitly do not support cookies. If your endpoint depends on a session cookie, it will behave like a cat asked to do algebra.
“App proxies don't support cookies because the app is accessed through the shop's domain.”
A purpose-built Rails verifier for proxy-backed extension requests
# app/services/shopify/app_proxy_signature_verifier.rb
require "openssl"
class Shopify::AppProxySignatureVerifier
def initialize(params:, shared_secret:)
@params = params.to_h.stringify_keys
@shared_secret = shared_secret
end
def valid?
provided_signature = @params["signature"].to_s
return false if provided_signature.empty?
signed_params = @params.except("signature")
message = signed_params
.sort_by { |key, _| key }
.map { |key, value| "#{key}=#{Array(value).join(',')}" }
.join
digest = OpenSSL::HMAC.hexdigest("sha256", @shared_secret, message)
ActiveSupport::SecurityUtils.secure_compare(digest, provided_signature)
end
endThen keep the controller tiny and explicit. Theme extensions do not need your whole app. They need one reliable answer.
# app/controllers/app_proxy/feature_states_controller.rb
class AppProxy::FeatureStatesController < ApplicationController
skip_before_action :verify_authenticity_token
def show
verifier = Shopify::AppProxySignatureVerifier.new(
params: request.query_parameters,
shared_secret: ENV.fetch("SHOPIFY_API_SECRET")
)
return head :unauthorized unless verifier.valid?
shop = Shop.find_by!(shopify_domain: params[:shop])
render json: {
enabled: shop.feature_enabled?,
message: shop.feature_enabled? ? nil : "Feature disabled for this shop"
}
end
endBeyond backend correctness, there are two underrated tools here. Use Theme Check to catch theme app extension errors statically during development and CI. Use Shopify Theme Inspector when the block or embed technically works but seems to make the storefront slower in ways the merchant can feel.
Also remember that Shopify serves extension assets from its CDN. If the extension shell loads but your feature logic expects a brand-new backend contract, deployment order matters. A fresh asset talking to stale app data is one of the least glamorous, most common production bugs in app development.
The practical rule
When the extension is visibly present, stop debating installation. Audit state, contracts, and request verification. That is where the bug lives now.
Best internal links
Sources and further reading
Shopify Dev: Configure theme app extensions
Shopify Dev: Verify theme support
Shopify Dev: App blocks for themes
Shopify Dev: App Home API, extension status and activations
Shopify Dev: Authenticate app proxies
Shopify Dev: UX for theme app extensions
Shopify Dev: Theme Check
Shopify Dev: Shopify Theme Inspector for Chrome
Shopify Dev: Apps in the online store
FAQ
Can an app block be installed correctly and still never appear?
Yes. App blocks are not added to themes automatically after install, and they work only on JSON templates in sections that support blocks of type @app.
Can app embeds work on vintage themes?
Yes. Shopify documents app embed blocks as working on both vintage and Online Store 2.0 themes because they do not depend on section-based app block support.
Why does the extension look active in admin but the feature still fail?
Because activation proves placement, not business correctness. Your block or embed can be present while its required metafields, metaobjects, app-data metafields, or backend responses are missing, stale, or rejected.
Should a Theme App Extension call Rails for every storefront render?
Usually no. Extension assets are served by Shopify, and most storefront-safe state should live in theme settings, metafields, metaobjects, or app-data metafields. Reserve Rails calls for data that genuinely must be computed at request time.
Related resources
Keep exploring the playbook
Shopify Theme App Extension debugging guide
A debugging workflow for Shopify Theme App Extensions that covers extension deployment, theme activation, storefront rendering, and the specific places app blocks and app embeds usually fail.
How to detect whether a merchant enabled your app block or app embed
Practical detection patterns for Shopify app blocks and app embeds, including App Bridge extension status in the embedded app and theme inspection strategies when you need server-side truth.
Theme App Extensions with a Rails-backed Shopify app
How to structure Theme App Extensions when your Shopify app backend is Rails, including configuration ownership, data flow, and the boundary between theme-safe data and app-owned backend logic.