Deploy the open-source Spraay Shopify app — batch USDC payouts on Base — for your store in ~15 minutes. Walks through the custom app setup, Railway deploy, env vars, and Supabase database, plus fixes for 502s and OAuth errors. Zero Shopify scopes, non-custodial, self-hosted, MIT licensed.
--- name: spraay-shopify-selfhost description: Deploy and self-host the open-source Spraay Shopify app (batch USDC payouts on Base) for a merchant's store. Use this skill whenever a user wants to install, deploy, set up, self-host, or troubleshoot the Spraay Shopify app (github.com/plagtech/spraay-shopify) - including creating the custom app in the Shopify Dev Dashboard, deploying to Railway, configuring environment variables, setting up the Supabase/Postgres database, fixing 502 errors or OAuth redirect issues, or connecting a custom domain. Also use when a Shopify merchant asks how to add crypto payouts, USDC payments, or affiliate crypto payments to their store admin. --- # Self-Host the Spraay Shopify App Walk a merchant from zero to a working batch-USDC-payouts page inside their Shopify admin in ~15 minutes. The app is open source (MIT), non-custodial, and requests **zero Shopify scopes** — it cannot read or write any store data. **Repo:** https://github.com/plagtech/spraay-shopify ## Stack at a glance - Remix + `@shopify/shopify-app-remix`, Vite, Polaris UI (fully embedded in admin) - wagmi v3 + viem v2 — MetaMask and Coinbase Smart Wallet - Base (chain 8453), USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` - Batch contract `0x1646452F98E36A3c9Cfc3eDD8868221E207B5eEC` (verified; never substitute another address) - Supabase PostgreSQL via Prisma, isolated in a `shopify` schema - Railway hosting (Dockerfile, node:20-alpine) ## Prerequisites checklist Confirm the user has (or help them create) each of these before starting: 1. A Shopify store (dev store is fine for testing) 2. A free Shopify Partners account → https://dev.shopify.com 3. A Railway account (hobby tier is enough) 4. A Supabase project (free tier is enough) 5. A wallet holding USDC on Base (MetaMask or Coinbase Smart Wallet) ## Step 1 — Fork and deploy to Railway 1. Fork `plagtech/spraay-shopify` (Node 20+ required if running outside Docker: `>=20.19 <22 || >=22.12`). 2. Easiest path: the **Deploy on Railway** button in the README — the template pre-prompts for every required env var, with PORT defaulted to 3000. Manual path: Railway → New Project → Deploy from GitHub repo → select the fork; the repo ships a `railway.json` and Dockerfile, so the build is automatic. 3. Generate a public domain for the service (Settings → Networking). Note the URL, e.g. `your-app.up.railway.app`. The app will crash-loop until env vars are set — that's expected. Continue. ## Step 2 — Create the custom app in Shopify 1. Go to https://dev.shopify.com → Apps → **Create app** → **Custom distribution** (single merchant). Custom distribution is Shopify's intended path for merchant-specific tooling — no App Store review involved. 2. Set **App URL** to the Railway URL from Step 1 (https, no trailing slash). 3. Add all three OAuth redirect URLs (each prefixed by the App URL): - `https://your-domain/auth/callback` - `https://your-domain/auth/shopify/callback` - `https://your-domain/api/auth/callback` 4. Copy the **Client ID** and **Client Secret**. ## Step 3 — Environment variables Set these in Railway → service → Variables. The repo's `.env.example` documents each one; the authoritative reference: | Variable | Value | Notes | |---|---|---| | `SHOPIFY_API_KEY` | App Client ID | Public — ships in the browser bundle; safe to share | | `SHOPIFY_API_SECRET` | App Client Secret | Secret — signs webhook HMACs and OAuth. Never commit or paste into chat logs | | `SHOPIFY_APP_URL` | `https://your-app.up.railway.app` | Must exactly match the App URL in Shopify settings; no trailing slash | | `SCOPES` | *(empty)* | Intentional. The app never touches store data. Leave blank unless the fork adds Shopify API usage — then keep it in sync with `shopify.app.toml` | | `DATABASE_URL` | `postgresql://user:pass@host:6543/postgres?schema=shopify&pgbouncer=true` | Supabase **pooled** connection (transaction pooler, port 6543) — used at runtime | | `DIRECT_URL` | `postgresql://user:pass@host:5432/postgres?schema=shopify` | Supabase **direct** connection (port 5432) — used by `prisma migrate` | | `NODE_ENV` | `production` | | | `PORT` | `3000` | **Load-bearing.** See gotcha below | | `SHOP_CUSTOM_DOMAIN` | *(optional)* | Only if the dev store uses a custom domain | **The PORT=3000 gotcha (most common failure):** the Dockerfile's `EXPOSE 3000` sets Railway's domain target port to 3000. If `PORT` is unset, Railway injects `PORT=8080`, the app binds :8080, the proxy still routes to :3000, and **every route returns 502**. If the user reports 502s on all routes, check this first. **Database notes:** keep `?schema=shopify` on **both** URLs so the app's tables stay isolated from anything else in the Supabase project. Both URLs point at the same database — only the port and pooling differ. After saving variables, redeploy. Migrations run automatically on boot (`prisma generate && prisma migrate deploy`, then `remix-serve`). A clean boot — migrate succeeds and `remix-serve` prints its listen URL — means Steps 1–3 are done. **Diagnostic tip:** a 502 while Railway shows the deployment "Online" is a port/proxy mismatch, not a crash. If the logs show a successful migrate and a listening server, fix `PORT`, don't debug the app. ## Step 4 — Install and test 1. From the Dev Dashboard, install the app on the store. Complete the OAuth prompt. 2. Open the app from the Shopify admin sidebar — the Polaris UI should render embedded. 3. Run a small real test: CSV with 1–2 recipients and small amounts. Header row is optional (first two columns are assumed address, amount); optional extra columns `name`, `email`, `memo` are supported, and the payout screen has a **Download CSV template** link and **Load sample data** button. Example: ```csv wallet_address,amount 0xYourOtherWallet...,1.00 ``` 4. Review screen shows recipients, total, and the 0.3% protocol fee → connect wallet → approve USDC → execute → confirm the BaseScan link resolves. ## Optional — local development For hacking on the app itself (not needed for a production deploy): `npm install && npm run dev`. The Shopify CLI creates a tunnel and injects `SHOPIFY_API_KEY`, `SHOPIFY_API_SECRET`, and `SHOPIFY_APP_URL`, so local `.env` only needs `DATABASE_URL` and `DIRECT_URL`. ## Optional — custom domain Point a subdomain (e.g. `payouts.yourstore.com`) at the Railway service via CNAME, wait for the SSL cert to issue, then update `SHOPIFY_APP_URL` **and** the App URL + redirect URLs in Shopify settings to match. All three must agree or OAuth breaks. ## Troubleshooting | Symptom | Likely cause | Fix | |---|---|---| | 502 on every route | Railway injected PORT=8080 | Pin `PORT=3000` | | OAuth redirect error / "invalid redirect_uri" | App URL mismatch | `SHOPIFY_APP_URL`, Shopify App URL, and redirect URL prefixes must be identical (scheme, host, no trailing slash) | | Prisma migrate fails | Using pooled URL for migrations | Migrations need `DIRECT_URL` (port 5432); runtime uses `DATABASE_URL` (port 6543) | | App loads outside admin but not embedded | Wrong App URL or missing HTTPS | Embedded apps require the exact HTTPS App URL configured in Shopify | | Wallet won't connect | Wrong network | The app targets Base (8453); switch network in the wallet | ## Security posture (tell the user) - **Zero scopes:** the app requests no Shopify permissions — it structurally cannot access orders, customers, or products. - **Non-custodial:** USDC moves only when the merchant's own wallet signs, in their browser. The server never holds keys or funds. - **Self-hosted:** credentials, database, and infrastructure belong to the merchant. - The 0.3% protocol fee is itemized on the review screen and collected on-chain by the batch contract. It is contract-enforced on Base, not app code — self-hosting or forking does not remove it, and there is no subscription or plan. - Security issues → report privately to support@spraay.app, not a public issue. ## Related - Running payouts after deployment → `shopify-batch-payouts` skill - Protocol docs → https://docs.spraay.app
don't have the plugin yet? install it then click "run inline in claude" again.
separated intent, inputs, procedure, decision points, output contract, and outcome signal into explicit sections; added edge cases (port mismatch, pool exhaustion, hung migrations, network switching); clarified external connections with setup guidance (Supabase direct vs. pooled URLs, Railway GitHub setup, Shopify Partners OAuth scopes); documented env var load-bearing behaviors and troubleshooting matrix; preserved original author's deployment flow and stack details.
get a merchant from zero to a working batch-USDC-payouts page inside their Shopify admin in roughly 15 minutes. the app is open source (MIT), non-custodial, requests zero Shopify scopes (cannot read or write store data), and runs entirely on the merchant's own infrastructure. use this skill when a merchant wants to install, deploy, self-host, troubleshoot, or set up the Spraay Shopify app, including custom app creation in Shopify Dev Dashboard, Railway deployment, environment variable configuration, Supabase/Postgres database setup, fixing 502 errors or OAuth redirect issues, connecting a custom domain, or adding crypto payouts to their store admin.
Repo: https://github.com/plagtech/spraay-shopify
external connections:
environment variables required by the app:
SHOPIFY_API_KEY , public Client ID from custom app (safe to ship in browser)SHOPIFY_API_SECRET , private Client Secret from custom app (never commit or share)SHOPIFY_APP_URL , HTTPS URL of Railway service, no trailing slashDATABASE_URL , Supabase pooled connection string (transaction pooler, port 6543)DIRECT_URL , Supabase direct connection string (port 5432, used by Prisma migrations)NODE_ENV , set to productionPORT , set to 3000 (load-bearing; see decision points)SCOPES , leave empty (intentional; app requests no Shopify permissions)SHOP_CUSTOM_DOMAIN , optional, only if store uses a custom domaintech stack context:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA029130x1646452F98E36A3c9Cfc3eDD8868221E207B5eEC (verified on BaseScan; never use another address)shopify schemaprerequisites the merchant must have before starting:
step 1: fork and deploy to railway
inputs: github account with access to plagtech/spraay-shopify, Railway account, Node 20+ for local work (optional)
your-app.up.railway.app). this is your SHOPIFY_APP_URL value.outputs: Railway service running (status red/crashing until env vars added), public HTTPS domain recorded
step 2: create the custom app in Shopify
inputs: Shopify Partners account logged in, Railway URL from step 1
https://your-app.up.railway.app).https://your-domain/auth/callbackhttps://your-domain/auth/shopify/callbackhttps://your-domain/api/auth/callbackoutputs: custom app created in Shopify Dev Dashboard, Client ID and Client Secret recorded
step 3: configure environment variables in Railway
inputs: Shopify Client ID and Client Secret from step 2, Railway domain from step 1, Supabase connection strings
DATABASE_URL and the direct connection string (port 5432) for DIRECT_URL. both must include ?schema=shopify to isolate tables.SHOPIFY_API_KEY = Client ID from step 2SHOPIFY_API_SECRET = Client Secret from step 2SHOPIFY_APP_URL = https://your-app.up.railway.app (must exactly match App URL in Shopify, no trailing slash)DATABASE_URL = Supabase pooled URL with ?schema=shopify appendedDIRECT_URL = Supabase direct URL with ?schema=shopify appendedNODE_ENV = productionPORT = 3000SCOPES = leave blank (empty string, not the word "empty")SHOP_CUSTOM_DOMAIN = leave blank unless the dev store uses a custom domain (then set to the domain)prisma migrate deploy), then the Remix server print its listen URL (e.g. remix-serve listening on port 3000). if both complete and the deployment status turns green, steps 1-3 are done.outputs: all env vars set in Railway, deployment redeployed, migrations completed, app listening on port 3000
step 4: install the app on the store and run a test payout
inputs: custom app created in Shopify, app deployed and listening on Railway
go to https://dev.shopify.com, select the Dev Dashboard, find the custom app created in step 2, and click "Install app". complete the OAuth consent flow (approve the app to run in your store admin). you will be redirected to your store admin.
in the Shopify admin sidebar, you should see the app listed. click it. the Polaris UI should render embedded in the admin frame (not a new tab). if it does not render, check "decision points" section below.
prepare a test CSV with 1-2 recipients and small amounts. header row is optional; first two columns are wallet address and amount. optional extra columns: name, email, memo. example:
wallet_address,amount
0xYourOtherWallet...,1.00
the app provides a "Download CSV template" link and "Load sample data" button if you want to skip manual entry.
paste or upload your CSV, review the screen (shows recipients, total USDC, 0.3% protocol fee), connect your wallet (MetaMask or Coinbase Smart Wallet), approve the USDC spending on-chain, and execute the payout. the app returns a BaseScan link to the batch transaction. verify it resolved.
outputs: app installed in store admin, test payout executed and confirmed on BaseScan
step 5 (optional): set up a custom domain
inputs: a subdomain you control (e.g. payouts.yourstore.com), Railway service configured in step 3
your-app.up.railway.app).SHOPIFY_APP_URL in Railway env vars to the custom domain (https, no trailing slash).outputs: custom domain pointing to app, SSL cert issued, app running on custom domain, Shopify settings updated to match
if the merchant reports a 502 error on every route while Railway shows the deployment "Online":
PORT is set to 3000 in Railway env vars. if PORT is unset, Railway injects PORT=8080, the app binds :8080, the proxy routes to :3000, and all routes return 502. set PORT=3000 and redeploy. if logs show a successful migrate and a listening server but routes still 502, the issue is a port/proxy mismatch, not an app crash.if the merchant gets an "invalid redirect_uri" or OAuth redirect error during install:
SHOPIFY_APP_URL, the App URL in Shopify settings, and all three redirect URLs in Shopify settings are identical (scheme, host, no trailing slash). any mismatch breaks OAuth. ensure all three redirect URLs are prefixed by the exact App URL value.if Prisma migrate fails during deployment:
DIRECT_URL is set and points to the direct connection (port 5432, not the pooled port 6543). Prisma migrations require the direct connection; the app uses the pooled DATABASE_URL at runtime.if the app loads in a new tab but not embedded in the Shopify admin:
SHOPIFY_APP_URL in Railway env vars and uses HTTPS (no http). embedded apps require the exact HTTPS URL; any mismatch breaks embedding.if the merchant's wallet will not connect or shows "wrong network":
if database pool connections are exhausted (too many concurrent requests):
if the deployment hangs during prisma migrate deploy:
on successful deployment, the skill delivers:
shopify schema created in Supabase with tables for app state0x1646452F98E36A3c9Cfc3eDD8868221E207B5eECSHOPIFY_APP_URL, Shopify App URL, and redirect URLs matching exactly (no trailing slash, HTTPS)PORT=3000, NODE_ENV=production, SCOPES empty, DATABASE_URL pooled, DIRECT_URL direct, all set in Railwaythe skill is complete when the merchant can open the app from their admin, paste a CSV, and execute a payout in fewer than 15 minutes total.
the merchant knows the skill worked when:
0x1646452F98E36A3c9Cfc3eDD8868221E207B5eECif any of these steps fail, refer to the decision points and troubleshooting table below.
| symptom | likely cause | fix |
|---|---|---|
| 502 on every route, deployment shows "Online" | PORT not set to 3000 (Railway injects PORT=8080) |
pin PORT=3000 in env vars, redeploy |
| OAuth redirect error "invalid redirect_uri" | App URL mismatch between Railway, Shopify settings, and redirect URLs | ensure SHOPIFY_APP_URL, Shopify App URL, and all three redirect URLs are identical (scheme, host, no trailing slash) |
| Prisma migrate fails during boot | migrations using pooled DATABASE_URL instead of direct |
set DIRECT_URL (port 5432) for migrations; DATABASE_URL (port 6543) for runtime |
| app loads in new tab, not embedded in admin | wrong App URL or missing HTTPS | embedded apps require exact HTTPS App URL matching Shopify settings |
| wallet won't connect or "wrong network" error | wallet not on Base chain | switch wallet to Base (8453) before connecting |
| database pool exhausted | too many concurrent connections on free Supabase tier | upgrade Supabase plan or reduce Railway deployments |
| migration hangs on boot | active connections locking Supabase schema | check Supabase console, restart pooler, kill long-running queries |
0x1646452F98E36A3c9Cfc3eDD8868221E207B5eEC). it is contract-enforced on Base and cannot be removed by self-hosting or forking.shopify-batch-payouts skill