Guides

Developer guide

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.

Published by Addora
Updated March 12, 2026
14 min read
Editorial note: This is one of those Shopify topics where the bug is often not in your code. It is in the question. “Is the extension enabled?” sounds simple, but it collapses three different kinds of truth into one lying boolean.

Decide what kind of truth you need

Teams usually ask one vague question: is the extension enabled? That question is doing too much work and doing it badly.

In practice, there are at least three different questions hiding inside it:

  • Onboarding truth: should the embedded app show a setup banner, success state, or “go enable this” prompt right now?

  • Operational truth: does the live storefront theme currently contain the thing you think it contains?

  • Rendering truth: will this extension actually render for the specific page, resource, template, and conditions in front of a shopper?

These are related, but they are not interchangeable. Treating them as interchangeable is how you end up with a dashboard proudly announcing “installed” while the storefront is as empty as a startup’s staging database on a Friday evening.

“The App API provides information about the app and the status of its extensions.”

The working rule

Choose the truth source based on the decision you need to make. Do not ask one generic boolean to act as onboarding state, storefront audit, and rendering proof at the same time.

The Shopify truth sources available today

Shopify now gives you two serious detection paths, and they are both valid:

  • Embedded-app status via App Bridge: fast, merchant-facing, and ideal for setup UX.

  • Theme inspection: slower, backend-oriented, but better when you need server-side or offline truth.

Shopify’s App API now returns theme app extension information, including theme app blocks and app embeds, with per-activation status values of active, available, and unavailable. That is a major improvement for embedded onboarding flows because the admin surface can ask Shopify directly instead of turning your backend into a haunted JSON archaeology team.

At the same time, Shopify’s theme app extension docs still explicitly document theme file inspection for detecting app blocks and app embeds. That remains useful whenever the browser is not the source of truth you want, such as support tooling, cron-driven audits, or checks that must run before a merchant opens the app.

Truth sourceBest forMain strengthMain caution
App Bridge shopify.app.extensions()Embedded onboarding and setup UIFast and merchant-visibleNot the right tool for deep offline auditing
Theme files via Admin APIBackend verification and support toolingWorks outside the embedded appState interpretation is your problem now, enjoy
Storefront callback from the extension itselfTelemetry and runtime confirmationProves the extension executedOnly fires when storefront traffic exists

There is also an important product distinction here:

  • App blocks are added into theme sections and depend on compatible templates and section schemas.

  • App embeds inject into head, body, or compliance_head, and Shopify says they are deactivated by default after install.

So even before detection, the activation model is already different. That means your state model should be different too.

“By default, app embed blocks are deactivated after an app is installed.”

Use App Bridge extension status first

If your goal is to decide what setup UI to show inside the embedded app, App Bridge is the default winner.

Shopify’s App API exposes theme app extensions and their activation records. For theme app extensions, the important bits are:

  • type, which includes theme_app_extension
  • nested activation entries with a handle, name, target, and status

  • theme-specific placements under nested activations, including themeId

The subtle but important part is that target already tells you what kind of thing you are looking at:

  • section means app block
  • head, body, or compliance_head means app embed

That means you do not need one giant regex blob named detectIfStuffMaybeEnabledSomehow(). You can model blocks and embeds explicitly, which is both cleaner and less likely to get future-you cursed at in a code review.

React example for embedded setup state

import {useEffect, useMemo, useState} from 'react';
import {useAppBridge} from '@shopify/app-bridge-react';
 
type ThemeActivationStatus = 'active' | 'available' | 'unavailable';
 
type ThemeActivation = {
  handle: string;
  name: string;
  target: 'section' | 'head' | 'body' | 'compliance_head';
  status: ThemeActivationStatus;
  activations: Array<{
    target: string;
    themeId: string;
  }>;
};
 
type ExtensionInfo = {
  handle: string;
  type: 'ui_extension' | 'theme_app_extension';
  activations: ThemeActivation[];
};
 
function classifyActivation(target: ThemeActivation['target']) {
  return target === 'section' ? 'app_block' : 'app_embed';
}
 
export function ThemeExtensionStatusCard() {
  const shopify = useAppBridge();
  const [extensions, setExtensions] = useState<ExtensionInfo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    let cancelled = false;
 
    async function load() {
      try {
        setLoading(true);
        const result = (await shopify.app.extensions()) as ExtensionInfo[];
        if (!cancelled) setExtensions(result);
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : 'Unknown error');
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
 
    load();
 
    return () => {
      cancelled = true;
    };
  }, [shopify]);
 
  const themeActivations = useMemo(() => {
    return extensions
      .filter((extension) => extension.type === 'theme_app_extension')
      .flatMap((extension) =>
        extension.activations.map((activation) => ({
          extensionHandle: extension.handle,
          kind: classifyActivation(activation.target),
          ...activation,
        })),
      );
  }, [extensions]);
 
  const appEmbeds = themeActivations.filter((a) => a.kind === 'app_embed');
  const appBlocks = themeActivations.filter((a) => a.kind === 'app_block');
 
  if (loading) return <p>Loading extension status...</p>;
  if (error) return <p>Could not load extension status: {error}</p>;
 
  return (
    <div>
      <h2>Theme setup</h2>
 
      <h3>App embeds</h3>
      <ul>
        {appEmbeds.map((embed) => (
          <li key={`${embed.extensionHandle}:${embed.handle}`}>
            {embed.name} ({embed.handle}) → {embed.status}
          </li>
        ))}
      </ul>
 
      <h3>App blocks</h3>
      <ul>
        {appBlocks.map((block) => (
          <li key={`${block.extensionHandle}:${block.handle}`}>
            {block.name} ({block.handle}) → {block.status}
          </li>
        ))}
      </ul>
    </div>
  );
}

This is the right place to drive onboarding cards like:

  • Embed active: great, continue
  • Embed available: show deep link to activate
  • Embed unavailable: surface that the block exists but is not currently available, often because of an available_if condition

Shopify documents unavailable for theme app blocks or embeds that exist but are disabled, such as via available_if. That means unavailable is not the same as “merchant forgot setup”. Sometimes it means “your app logic said no”.

Use App Bridge with deep links, not with vibes

If the status says an embed is available but not active, link the merchant straight to the right activation flow. Shopify documents an app embed deep link using activateAppId:

export function buildAppEmbedDeepLink({
  shop,
  template = "index",
  apiKey,
  handle,
}: {
  shop: string;
  template?: string;
  apiKey: string;
  handle: string;
}) {
  const params = new URLSearchParams({
    context: "apps",
    template,
    activateAppId: `${apiKey}/${handle}`,
  });
 
  return `https://${shop}/admin/themes/current/editor?${params.toString()}`;
}

For app blocks, use Shopify’s documented addAppBlockId flow and target a sensible location such as a new Apps section on the default product template.

The default in 2026

For setup UX inside the embedded app, use App Bridge first and reserve theme inspection for the places where frontend truth is not enough.

When server-side theme inspection still makes sense

Theme inspection still matters a lot. It just should not be your first instinct for every onboarding banner.

Use server-side inspection when you need one of these:

  • support tooling that runs without the merchant opening the app
  • scheduled audits
  • draft-theme checks
  • pre-publish verification
  • historical or cross-theme analysis
  • backend-only workflows and webhooks

Shopify’s theme extension docs still describe detection through theme files. For app blocks, the reference is in template JSON. For app embeds, the reference is in config/settings_data.json, and Shopify explicitly notes that an embed remains there after first enablement with disabled: true if later turned off.

“App embed blocks appear in settings_data.json.”

There is one important implementation update that a lot of older articles miss: Shopify’s docs still mention the Asset REST API for detection, but Admin GraphQL got theme and theme-file parity starting in API version 2024-10. For new backend reads, GraphQL is usually the cleaner default because you can fetch theme metadata and file content through the same API family.

That does not mean REST is wrong. It means you no longer need to reach for the older path by default when your real job is read-only inspection.

What to inspect for each case

Extension typePrimary file(s)What you actually checkMain trap
App embedconfig/settings_data.jsonBlock entry exists and disabled is not truePresence alone is not activation
App blocktemplates/*.jsonRelevant section or Apps section contains your block typeInstalled somewhere is not rendered everywhere

Prefer GraphQL reads for new backend implementations

Shopify’s Admin GraphQL themes query can find the published theme with role MAIN. The theme query can then fetch theme files and file content. Shopify documents up to 50 filenames per request and up to 2500 files in a fetch, subject to payload limits.

query MainTheme {
  themes(first: 1, roles: [MAIN]) {
    nodes {
      id
      name
      role
    }
  }
}
query ThemeFilesForDetection($themeId: ID!) {
  theme(id: $themeId) {
    id
    name
    role
    files(
      filenames: ["config/settings_data.json", "templates/*.json"]
      first: 50
    ) {
      nodes {
        filename
        body {
          ... on OnlineStoreThemeFileBodyText {
            content
          }
        }
      }
    }
  }
}

Two cautions here:

  • Do not blindly fetch every file in a giant merchant theme on every request just because you technically can.

  • For app blocks, do not confuse “block exists in some template JSON” with “this shopper will see it on this page”.

A Rails-friendly server-side detection pattern

A clean Rails pattern is to separate detection into two service objects:

  • ThemeExtensionSnapshotFetcher for fetching and parsing theme files

  • ThemeExtensionDetector for interpreting those files into business meaning

That keeps your Shopify API transport, JSON parsing, and product logic from becoming one big procedural lasagna.

Step 1: fetch the published theme and relevant files

class ThemeExtensionSnapshotFetcher
  MAIN_THEME_QUERY = <<~GRAPHQL
    query {
      themes(first: 1, roles: [MAIN]) {
        nodes {
          id
          name
          role
        }
      }
    }
  GRAPHQL
 
  THEME_FILES_QUERY = <<~GRAPHQL
    query($themeId: ID!, $filenames: [String!]) {
      theme(id: $themeId) {
        id
        name
        role
        files(filenames: $filenames, first: 50) {
          nodes {
            filename
            body {
              ... on OnlineStoreThemeFileBodyText {
                content
              }
            }
          }
        }
      }
    }
  GRAPHQL
 
  def initialize(admin_client:)
    @admin_client = admin_client
  end
 
  def call
    theme = fetch_main_theme
    files = fetch_files(theme.fetch("id"))
 
    {
      theme: theme,
      files: index_files(files)
    }
  end
 
  private
 
  attr_reader :admin_client
 
  def fetch_main_theme
    response = admin_graphql(MAIN_THEME_QUERY)
    response.dig("data", "themes", "nodes")&.first ||
      raise("No published theme found")
  end
 
  def fetch_files(theme_id)
    response = admin_graphql(
      THEME_FILES_QUERY,
      {
        themeId: theme_id,
        filenames: [
          "config/settings_data.json",
          "templates/*.json"
        ]
      }
    )
 
    response.dig("data", "theme", "files", "nodes") || []
  end
 
  def index_files(files)
    files.each_with_object({}) do |file, hash|
      hash[file.fetch("filename")] = file.dig("body", "content")
    end
  end
 
  def admin_graphql(query, variables = {})
    # Replace with your actual Shopify GraphQL client
    admin_client.query(query: query, variables: variables)
  end
end

Step 2: detect app embeds correctly

App embeds are the easier case because Shopify documents their location and lifecycle more clearly.

class AppEmbedDetector
  def initialize(settings_data_json:, app_slug:, embed_handle:)
    @settings_data_json = settings_data_json
    @app_slug = app_slug
    @embed_handle = embed_handle
  end
 
  def call
    current = parsed.fetch("current", {})
    blocks = current.fetch("blocks", {})
 
    matching_block = blocks.values.find do |block|
      type = block["type"].to_s
      type.include?("shopify://apps/#{app_slug}/blocks/#{embed_handle}/")
    end
 
    return { installed_once: false, active: false } if matching_block.nil?
 
    {
      installed_once: true,
      active: matching_block["disabled"] != true
    }
  end
 
  private
 
  attr_reader :settings_data_json, :app_slug, :embed_handle
 
  def parsed
    @parsed ||= JSON.parse(settings_data_json)
  end
end

The important bit is the distinction between:

  • installed_once: false, which means no trace found
  • installed_once: true, active: false, which means the embed was enabled before but is currently disabled

If you only check presence, you will misreport disabled embeds as active. That is not a hypothetical. That is a real class of bug, and it will make your onboarding UI gaslight merchants.

Step 3: detect app blocks without overselling certainty

App blocks are more annoying because “the block exists in a template file” is not equivalent to “the merchant has finished setup for the page that matters”.

class AppBlockDetector
  def initialize(template_files:, app_slug:, block_handle:)
    @template_files = template_files
    @app_slug = app_slug
    @block_handle = block_handle
  end
 
  def call
    matches = template_files.filter_map do |filename, content|
      next if content.blank?
 
      parsed = JSON.parse(content)
      next unless contains_block_type?(parsed)
 
      {
        filename: filename,
        installed: true
      }
    rescue JSON::ParserError
      nil
    end
 
    {
      installed_somewhere: matches.any?,
      templates: matches
    }
  end
 
  private
 
  attr_reader :template_files, :app_slug, :block_handle
 
  def contains_block_type?(node)
    case node
    when Hash
      node.any? do |key, value|
        (key == "type" && value.to_s.include?(expected_type_fragment)) ||
          contains_block_type?(value)
      end
    when Array
      node.any? { |value| contains_block_type?(value) }
    else
      false
    end
  end
 
  def expected_type_fragment
    "shopify://apps/#{app_slug}/blocks/#{block_handle}/"
  end
end

Notice the return value is installed_somewhere, not definitely_visible_everywhere_forever. That is not me being precious with naming. It is the difference between a useful backend fact and a support nightmare.

Step 4: turn raw detection into product language

class ThemeExtensionDetector
  def initialize(snapshot:, app_slug:, embed_handle:, block_handle:)
    @snapshot = snapshot
    @app_slug = app_slug
    @embed_handle = embed_handle
    @block_handle = block_handle
  end
 
  def call
    files = snapshot.fetch(:files)
 
    embed = AppEmbedDetector.new(
      settings_data_json: files["config/settings_data.json"].to_s,
      app_slug: app_slug,
      embed_handle: embed_handle
    ).call
 
    template_files = files.select { |filename, _| filename.start_with?("templates/") }
 
    block = AppBlockDetector.new(
      template_files: template_files,
      app_slug: app_slug,
      block_handle: block_handle
    ).call
 
    {
      theme_name: snapshot.dig(:theme, "name"),
      app_embed: embed,
      app_block: block
    }
  end
 
  private
 
  attr_reader :snapshot, :app_slug, :embed_handle, :block_handle
end

This gives your app honest output:

  • App embed: never enabled, enabled now, or enabled before but off
  • App block: found in these templates, or not found

Honest output beats magical output. Magical output is how support tickets reproduce in exactly one merchant theme named “Dawn copy copy FINAL for real”.

Edge cases that make your boolean lie

This is where most detection systems stop being architecture and start being folklore. Here are the traps that actually matter.

1. Published-theme truth is not draft-theme truth

Shopify staff said in February 2026 that App Bridge extension status for theme app extension checks currently supports the published theme. That is fine for many setup flows, but it is not enough for draft-theme workflows, preview tooling, or “are we ready to publish?” checks.

If unpublished themes matter, inspect the theme you care about directly from the backend.

2. App embed presence is not current activation

Shopify documents that once an app embed has been enabled for the first time, it stays in settings_data.json even after being disabled, with disabled: true. So “found in settings_data.json” is not the same as “currently active”.

3. App block installed somewhere is not storefront rendering proof

App blocks live inside theme sections and templates. A block can be present in:

  • the wrong template
  • a non-default template
  • a template not assigned to the resource you care about
  • a section whose own rendering conditions are not met

So if your product team asks, “did the merchant install the block?”, ask back: “installed where?”

4. available_if can make your extension unavailable

Shopify supports available_if for app blocks and app embeds through an app-data metafield boolean. That means your extension can exist but be unavailable. If you flatten that into a generic falsey “not installed” state, your setup messaging becomes misleading.

5. Multiple handles mean multiple truths

Many apps ship more than one block or embed. If your code says “some theme app extension is active”, that may be technically true and product-wise useless.

Detect by exact handle and exact intent:

  • badge block
  • reviews block
  • analytics-widget embed
  • consent-loader embed

6. Recent platform behavior can lag your assumption

Early 2026 forum threads reported that App Bridge status for app embeds could remain active after disablement until Shopify fixed a bug, and that published-theme coverage should be assumed for theme app extension status today. So treat newly shipped platform behavior with a little healthy paranoia until it proves itself in production.

Do not persist certainty you did not earn

Store detection as a snapshot with a timestamp, not as eternal truth. Re-check whenever onboarding, support, publishing, or billing logic actually depends on it.

The decision matrix to standardize on

If you want one house rule for the team, use this:

Question you are answeringUse this sourceWhy
Should the embedded app show a setup checklist item?App Bridge extension statusFast and native to the merchant workflow
Should we deep link the merchant to activate an embed?App Bridge extension statusBest fit for current-session UX
Did the live theme ever enable this embed?settings_data.jsonCan distinguish installed-once from active-now
Is this block present in the published theme files?Theme templates via backendWorks offline and outside the embedded app
Is this ready in a draft theme before publish?Theme files for that theme IDPublished-theme App Bridge checks are not enough
Will this render for this specific product or page?Theme files plus resource/template contextNeeds more than existence checks

And if you want the opinionated version:

  • Do not call the backend on every admin page load just to guess what App Bridge can now tell you directly.

  • Do not trust App Bridge alone for draft-theme or offline use cases.

  • Do not reduce all extension state to one boolean.

  • Do model app blocks and app embeds separately.

  • Do return explicit states like active, available, unavailable, installed_somewhere, and installed_once_but_disabled.

The apps that feel polished here are usually not doing something magical. They are just asking a narrower question and answering it honestly.

Best internal links

Sources and further reading

FAQ

Can I detect app embed activation without read_themes?

In the embedded app, yes, App Bridge is now the best first check. For backend or offline verification, you still need theme access patterns such as read_themes and theme file inspection.

Does App Bridge tell me about all themes?

Not reliably for every use case. As of February 2026, Shopify staff said App Bridge extension status currently supports the published theme for theme app extension checks. If draft-theme truth matters, inspect theme files directly.

Should I store enabled state in my database?

Only as a cache or operational hint. Recompute when setup state matters. Theme state can drift, merchants can switch themes, and app availability rules can change.

Why can an app block appear installed but still not render?

Because installed somewhere in a theme is not the same as rendered for the current resource. Template assignment, section compatibility, parent section state, and available_if rules can all prevent output.

Related resources

Keep exploring the playbook

Guides

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.

guidesShopify developertheme app extensions