Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalid signature with App Proxy for Remix loaders and actions #455

Closed
joelvh opened this issue Sep 27, 2023 · 26 comments
Closed

Invalid signature with App Proxy for Remix loaders and actions #455

joelvh opened this issue Sep 27, 2023 · 26 comments
Assignees

Comments

@joelvh
Copy link
Contributor

joelvh commented Sep 27, 2023

Issue summary

When submitting a form with Remix, it appears that query string parameters are added to the form action without a value (e.g. ?index). These parameters are removed from the request in the loader and action handler when there is no value, but remain when there is a value.

This causes the App Proxy signature check to fail because Shopify signs the index query string parameter without a value, but it is no longer present when validating the signature in the Remix loader or action handler.

We're using the new await shopify.authenticate.public.appProxy(request) feature for signature validation.

  • @shopify/shopify-app-remix 1.2.1
  • @remix-run/react 2.0.0
  • Node version: v18.18.0
  • Operating system: macOS 13.4.1 (Ventura)

The GET log appears after the loader and action handler logs, which is logged by the Shopify dev server.

This is an example of the invalid signature:

# Loader request.url:
http://localhost/?shop=example.myshopify.com&logged_in_customer_id=1234567890&path_prefix=%2Fa%2Fexample&timestamp=1234567890&signature=XXXXXXXXXX

[shopify-app/INFO] Authenticating app proxy request
[shopify-app/INFO] App proxy request has invalid signature

GET /?index=&example.myshopify.com&logged_in_customer_id=1234567890&path_prefix=%2Fa%2Fexample&timestamp=1234567890&signature=XXXXXXXXXX
400 - - 23.330 ms

This is an example of signature validation passing with index=value present (instead of index or index= without a value).

# Loader request.url:
http://localhost/?shop=example.myshopify.com&logged_in_customer_id=1234567890&path_prefix=%2Fa%2Fexample&timestamp=1234567890&signature=XXXXXXXXXX&index=value

[shopify-app/INFO] Authenticating app proxy request

GET /?index=value&example.myshopify.com&logged_in_customer_id=1234567890&path_prefix=%2Fa%2Fexample&timestamp=1234567890&signature=XXXXXXXXXX
200 - - 1454.664 ms

Expected behavior

Expect all query string parameters to be present to validate App Proxy signature.

Actual behavior

Query string parameters are missing in the loader where the App Proxy signature validation occurs.

Steps to reproduce the problem

  1. Add await shopify.authenticate.public.appProxy(request) to the loader in a page in your Remix app.
  2. Access the page through the Shopify App Proxy
  3. Add ?index to the page URL and observe a bad request response (invalid signature)
  4. Add ?index=value to the page URL and observe a successful response (valid signature)
@joelvh joelvh changed the title Invalid signature with App Proxy for Remix actions Invalid signature with App Proxy for Remix loaders and actions Sep 27, 2023
@joelvh
Copy link
Contributor Author

joelvh commented Sep 27, 2023

@byrichardpowell it appears Remix strips index and _data from the request before passing a new Request to the loader or action handler.

@paulomarg
Copy link
Contributor

Hey, thanks for reporting this issue, and thank you for all the details, they make our lives a lot easier :)

We will investigate whether there's anything we can do from this package to solve it, but if this is happening on Remix it's possible we won't be able to. Based on the code comments though, it sounds like this might be fixed with the recent v2 release of Remix?

In the meantime, do you still observe this behaviour if you use a name other than index for the param? That might be a way to work around this issue in the meantime.

@joelvh
Copy link
Contributor Author

joelvh commented Oct 2, 2023

hi @paulomarg,

The index param is actually built-in to Remix. It is a query string parameter added by Remix and then removed. Shopify sees the index param, uses it as part of the signature, and behind the app proxy, Remix will then remove it before the request (with changed query string) is passed to the loader where the signature validation occurs. That's why my example may be confusing.

Regarding the code comments, I was hopeful that v2 would address this, but unfortunately it's still there in v2.

The workaround I was pursuing was to put the original URL in a header in entry.server and then access that in the loader and use that for validation. However, Remix seems to pass a copy of the request to the entry and doesn't persist the headers to the other copy of the request passed to the loader (in my experience).

Don't know how closely the Remix team is working with you on these Shopify projects, but maybe @ryanflorence or someone on his team have an idea about this.

I realize the app proxy behavior is new in this package, but presumably this affects everyone. Is anyone else having this issue?

Thanks!

@paulomarg
Copy link
Contributor

Hey, sorry for the delayed response! Thank you for the detailed write-up, this is super helpful. Unfortunately, I think you're right, and I can't see a way to work around this right now.

It sounds like this would only be a problem if the page needs to interact with the server after the initial load, since that's when Remix will set those search params, so it might not affect everyone, but it's still significant.

Just in case it helps, it might be possible to work around this if you use the liquid helper to return a template with your page, since Shopify will render that content directly - so you could potentially create a form in that page that points to the app proxy endpoint, but we'd be losing all of the benefits of using Remix.

I'll bring this to the Remix team to see if we can work out a better solution.

Thanks!

@joelvh
Copy link
Contributor Author

joelvh commented Oct 24, 2023

OK thanks @paulomarg. Will keep an eye out for any changes in Remix.

Copy link
Contributor

This issue is stale because it has been open for 90 days with no activity. It will be closed if no further action occurs in 14 days.

@github-actions github-actions bot added the Stale label Dec 24, 2023
@vincaslt
Copy link

vincaslt commented Jan 5, 2024

I'm encountering the same issue.

I managed to work around it by creating a custom server.ts file. Since I'm using cloudflare, I copied the adapter code and set the header with original URL just before this line : https://github.com/remix-run/remix/blob/main/packages/remix-cloudflare-pages/worker.ts#L33

context.request.headers.set("X-Original-Url", context.request.url);

In server.ts I use my custom adapter:

import { createPagesFunctionHandler } from "./cloudflare-adapter";

export const onRequest = createPagesFunctionHandler({
  build,
  getLoadContext: (context) => ({ env: context.env }),
  mode: build.mode,
});

Then in loader I restore the URL for authentication:

const originalUrl = request.headers.get("X-Original-Url")!;
const originalRequest = new Request(originalUrl, request);
await shopify.authenticate.public.appProxy(originalRequest);

@github-actions github-actions bot removed the Stale label Jan 6, 2024
@RajanTpss
Copy link

Hello Folks, I hope you are doing well.

Have this issue is being resolved?

I am also facing the same problem. Actions in remix are not validating signature but loader are. Showing "App proxy request has invalid signature" error message in terminal.

Thanks

@ConnorIngold
Copy link

Hi Guys 👋 ,

Any updates on this? I'm experiencing the same issue regardless of whether it's a loader or action.

backend

import { json } from '@remix-run/node'
import type { LoaderFunction } from '@remix-run/node'

import { authenticate } from '../shopify.server'

export const loader: LoaderFunction = async ({ request }) => {
  console.log("🚀 ~ constloader:LoaderFunction= ~ request:", request)
  try {
    const { admin } = await authenticate.public.appProxy(request)

    return await admin
      ?.graphql(
        `#graphql
      query Products {
          products(first: 3) {
            edges {
              node {
                id
                title
              }
            }
          }
        }`
      )
      .then(async (response) => response.json())
      .then((data) => json({ data }, { status: 200 }))
      .catch((error) => {
        console.error((error as Error).message) // Log the error for server-side debugging
        return json(
          { error: 'Failed to fetch locations', err: error },
          { status: 500 },
        )
      })
  } catch (error) {
    console.error((error as Error).message) // Log the error for server-side debugging
    return json(
      { error: 'Failed to fetch locations', err: error },
      { status: 500 },
    )
  }
}

Frontend:

  fetch('/apps/proxy/location-action', {
    method: 'GET', // or 'POST' if you are sending data to the server
    headers: {
      'Content-Type': 'application/json',
    },
  })
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => {
    console.log(data); // Process and display the data on your page
  })
  .catch(error => console.error('There has been a problem with your fetch operation:', error.message));

@paulomarg
Copy link
Contributor

paulomarg commented Mar 25, 2024

Hey folks! We've just changed how app proxy authentication works to compensate for the _data and index fields, added some new React components to make it easier to use JS, forms and links when using non-liquid proxied pages.

The only caveat with this solution is that proxied route pathnames must match the value set in the dashboard (i.e. /apps/proxy needs to be handled behind an apps.proxy._index route), and you must add a / to the end of the URL when opening it in the browser (e.g. /apps/proxy/).

Both of these restrictions happen because Remix doesn't support URL rewriting yet, so the paths need to match. We're working with the Remix team on removing the need for the trailing slash, but if that happens it won't require any changes on apps that are using these new components.

This will be included in the next release of the package, and hopefully it'll make the experience of working with proxies better. And please do let us know if you still run into issues with these.

@paulomarg paulomarg self-assigned this Mar 25, 2024
@charlesr1971
Copy link

charlesr1971 commented Mar 28, 2024

Hi @paulomarg

I have a POST request from a Theme Extension App to a Remix App, like:

App Proxy Config:

Subpath prefix: apps
Subpath: my-app-proxy
Proxy URL: https://sb-user-admin-app-v1.fly.dev/app

I think that the form submission URL, transfers the data to the app proxy URL, like this:

https://sandbanks-test.myshopify.com/apps/my-app-proxy/api

-> 

https://sb-user-admin-app-v1.fly.dev/app/api

extensions/sb-user-extension-app-v2/assets/extension.js

const url = 'https://sandbanks-test.myshopify.com/apps/my-app-proxy/api';

const headers = {
  'Content-Type': 'application/json'
};

const body = {
  forename: forename.value,
  surname: surname.value,
  email: email.value
}

const options = {
  method: 'POST',
  headers,
  body: JSON.stringify(body),
};

const response = await fetch(url, options);
const data = response.json();

app/routes/app.api.jsx:

export async function action({ request, params }) {

  const { admin } = await authenticate.public.appProxy(request);
  ...

}

And in my fly.io live logs, I see:

2024-03-28T21:30:40.497 app[2875691f3273d8] ams [info] [shopify-app/INFO] Authenticating app proxy request
2024-03-28T21:30:40.514 app[2875691f3273d8] ams [info] [shopify-app/INFO] App proxy request has invalid signature
2024-03-28T21:30:40.515 app[2875691f3273d8] ams [info] POST /app/api?shop=sandbanks-test.myshopify.com&logged_in_customer_id={CUSTOMER ID]&path_prefix=%2Fapps%2Fmy-app-proxy&timestamp=1711661440&signature=[SIGNATURE] 400 - - 23.432 ms 

Now, the interesting thing is, that the authentication works, when I run this in development with my ngrok tunnel URL:

https://alien-amazed-lab.ngrok-free.app

But, with my production fly.io URL:

https://sb-user-admin-app-v1.fly.dev

The authentication fails, although the rest of the method's business logic succeeds [apart from the graphql part], if I remove:

const { admin } = await authenticate.public.appProxy(request);

But, then I cannot expose admin for my graphql query, so this is not an acceptable work around.

I am not sure whether my problem is caused by the stuff, you've been talking about, in this thread.
If I'm being totally honest, I don't fully understand what this thread, is all about.
I’ve never actually seen an index param, in my logs, so I’m fairly sure that the reason for my invalid signature, might be different?
I don't actually pass any custom params, because I am posting data in the body of the request.
This is why I am using the Remix action() [for form POST verbs] method, and not the Remix loader() [for form GET verbs] method, in the route.

Can you suggest a solution, to allow the authentication to work, properly?

For instance, is there anyway I can import the admin object, without passing in the request object? In other words, by-pass authentication and grab the admin object, which I know is probably frowned upon, but I need to get this working somehow?

If you want to see more detail in my fly.io log, I would be happy to send you the file.
I cannot show it on this thread, because it contains my Client ID etc

Thanks, in advance...

@charlesr1971
Copy link

charlesr1971 commented Mar 31, 2024

@vincaslt
Hi 😊

I would be interested in trying out your solution.
I am using Remix.
When you say you made a custom server, was this in addition to the default:

shopify.server.js

Could you provide some more code, so that I can get a better understanding of what you are doing?

I am using ngrok in development, as my tunnel, and fly.io, in production?

In development, the app proxy works perfectly, but it fails with:

App proxy request has invalid signature

In production?

I have looked at the search string, from the request request.parsedURL.search and it seems like, the signature is different to the one that I can create, using the following utility function:

export function computeSignature(querystringFromClient, shopifyAppSecret) {
  const querystring = require('node:querystring');
  const crypto = require('node:crypto');
  const formattedQueryString = querystringFromClient.replace("/?", "").replace(/&signature=[^&]*/, "").split("&").map(x => querystring.unescape(x)).sort().join("");
  // @ts-ignore
  const computedSignature = crypto.createHmac('sha256', shopifyAppSecret).update(formattedQueryString).digest('hex');
  return computedSignature;
}

I am posting the data from a form, in my Theme App Extension, to my Remix app via an App Proxy. I am using POST, so my request hits the Remix action() method, and not the loader() method.

The only weird thing that I have noticed is that some of the references to the app proxy URL, in the request data, have http and not https, as the protocol?

Here is the request object:

request: {
	method: 'POST',
	redirect: 'follow',
	headers: {
		accept: '*/*',
		'accept-encoding': 'gzip',
		'accept-language': 'en',
		'cdn-loop': 'cloudflare',
		'cf-connecting-ip': '2a00:23c5:fd83:d501:b437:9f77:a7d6:e778',
		'cf-ipcountry': 'GB',
		'cf-ray': '86c347e4feac63fa-LHR',
		'cf-visitor': '{"scheme":"https"}',
		'content-length': '273',
		'content-type': 'application/json',
		'edge-bot-ja3hash': '2a596c42bf3e2e4f4b32f98b197480c3',
		'edge-bot-score': '89',
		'edge-client-bot': 'false',
		'edge-client-src-port': '55805',
		'edge-colo-code': 'LHR',
		'edge-ip': '2620:127:f00f:e::',
		'edge-zone-name': 'myshopify.com',
		'fly-client-ip': '35.242.153.81',
		'fly-forwarded-port': '443',
		'fly-forwarded-proto': 'https',
		'fly-forwarded-ssl': 'on',
		'fly-region': 'lhr',
		'fly-request-id': '01HT654AXKTJRSZV5SG4B4SSE6-lhr',
		'fly-traceparent': '00-f1ba9549259109be1ff5a79248259076-2f2b48fe97d05386-00',
		host: 'sb-user-admin-app-v1.fly.dev',
		origin: 'https://sandbanks-test.myshopify.com',
		priority: 'u=4',
		referer: 'https://sandbanks-test.myshopify.com/pages/upload-driving-license-2',
		'sec-fetch-dest': 'empty',
		'sec-fetch-mode': 'cors',
		'sec-fetch-site': 'same-origin',
		'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
		via: '1.0 fly.io',
		'x-downstream-dc': 'gcp-europe-west2,gcp-europe-west2',
		'x-force-upstream': 'app_proxy_pool',
		'x-forwarded-for': '2a00:23c5:fd83:d501:b437:9f77:a7d6:e778, 35.242.153.81, 66.241.124.82',
		'x-forwarded-host': 'sandbanks-test.myshopify.com',
		'x-forwarded-port': '443',
		'x-forwarded-proto': 'https',
		'x-forwarded-ssl': 'on',
		'x-ip-metadata': 'asn:2856;anon:0;anonVpn:0;hostProvider:0;pubProxy:0;torExit:0;countryCode:GB;regionCode:ENG;regionNameBase64:RW5nbGFuZA==;subRegionCode:SRY;subRegionNameBase64:U3VycmV5;cityNameBase64:RG9ya2luZw==;postalCode:RH4;timeZone:Europe/London;lat:51.23440;long:-0.33360;ssbipAnon:0;ssbipAnonVpn:0;ssbipHostProvider:0;ssbipPubProxy:0;ssbipTorExit:0',
		'x-request-id': 'a0950a64-e449-415c-ada3-be03dbdd353a-1711751179',
		'x-request-start': 't=1711751179187950',
		'x-shop-domain': 'sandbanks-test.myshopify.com',
		'x-shopify-client-ip': '2a00:23c5:fd83:d501:b437:9f77:a7d6:e778',
		'x-shopify-client-ip-sig': '1711749600-o0rJwUwY2uQX9XsEUA76qCjmlO+zNmE0uRNp3BJn2c4=',
		'x-shopify-request-timing': 'cf;t=1711751179.032'
	},
	credentials: 'same-origin',
	parsedURL: URL {
		href: 'http://sb-user-admin-app-v1.fly.dev/app/api?shop=sandbanks-test.myshopify.com&logged_in_customer_id=[CUSTOMER_ID]&path_prefix=%2Fapps%2Fmy-app-proxy&timestamp=1711751179&signature=9d9aa16024f925662772683996df04e00e358e10217486bc3cc04cb9705254ca',
		origin: 'http://sb-user-admin-app-v1.fly.dev',
		protocol: 'http:',
		username: '',
		password: '',
		host: 'sb-user-admin-app-v1.fly.dev',
		hostname: 'sb-user-admin-app-v1.fly.dev',
		port: '',
		pathname: '/app/api',
		search: '?shop=sandbanks-test.myshopify.com&logged_in_customer_id=[CUSTOMER_ID]&path_prefix=%2Fapps%2Fmy-app-proxy&timestamp=1711751179&signature=9d9aa16024f925662772683996df04e00e358e10217486bc3cc04cb9705254ca',
		searchParams: URLSearchParams {
			'shop' => 'sandbanks-test.myshopify.com',
			'logged_in_customer_id' => '[CUSTOMER_ID]',
			'path_prefix' => '/apps/my-app-proxy',
			'timestamp' => '1711751179',
			'signature' => '9d9aa16024f925662772683996df04e00e358e10217486bc3cc04cb9705254ca'
		},
		hash: ''
	}
	signal: AbortSignal {}
}

Notice how the request.parsedURL.href and request.parsedURL.origin are using the http protocol

UPDATE: 03.04.2024

I managed to resolve the invalid signature issue, after a deep dive into:

https://github.com/Shopify/shopify-api-js/tree/main

I would be happy to share my solution with anyone, who can’t resolve this. Please reply to this post with my username @ reference and I will get back to you.

Essentially, I think my signature was corrupted due to a timestamp discrepancy, between the Shopify & the fly.io server.

Everything is now working well, in both development & production apps. 🙂

@paulomarg
Copy link
Contributor

Good to hear is working @charlesr1971! Sorry for the delay in responding, but yes, since the requests are signed with a timestamp, that can be a problem. It's good to know that can happen so we can advise folks that run into this kind of thing in the future.

Thanks for sharing your solution!

@charlesr1971
Copy link

@paulomarg To be honest, I am not sure whether my fix, is a particularly secure one, as it is difficult to know whether my signature has been tampered with or whether there is a timestamp discrepancy?

But, I had to resolve this somehow.

It would be better to use query params that are not so exposed to discrepancies between the Shopify and remote hosting server.
With such a huge variety of remote hosting solutions, there is a good chance that the timestamp might be different. I am using fly.io and I have used the timezone Europe/London in the fly.toml file, but I have noticed that the fly.io logs are always an hour behind?

It might be better to rethink whether using a timestamp is such a good idea?

@patrick-schneider-latori

@paulomarg How can I get #710 installed in my current Remix project? Installing the latest @shopify/shopify-app-remix does not seem to include the new component.

@paulomarg
Copy link
Contributor

Hey, good callout - it'll be included in the next release, which we're due for!

@paulomarg
Copy link
Contributor

I'm going to close this in favour of #436 since we have two discussions going in different issues, and the latest is there.

@iamgerardm
Copy link

@charlesr1971

I am running into what seems to be similar behavior. When I first publish to Fly.io and immediately test it -- all is well but after 10 minutes or so it's like it goes stale and fails. The only way I have found to get it to work again is to click the app inside the Shopify admin. Then all permissions work as expected for a bit.

Can you share what you did please?

@charlesr1971
Copy link

@iamgerardm OK. My solution will only work if you get an invalid signature message in your fly.io logs.
Please confirm that you are getting this error and I will share my solution. 🙂

@iamgerardm
Copy link

@charlesr1971 Hmm, no. My request to the proxy comes from an App Block and when I use authenticate.public.appProxy(request); to authenticate it session is undefined. Then if I go into the Shopify Store and click the App from the sidebar the fetch requests authenticate correctly and start working again for a few minutes.

I thought maybe it was something to do with the initial fetch headers in particular the redirect parameter but I have tried both manual and follow.

Even if your solution is not the exact solve for me I would love to see it to maybe get some ideas as to possible alternative solutions.

@charlesr1971
Copy link

charlesr1971 commented Apr 21, 2024

@iamgerardm I don’t think my solution will work, for you, but here it is.

Basically, I had to recalculate the signature.
But you have to use Shopify’s createSHA256HMAC(), to do the recalculation. Other signature recalculation methods do not work.

Please note that I am making my request from a Theme App Extension.
It is a fetch() POST request, which is why it is picked up by the Remix App action() method and not the loader() method.

And this issue only occurred in my fly.io production app, and not when used in development, using App Bridge and my NGROK tunnel.

Also, note this is a précis version of my code, so I have removed unnecessary lines & variables etc. I hope it still works, but you should get an idea of what I’m trying to do. 🙂

// app.api.jsx

import { computeSignature } from "./utils";

export async function action({ request, params }) {

  // auth logic

  let admin;
  let session;
  let env = 'dev';

  try{
    const $admin = await authenticate.public.appProxy(request);
    admin = $admin.admin;
    session = $admin.session;
  }
  catch(error){
    env = 'prod';
  }

  const formData = await request.json();

  const sym = Object.getOwnPropertySymbols(request).find(
    (s) => s.description === 'Request internals'
  );

  let href = '';

  if(request[sym] && 'parsedURL' in request[sym] && 'href' in request[sym].parsedURL){
    href = request[sym].parsedURL.href;
  }

  if(request[sym] && 'parsedURL' in request[sym] && 'search' in request[sym].parsedURL){
    const querystringFromClient = request[sym].parsedURL.search;
    const signatureComputed = await computeSignature(querystringFromClient, process.env.SHOPIFY_API_SECRET);
    if(href !== '' && signatureComputed !== ''){
      const href$ = href.replace(/(.*?)(&signature=.*)/igm,'$1&signature=' + signatureComputed);
      // I use the variables above, later on, which is why I set this conditional, so late
      if(env === 'prod'){
        if(href$ !== ''){
          try{
            const originalRequest = new Request(href$, request);
            const $admin = await authenticate.public.appProxy(originalRequest);
            admin = $admin.admin;
            session = $admin.session;
          }
          catch(error){}
        }
      }
    }
  }
	
  // business logic
  ...
	
}


// utils.jsx

import { HashFormat, createSHA256HMAC } from "@shopify/shopify-api/runtime";

const queryStringToObject = url => Object.fromEntries([...new URLSearchParams(url.split('?')[1])]);

function stringifyQueryForAppProxy(query) {
  return Object.entries(query)
    .sort(([val1], [val2]) => val1.localeCompare(val2))
    .reduce((acc, [key, value]) => {
      return `${acc}${key}=${Array.isArray(value) ? value.join(',') : value}`;
    }, '');
}

function createAuthQuery(querystring) {
  const querystringFromClient = querystring.replace('&timestamp','&stamp');
  let queryStringObj = queryStringToObject(querystringFromClient);
  const { signature, ...query } = queryStringObj;
  if('stamp' in query){
    query.timestamp = query.stamp;
    delete query.stamp;
    queryStringObj = {
      ...query,
      signature
    }
  }
  return queryStringObj;
}


export async function computeSignature(querystringFromClient, shopifyAppSecret) {
  // a bug in URLSearchParams, doesn't escape certain HTML entities properly
  const queryStringObj = createAuthQuery(querystringFromClient);
  const { signature, ...query } = queryStringObj;
  const res = await createSHA256HMAC(shopifyAppSecret, stringifyQueryForAppProxy(query), HashFormat.Hex);
  return res;
}

@patrick-schneider-latori

Wow this is so mad that this has to be done by end users like @charlesr1971 ... 😱

Thanks for sharing your solution!

@charlesr1971
Copy link

@patrick-schneider-latori Yes. It’s crazy, but I think sometimes the timestamp between the Shopify server & the remote server, give different values, due to localisation divergence, which causes the signature to fail?

@iamgerardm
Copy link

@charlesr1971 Thanks for the snippets. In the end I determined it was a data persistence issue. The app is using the out of the box sqlite db and needed to add volumes on Fly.IO to maintain the offline session token when the servers went to sleep/restarted.

@charlesr1971
Copy link

charlesr1971 commented Apr 22, 2024

@iamgerardm

Yes. Absolutely.
On fly.io you need to add a volume, if you are using SQLite and you want the DB values to persist across sessions.

I will add my fly.io config files, so that others can use these settings, if they are suffering similar issues, in the future.

fly.toml

# fly.toml app configuration file generated for sb-user-admin-app-v1 on 2024-03-22T11:44:39Z
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'sb-user-admin-app-v1'
primary_region = 'ams'

[build]

[env]
  PORT = "8081"
  SHOPIFY_APP_URL = "https://sb-user-admin-app-v1.fly.dev"
  SHOPIFY_API_KEY = "[SHOPIFY_API_KEY]"
  SCOPES = "read_checkouts,read_content,read_customers,read_files,read_orders,read_products,write_checkouts,write_customers,write_files,write_orders,write_products,write_themes,write_content"
  DATABASE_URL = "file:/data/dev.sqlite"
  TZ="Europe/London"

[experimental]
  cmd = "start_with_migrations.sh"
  entrypoint = "sh"

[http_service]
  internal_port = 8081
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[mounts]
  source = "sb_user_admin_app_v1_data"
  destination = "/data"

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

start_with_migrations.sh

#!/bin/sh

set -ex
npx prisma migrate deploy
npm run start

Notes:

Error, running start_with_migrations.sh:

The following problem, is only really an issue for Windows users.

If you have an issue getting the sh script to run, on fly.io, make sure that you don’t have a:

CRLF

At the end of each line.
You can display new line characters, using Notepad++:

View > Show Symbol > Show All Characters

Here is a solution, on how to resolve this issue:

https://community.fly.io/t/sqlite-not-getting-setup-properly/4386/11

There should only be an LF character, at the end of each line.

@iamgerardm
Copy link

@charlesr1971 Thanks - this is pretty similar to what I ended up doing. I was talking about this topic on another thread as well and I believe in the future there may be a note/tip of this within the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants