This post wants to explain how to run a Shopify'app outside the cli and the app bridge.
Please pay attention, you won't be able to use anything from the App Bridge API. The installation process is also affected, since you won't be able to install the app without it.
Let's start with the Shopify Remix Template setup, one of the main file is the shopify.server.ts
:
It exports a bunch of utilities, but the one we are interested in is the authenticate
one. It has a bunch of methods to check if a request is from Shopify Admin, Shopify Flow, a Shopify Webhook etc.
You should use it as the first call of each loader
and action
in all your routes:
A great convention you can use is to run your embedded app within the /app layout, in this way you can configure common react contexts to be shared across the app. Also, you may like to have the /webhook route on the first level, not something like /app/webhook
The problem with await authenticate.admin(request)
is that it throws a redirect response to the authPathPrefix
you defined above, so we simply need the call to not throw.
The solution is quite easy, we need to encapsulate the logic for it, returning one thing from the session when we want to run the app outside the app bridge.
I said "one thing from the session" because it is unlikely you are using the prisma Session model to save
your business logic, I bet you have a Shop
model where you save your data, maybe from the afterAuth
hook.
If that's the case excellent, you may have something like the following:
Do you see the point here? We actually do not need the session if we already have the shopifyDomain
(It is the one in the form <name>.myshopify.com
, you can find it in the Session.shop
column).
Let's write now some logic to handle this, we want to run our app with a shopifyDomain
we define, something like:
_10export async function requireShopifyDomain(request: Request) {_10 if(process.env.NODE_ENV === "development" && process.env.RUN_AS_SHOPIFY_DOMAIN) {_10 const shopifyDomain = process.env.RUN_AS_SHOPIFY_DOMAIN;_10 return shopifyDomain;_10 }_10_10 const { session } = await authenticate.admin(request);_10 return session.shop;_10}
Excellent, we can retrieve now the shopifyDomain
we define as an env, just in development to avoid any potential issue while running in production.
Let's write now a simple utility to handle all loaders requests in the same way:
_10export function handleLoaderRequest<T>(_10 request: Request,_10 callback: (shopifyDomain: string) => Promise<T>_10) {_10 const shopifyDomain = await requireShopifyDomain(request);_10 return await callback(shopifyDomain);_10}
With this our loader
gets updated like the following:
You may now ask yourself: "Great, but I have no way to access the GraphQL admin". You are right, it is not possible to access it in this way, but the Shopify GraphQL client is not the only client out here.
A great replacement for it is Genql, to build the client for the shop we only need two things: the shopify domain and the access token.
So let's start with a little model to get the info about the shop:
After this let's set up the GraphQL schema for Genql, simply run the following command:
_10npx genql --endpoint "https://<name>.myshopify.com/admin/api/2024-07/graphql.json" -S --output "app/lib/genql/generated.server" -H "X-Shopify-Access-Token: <access token>" --esm
Replace the <name>.myshopify.com
and <access token>
with some real data (you can also use the data from a test store, the command is just needed to generate the graphql schema).
Let's now create the little utility we will use to generate the graphql client:
That's it, let's glue all together:
Nice, now time for some questions.
What about the billing api?
The authenticate.admin()
call also returns the billing
object, that allows you to handle all aspects of your app's billing,
but it is just a wrapper for some graphql mutations, like appPurchaseOneTimeCreate and
appSubscriptionCreate, you can handle them with Genql as well.
I also use some actions in my app!
The process to handle action
s request is the same, but you just need to check for the method of the request:
_43type PostHandler<T> = {_43 POST: (shopifyDomain: string) => Promise<T>;_43};_43_43type PutHandler<T> = {_43 PUT: (shopifyDomain: string) => Promise<T>;_43};_43_43type PatchHandler<T> = {_43 PATCH: (shopifyDomain: string) => Promise<T>;_43};_43_43type DeleteHandler<T> = {_43 DELETE: (shopifyDomain: string) => Promise<T>;_43};_43_43type RequestHandlerMap<PostResult, PutResult, PatchResult, DeleteResult> =_43 Partial<_43 PostHandler<PostResult> &_43 PutHandler<PutResult> &_43 PatchHandler<PatchResult> &_43 DeleteHandler<DeleteResult>_43 >;_43_43export async function handleActionRequest<_43 PostResult = never,_43 PutResult = never,_43 PatchResult = never,_43 DeleteResult = never,_43>(_43 request: Request,_43 map: RequestHandlerMap<PostResult, PutResult, PatchResult, DeleteResult>,_43) {_43 const requestMethod = request.method as keyof typeof map;_43 const requestHandler = map[requestMethod];_43_43 if (requestHandler) {_43 const shopifyDomain = await requireShopifyDomain(request);_43 return await requestHandler(shopifyDomain);_43 }_43_43 throw methodNotAllowed();_43}
You can update all your action
s like this:
_10export const action = async ({ request }: ActionFunctionArgs) => {_10 return handleActionRequest(request, {_10 POST: async (shopifyDomain) => {_10 // logic for the POST method_10 },_10 DELETE: async (shopifyDomain) => {_10 // logic for the DELETE method_10 },_10 });_10}
Do I need the record of the shop to be in my db?
Yes, since it is not possible to use the app bridge to get the session the only possible point is your db.