DEV Community

João Cunha
João Cunha

Posted on

Building a Shopify Sidekick app extension: what the docs don't tell you

Shopify quietly opened Sidekick app extensions to all developers on June 17, 2026. If you build Shopify apps, this is a new surface worth grabbing early: your app's data becomes queryable from the AI assistant that ships in every admin, and almost nobody has shipped one yet.

I added one to my B2B onboarding app, Tradelane, in an afternoon. The happy path is genuinely small — but I hit four walls the docs don't warn you about. Here's the whole thing, walls included.

What you're building

A Sidekick data extension is three files and a backend route:

  • shopify.extension.toml — declares a headless UI extension with the special target admin.app.tools.data
  • tools.json — JSON Schema for the tools Sidekick can call
  • src/index.js — registers the tools; each one fetches from your app's backend
  • a backend route that returns the data

When a merchant asks Sidekick something your extension's description matches ("do I have pending wholesale applications?"), Sidekick calls your tool in a sandbox, your code fetches your backend, and the answer lands in the chat — with clickable links into your app.

The extension

extensions/sidekick-tools/shopify.extension.toml:

api_version = "2026-07"

[[extensions]]
name = "Tradelane Wholesale Application Tools"
description = "Search and summarize B2B wholesale applications: pending, approved, and rejected applications, applicant details, EU VAT (VIES) verification status."
handle = "sidekick-tools"
type = "ui_extension"

  [[extensions.targeting]]
  module = "./src/index.js"
  target = "admin.app.tools.data"
  tools = "./tools.json"
  instructions = "./instructions.md"
Enter fullscreen mode Exit fullscreen mode

The description is not decoration — it's how Sidekick decides your extension is relevant. Write it like you're prompting a model, because you are.

src/index.js is tiny. The sandbox gives you a global shopify object and authenticated fetch to your own backend:

export default () => {
  shopify.tools.register(
    "search_wholesale_applications",
    async ({ status, query, first }) => {
      const params = new URLSearchParams();
      if (status) params.set("status", status);
      if (query) params.set("query", query);
      if (first) params.set("first", String(first));
      const response = await fetch(`/api/sidekick/applications?${params}`);
      return response.json();
    },
  );
};
Enter fullscreen mode Exit fullscreen mode

tools.json declares the input schema (name, description, JSON Schema inputSchema). Same rule as the toml: tool descriptions are prompts. "Search extension for your app" gets you nothing; name the domain and the questions it answers.

You also must add a summary to your app's main shopify.app.toml — deploy fails without it:

[sidekick]
extensions_summary = "Search and summarize B2B wholesale account applications and buyer verification status."
Enter fullscreen mode Exit fullscreen mode

The backend route returns results in MCP Resource Links format, so answers are clickable:

return cors(Response.json({
  results: applications.map((app) => ({
    type: "resource_link",
    uri: `gid://tradelane/CompanyApplication/${app.id}`,
    name: app.companyName,
    mimeType: "application/vnd.tradelane.wholesale-application",
    _meta: {
      status: app.status,
      country: app.country,
      vatCheckStatus: app.vatCheckStatus ?? undefined,
    },
  })),
}));
Enter fullscreen mode Exit fullscreen mode

That cors(...) wrapper is wall #2. Let's do the walls.

Wall 1: the docs say you can't have this (you can)

The build guides still show a "Developer preview — we're selecting partners" banner with an application form. That banner is stale. Sidekick app extensions have been GA for every developer since June 17, 2026 — there's a changelog entry that says so. I almost didn't build this because the docs told me I wasn't allowed to. Trust the changelog, not the banner.

Wall 2: CORS, and an error message that tells you nothing

The extension sandbox runs on a Shopify origin. Your backend runs on yours. Every fetch your tool makes is cross-origin, and if your backend doesn't send CORS headers, Sidekick reports a vague "couldn't connect to the app" failure. Nothing in the extension guide mentions this.

If you're on shopify-app-remix / shopify-app-react-router, the fix is one line — the auth helper hands you a cors wrapper:

export const loader = async ({ request }) => {
  const { session, cors } = await authenticate.admin(request);
  // ... query your data ...
  return cors(Response.json(payload));
};
Enter fullscreen mode Exit fullscreen mode

Authentication, by the way, just works: the sandbox attaches a session token to your fetches, so authenticate.admin() resolves the shop like any embedded request.

Wall 3: you have one second

Sidekick expects tool responses within 1 second, and extensions that consistently miss get skipped. If your app scales to zero (Fly.io, Cloud Run, Lambda...), a cold start eats your entire budget before your query runs.

Practical mitigations: keep at least one machine warm, keep the query trivial (indexed lookups, small take), and keep _meta lean — there's also a 4,000-token response cap, and blowing it rejects the whole response.

Wall 4: testing lies to you twice

Two things made me think the extension was broken when it wasn't:

Sidekick remembers the conversation. I submitted new test data and re-asked in the same chat — Sidekick answered from memory: "no new applications since the last time we checked." It never called the tool again. Test every change in a fresh conversation.

The model summarizes. Sometimes it got the data and simply didn't mention the fields I cared about (it offered them as a follow-up instead). That's not a bug in your extension; it's model discretion. Your instructions.md — an optional file the docs undersell — is where you nudge which fields matter and when.

The rules of the road

Before you ship, read requirements 2.2.8 and 2.2.9: tools must match your app's stated functionality, and no promotions, upsells, or review begging inside the extension — content is checked at deploy and at runtime. Keep data tools read-only; anything that changes state belongs in action extensions where the merchant confirms.

Also: 20 tools per app, 5 intents, 512 chars per tool description. You won't hit these in v1.

Was it worth it?

The whole thing — extension, backend route, deploy, debugging all four walls — was one afternoon. In exchange, my app's data answers questions in the surface every merchant already uses, and "Works with Sidekick" goes on the listing while that sentence is still rare.

If you maintain a Shopify app with any queryable data in it, this is the cheapest differentiation you'll ship this year.

Questions welcome — I kept the war wounds fresh.

Top comments (0)