Skip to content

Anatomy of the app template ​

The React Router template is a full-stack app. React Router v7 (framework mode) renders both the pages and the server routes. Prisma stores sessions.

text
my-app/
├─ app/
│  ├─ shopify.server.ts     ← configures @shopify/shopify-app-react-router (auth, API version, session storage)
│  ├─ db.server.ts          ← Prisma client
│  ├─ routes/
│  │  ├─ app.tsx            ← layout for embedded pages (App Bridge + nav)
│  │  ├─ app._index.tsx     ← home page inside Shopify Admin
│  │  ├─ auth.$.tsx         ← auth callback catch-all
│  │  └─ webhooks.*.tsx     ← one route per webhook topic
│  └─ root.tsx
├─ extensions/              ← theme / checkout / Function extensions live here
├─ prisma/schema.prisma     ← Session table (+ your own models)
├─ shopify.app.toml         ← THE app config: scopes, URLs, webhooks, app proxy
└─ package.json

shopify.server.ts — the heart ​

ts
import { shopifyApp, ApiVersion, AppDistribution } from "@shopify/shopify-app-react-router/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY,
  apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
  apiVersion: ApiVersion.October25, // pin a version; bump it each quarter
  scopes: process.env.SCOPES?.split(","),
  appUrl: process.env.SHOPIFY_APP_URL || "",
  sessionStorage: new PrismaSessionStorage(prisma),
  distribution: AppDistribution.AppStore,
});

export const authenticate = shopify.authenticate;
export default shopify;

API version enum

The enum name tracks Shopify's quarterly versions (2026-10 is current stable). Check which enum values your installed package version exports.

Every route starts with authenticate ​

tsx
// app/routes/app._index.tsx
export const loader = async ({ request }) => {
  const { admin, session } = await authenticate.admin(request);
  const res = await admin.graphql(`{ shop { name } }`);
  return { shop: (await res.json()).data.shop };
};

authenticate.admin checks the session token, exchanges it for an access token if needed, and hands you a ready-to-use admin.graphql client.

HelperUse it in
authenticate.admin(request)Embedded admin pages and their actions
authenticate.webhook(request)Webhook routes (verifies HMAC)
authenticate.public.appProxy(request)App proxy routes called from the storefront
authenticate.public.checkout(request)Requests from checkout UI extensions

shopify.app.toml ​

toml
client_id = "abc123"
name = "Order Notes Notifier"
application_url = "https://example.com"
embedded = true

[access_scopes]
scopes = "read_orders,write_products"

[auth]
redirect_urls = ["https://example.com/auth/callback"]

[webhooks]
api_version = "2026-10"

  [[webhooks.subscriptions]]
  topics = ["app/uninstalled"]
  uri = "/webhooks/app/uninstalled"

  [[webhooks.subscriptions]]
  compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
  uri = "/webhooks/compliance"

Changing scopes or webhooks in the toml and running shopify app deploy updates them for every install. You don't need per-shop API calls.

Multiple environments

Keep shopify.app.toml (prod) and shopify.app.staging.toml, which point at two different apps. Switch between them with shopify app config use staging.

Check yourself ​

  1. What three things does authenticate.admin do for you?
  2. You add read_customers to the toml. What happens to existing installs after deploy?