Developer guide
React frontend plus Rails backend for Shopify embedded apps
How to structure a Shopify embedded app with React on the frontend and Rails on the backend, including auth boundaries, deployment shape, and the operational tradeoffs that matter in production.
Why this split works well for Shopify apps
Shopify embedded apps rarely stay small. The app home starts as a dashboard, then billing logic arrives, then webhooks, then background syncs, then admin UI extensions, then some merchant asks for a bulk action that turns your neat little frontend into an operational control panel with opinions.
React plus Rails works well because it matches the real platform split. Shopify says app home is the primary place where merchants engage with your app, and on the web it renders inside an iframe in Shopify admin. Shopify also says App Bridge is what lets your app communicate with the admin and create admin-owned UI like navigation menus, title bars, and save bars.
“On the web, the surface is an iframe”
“App Bridge components don't render as part of the app's component hierarchy.”
That last detail is more important than it looks. It means App Bridge is not your UI framework. It is your bridge to Shopify-admin-owned behavior. React still owns your actual component tree, client interaction state, and the merchant workflows inside the app surface. Rails owns the parts that must remain true when React is gone, reloaded, broken, or being “refactored” by someone with too much confidence.
Shopify also requires public apps to provide a consistent embedded experience and use the latest App Bridge. So this split is not just nice engineering hygiene. It aligns with the platform expectations your app is being judged against.
“Your app must provide a consistent embedded experience”
Optimize for year two, not week two
The best Shopify app architecture is the one that still makes sense after you add webhooks, bulk jobs, billing, admin extensions, and a merchant-specific “one tiny exception” that somehow becomes permanent.
The minimum request lifecycle to keep
Keep the request lifecycle narrow. The app loads inside Shopify admin. App Bridge handles the embedded context. React makes backend requests. Shopify session-token authentication protects those requests. Rails verifies them, resolves the shop and user context, and then decides what durable work needs to happen.
Shopify’s session-token docs say the embedded app first loads unauthenticated and
serves the frontend shell. After that, the frontend includes a session token in the
authorization header for backend requests. Shopify’s embedded-authorization docs also
say that when App Bridge is set up properly, it appends the
authorization header automatically.
“When your embedded app first loads, it's unauthenticated”
“it'll append a new header field
authorization”
In practice, the minimum healthy lifecycle looks like this:
- Shopify admin opens your embedded app home.
- React renders the shell and merchant UI.
- React calls Rails endpoints for authenticated app data.
- Rails verifies the request context and resolves shop plus user state.
Rails either reads or writes your own database, calls the Admin API, or enqueues background work.
- React renders the result and stays blissfully ignorant about token exchange.
That is the boundary worth protecting. React initiates requests. Rails owns durable correctness. If your frontend starts deciding which Admin API token type to use, your architecture has already wandered into the forest.
export async function apiFetch<T>(input: RequestInfo, init: RequestInit = {}) {
const response = await fetch(input, {
...init,
headers: {
Accept: "application/json",
...(init.headers || {}),
},
});
if (!response.ok) {
throw new Error(`Request failed with ${response.status}`);
}
return response.json() as Promise<T>;
}The point of this example is not the fetch wrapper. It is the absence of auth drama. In a healthy embedded setup, your frontend request code should be boring.
What belongs in React versus Rails
The cleanest split is responsibility by failure mode. Ask a simple question: if the merchant closes the tab, which parts still need to remain correct?
| Concern | React frontend | Rails backend | Why |
|---|---|---|---|
| Navigation and screens | Yes | No | The embedded app surface is a UI problem |
| Forms, tables, filters, onboarding polish | Yes | No | These are merchant interaction concerns |
| Session verification and shop resolution | No | Yes | Trusted auth context belongs on the server |
| Admin API orchestration | No | Yes | Requires durable credentials and controlled retries |
| Persistence and audit trails | No | Yes | The browser is not a reliable system of record |
| Webhook handling and jobs | No | Yes | They must run without an open browser |
| Optimistic UI and local state | Yes | No | Fast UX lives in the client |
| Business rules that multiple surfaces need | No | Yes | They should survive app home, extensions, and jobs |
Rails should own token verification, token exchange, Admin API clients, billing checks, persistence, job orchestration, and anything else that has to remain true when the UI is closed. React should own the merchant experience, including navigation, tables, forms, optimistic updates, and workflow polish.
Teams get into trouble when React knows too much about Shopify auth and Rails starts returning too much presentation logic. Then every new surface becomes a negotiation between two halves of the app that both think they are in charge.
Shopify’s own platform split reinforces this. App home is hosted by you inside the admin surface, while App Bridge is how you render admin-owned controls around that surface. That is a strong hint that your app should keep UI responsibilities and durable backend responsibilities clearly separated.
“App Bridge enables you to do the following from your app home”
The backend contract React should call
A good embedded frontend does not call “everything”. It calls a deliberate backend contract. That contract should be shaped around merchant actions and app workflows, not around random database tables or whatever your GraphQL client happened to return.
Good backend contracts usually look like this:
GET /api/embedded/dashboardfor the app-home boot payloadPOST /api/embedded/settingsfor merchant configuration writesPOST /api/embedded/syncsfor async work requestsGET /api/embedded/billingfor billing and plan state
Bad backend contracts usually look like this:
- generic pass-through GraphQL endpoints
- controllers that mirror frontend route names instead of business actions
- endpoints that only make sense for app home and cannot be reused anywhere else
- JSON payloads that dump raw internal models because it was “quicker”
A simple Rails shape:
# config/routes.rb
namespace :api do
namespace :embedded do
resource :dashboard, only: :show
resource :settings, only: [:show, :update]
resources :syncs, only: :create
end
endclass Api::Embedded::DashboardController < ApplicationController
def show
render json: DashboardPayloadBuilder.call(
shop: Current.shop,
shopify_user_id: Current.shopify_user_id
)
end
endclass DashboardPayloadBuilder
def self.call(shop:, shopify_user_id:)
{
shop: shop.shopify_domain,
userId: shopify_user_id,
plan: shop.current_plan_name,
pendingSyncs: shop.sync_runs.pending.limit(5).map { |run| serialize_sync(run) },
featureFlags: {
bulkEdit: shop.bulk_edit_enabled?,
autoSync: shop.auto_sync_enabled?
}
}
end
def self.serialize_sync(run)
{
id: run.id,
status: run.status,
startedAt: run.started_at
}
end
endThe important thing is not the JSON. It is that the frontend is calling an app-owned contract, not smuggling backend architecture decisions into the browser.
The backend API rule
React should call endpoints named after app actions and app state, not after your
internal tables. Merchants clicked a button, not a row in sync_runs.
Deployment and routing choices
Two deployment shapes work well in practice.
| Shape | When it fits | Strength | Main caution |
|---|---|---|---|
| Rails serves the built frontend | Small team, Shopify-specific app home, one release train | Operationally simple | Frontend deploys are coupled to backend deploys |
| Separate React and Rails deploys | Independent frontend cadence, multiple UI surfaces, larger team | Clearer API boundary | More deployment and environment coordination |
If the app is mostly Shopify app home UI and a Rails backend, a single deploy is often the better trade. Fewer moving pieces, fewer cross-origin headaches, fewer “why is staging talking to production” moments. Separate deploys make more sense when the frontend genuinely has its own life.
Shopify’s docs reinforce two constraints regardless of which shape you pick. First, your app home still lives inside the Shopify admin surface. Second, public apps are expected to use the latest App Bridge and provide a consistent embedded experience within the admin.
“The primary place where users engage with your app is its app home”
“all apps must use the latest Shopify App Bridge”
One more practical point: if you are building a new public app, Shopify says new public apps must use the GraphQL Admin API, with REST now considered legacy for new public-app work. That makes a Rails backend even more useful as the place where Admin-API orchestration lives cleanly instead of leaking into the client.
“all new public apps must be built exclusively with the GraphQL Admin API”
Designing for extensions jobs and webhooks
This is where the React plus Rails split really earns its keep. Your app home is not the only thing that will want backend logic. Admin UI extensions, webhook processors, and background jobs all want the same business rules without sharing the same request shape.
Shopify documents authenticated requests from admin UI extensions to your app’s
backend. It also says relative fetch() URLs in admin UI extensions
resolve against your app URL when the backend is on the same domain, and the
authorization header is added automatically. That is a strong reason to keep your
business logic in reusable Rails services instead of burying it inside an app-home
controller.
“an
Authorizationheader is automatically added”
That gives you a healthy layering model:
- controllers verify request context and shape input
- service objects implement business logic
- jobs run asynchronous or retryable work
- webhook handlers translate Shopify events into backend actions
- extensions call the same service layer through their own request path
class SyncProducts
def self.call(shop:, actor_shopify_user_id: nil, source:)
# business logic that app home, admin actions, and jobs can all reuse
SyncRun.create!(
shop: shop,
actor_shopify_user_id: actor_shopify_user_id,
source: source,
status: "queued"
)
ProductSyncJob.perform_later(shop.id)
end
endclass Api::Embedded::SyncsController < ApplicationController
def create
SyncProducts.call(
shop: Current.shop,
actor_shopify_user_id: Current.shopify_user_id,
source: "app_home"
)
head :accepted
end
endclass Webhooks::ProductsUpdateController < ApplicationController
def create
SyncProducts.call(
shop: Current.shop,
source: "webhook"
)
head :ok
end
endThe winning move is not “make everything reusable” in the abstract. It is “make the business logic reusable while keeping the request authentication specific to the surface”. App home auth is not webhook auth. Admin extension auth is not app-home auth. The service layer should not care.
Failure modes that show up later
These are the failure modes that do not always show up in week one, but absolutely show up later:
Trusting shop context from the client. The frontend may know which shop is open. That does not make it the authority.
Letting React own Shopify authorization decisions. React should initiate requests, not decide token exchange strategy or long-lived credential use.
Building endpoints that only work for app home. If admin extensions, jobs, or webhooks cannot reuse the business logic, your backend is too UI-shaped.
Returning HTML-shaped responses from Rails for everything. That makes new surfaces harder, not easier.
Mixing online and offline token usage without explicit service boundaries. Then debugging permissions becomes a group activity.
Letting frontend route structure leak into install or auth behavior. Shopify auth boundaries should not care what your tab layout looks like this week.
Over-separating deployments too early. Two deploys can be good. Two deploys plus vague ownership plus duplicated environment config is how tiny apps create unnecessary operational complexity.
Shopify’s starter paths already push you toward a sensible embedded architecture.
Shopify CLI scaffolds starter apps with embedded auth boilerplate, and the current
shopify_app defaults use session tokens for embedded admin auth and
OAuth 2.0 token exchange for API access. So when teams end up with an embedded
frontend that is also moonlighting as a backend, it is usually self-inflicted.
“starter app with boilerplate code that handles authentication and authorization”
“By default, this app is configured to use session tokens”
The common theme in all of these mistakes is boundary drift. Once the boundary drifts, auth gets weird, extensions get awkward, jobs get special cases, and your React app slowly develops backend opinions it absolutely should not have.
The smell test
If closing the browser would break the correctness of the operation, the operation belongs in Rails.
Best internal links
Shopify session tokens in Rails
for the embedded auth boundary and what the gem already handles.
Shopify OAuth and managed install in Rails apps
for install flow, token acquisition, and backend credential lifecycle.
Shopify Admin GraphQL patterns in Rails
for keeping Admin API orchestration where it belongs.
Sources and further reading
Shopify Dev: Apps in admin
Shopify Dev: About Shopify App Bridge
Shopify Dev: About session tokens
Shopify Dev: Set up embedded app authorization
Shopify Dev: Exchange a session token for an access token
Shopify Dev: Admin UI extensions app authentication
Shopify Dev: App Store requirements
Shopify shopify_app README
FAQ
Should React call Shopify directly for most app logic?
Usually no. React should drive the merchant experience and call your backend. Rails should own durable state, Admin API orchestration, and anything that must stay correct when the browser is closed.
Should I deploy React separately from Rails?
Sometimes, but not by default. If the app is mostly Shopify app home UI, a single deploy is often simpler. Separate deploys are more useful when the frontend truly has its own release cadence or multiple non-Rails surfaces.
Does App Bridge replace my frontend router or state model?
No. App Bridge handles communication with Shopify admin and admin-owned UI elements like nav, title bars, and save bars. Your app still owns its own UI tree and client-side state.
Can admin UI extensions reuse the same Rails backend?
Yes, and they should. Shopify documents authenticated requests from admin UI extensions to your app backend, so a clean Rails service layer can support both your embedded app home and your extensions.
Related resources
Keep exploring the playbook
Shopify app proxy guide for Rails backends and React frontends
A practical guide to using Shopify app proxies with a Rails backend and React frontend, including request validation, frontend architecture, and the cases where proxies are the wrong tool.
Shopify OAuth and managed install in Rails apps
A guide to the current Shopify installation model for Rails apps, explaining where managed install replaces older OAuth assumptions and where authorization code grant still matters.
Shopify session tokens in Rails
A Rails-focused guide to Shopify session tokens for embedded apps, covering the Shopify App gem, token exchange, frontend request flow, and the mistakes that still break production auth.