Troubleshooting

The errors people actually hit, what causes each one, and the exact fix.

Find your symptom below. If it isn't here, jump to Still stuck? at the bottom.

Install and build

npm install fails, or the build errors out with unfamiliar syntax errors

Cause: You are on Node 18 or older. ShipCommerce requires Node.js 20 or newer. On Node 18 the install can fail outright, or it can succeed and then the build breaks in a way that looks like a bug in the code.

Fix: Check your version, then switch.

node -v
# must print v20.x.x or higher

If it prints v18 or lower, install and select Node 20 with nvm:

nvm install 20
nvm use 20
node -v

Then delete the half-finished install and start again:

rm -rf node_modules package-lock.json
npm install

On Vercel: set the Node.js version in Project Settings → General → Node.js Version. If it is pinned to 18, the build will fail there even though it works on your machine.

The Vercel build fails with a missing environment variable

Cause: Environment variables were set locally in .env.local but never added to the Vercel project. .env.local is not committed and is never uploaded — Vercel only sees what you enter in its dashboard.

Fix: Add every required variable in Project Settings → Environment Variables, then redeploy:

NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY
SUPABASE_DB_URL
NEXT_PUBLIC_SITE_NAME
NEXT_PUBLIC_SITE_URL
CRON_SECRET

Why a redeploy is required: anything starting with NEXT_PUBLIC_ is baked into the JavaScript bundle at build time, not read at runtime. Adding or changing one of those values does nothing until you trigger a new deployment. Server-only values (such as SUPABASE_SERVICE_ROLE_KEY) are read at runtime, but redeploying after any change is the safe habit.

The full list, with what each one is for, is on the Configuration page.

Database

npm run db:setup says it cannot connect to the database

Cause: Nearly always one of two things about SUPABASE_DB_URL:

  1. It is the wrong connection string. It must be the URI (transaction mode) value — not the session-mode string, not the direct-connection string, and not the project URL.
  2. The password placeholder inside it was never replaced. Supabase hands you the string with a placeholder such as [YOUR-PASSWORD] in it. You have to paste your real database password in its place, brackets and all removed.

Fix:

  1. In Supabase, open Project Settings → Database → Connection string
  2. Select the URI tab and the transaction mode option
  3. Copy it into .env.local as SUPABASE_DB_URL
  4. Replace the password placeholder with the database password you saved when you created the project
  5. Run npm run db:setup again

Lost the password? Supabase only shows it once. Generate a new one under Project Settings → Database → Reset database password, then update SUPABASE_DB_URL with the new value.

If the password contains special characters (@, :, /, #), they must be percent-encoded inside a URI — this is the most common reason a password that is definitely correct still fails. Resetting to a generated password avoids the problem entirely.

The storefront loads but shows no products

Cause 1 — the products are not Active. A product only appears on the storefront when its Active toggle is on (the is_active column). A product created and saved with that toggle off exists in the admin panel and is invisible to shoppers, which looks exactly like a broken storefront.

Fix: Open Admin → Products, open each product, switch Active on, and save.

Cause 2 — the baseline migration did not finish. If npm run db:setup errored partway through, some tables exist and others do not.

Fix: Just run it again.

npm run db:setup

db:setup is idempotent — re-running it on a database that is already set up is safe and does not wipe your data. If you are unsure whether it completed, run it again.

The admin panel is empty, or every action returns a permission error

Cause: The account you are signed in with does not have is_admin = true on its profiles row. This is not a bug — Row Level Security is doing its job. The admin tables are readable only by admin accounts, so a non-admin sees empty lists and gets refused on writes.

Fix: Promote the account in Supabase.

  1. Sign up through your own storefront with the email you want to use as the admin
  2. In Supabase, open Table Editor → profiles
  3. Find the row for that email
  4. Set is_admin to true and save
  5. Sign out and back in, then reload /admin

Alternative: from a local clone whose .env.local points at this Supabase project, npm run seed creates an admin user. Change its password immediately afterwards.

Also check ADMIN_IP_WHITELIST. If you set it, only the listed CIDR ranges can reach /admin at all. Locking yourself out with your own home IP after it changed is a common self-inflicted version of this problem. Leave it empty to allow any IP.

Payments and webhooks

The webhook returns 400 and Stripe logs "signature verification failed"

Cause: The STRIPE_WEBHOOK_SECRET your app is running with does not belong to the endpoint that sent the event.

Two details catch nearly everyone:

  • Every endpoint has its own secret. If you have separate endpoints for test and live mode, or one for staging and one for production, each has a different whsec_... value. Copying one into the wrong environment produces exactly this error.
  • The local secret is not the deployed secret. The whsec_... printed by npm run stripe:listen belongs to the CLI forwarding session only. It is never the right value for your deployed site.

Fix: In the Stripe Dashboard, open Developers → Webhooks, click the endpoint whose URL matches the site that failed, reveal its signing secret, and set that value as STRIPE_WEBHOOK_SECRET for that environment. Redeploy afterwards.

Also make sure the keys match modes: a live-mode secret key with a test-mode webhook secret (or the reverse) will never verify. Test keys start sk_test_, live keys start sk_live_.

The payment goes through in Stripe, but no order appears in the admin panel

This is the single most common failure — and the most expensive, because nothing looks broken. The customer pays, Stripe shows the charge, and your store records nothing.

Cause: Your webhook endpoint is subscribed to the wrong events. The endpoint is reached, returns 200, and the handler ignores everything it was sent because none of it is an event it acts on.

Fix: The endpoint must be subscribed to exactly these four events:

payment_intent.succeeded
payment_intent.payment_failed
charge.refunded
charge.refund.updated

The endpoint URL is https://yourstore.com/api/stripe/webhook.

The reliable way to get this right is to let the product configure it for you:

npm run stripe:setup

It creates the endpoint with the correct four events and appends the signing secret to .env.local. It is idempotent: if an endpoint with that URL already exists it reuses it rather than creating a duplicate, so running it again to repair a misconfigured endpoint is safe.

If you followed an older version of this guide, fix your endpoint now. Earlier revisions told you to subscribe to checkout.session.completed, the customer.subscription.* events and the invoice.* events. Those are subscription-billing events that this store does not handle. An endpoint configured that way silently records no orders at all.

Open Developers → Webhooks → your endpoint → Update details, remove those events, and select the four listed above — or just run npm run stripe:setup, which will correct the existing endpoint.

After fixing it, place a test order with card 4242 4242 4242 4242 (any future expiry, any CVC) and confirm the order shows up in the admin panel.

Locally, webhooks never arrive at all

Cause: Stripe cannot reach localhost. Events have to be forwarded to your machine by the Stripe CLI.

Fix: In a second terminal, alongside npm run dev:

npm run stripe:listen

It prints a whsec_... secret. Paste that into .env.local as STRIPE_WEBHOOK_SECRET and restart npm run dev. Leave the listener running for as long as you are testing — closing it stops the forwarding. Do not run npm run stripe:setup for local development; it registers a public URL, which localhost is not.

Worth knowing: checkout works without Stripe at all. Cash on delivery and bank transfer are available out of the box, so you can launch with Supabase only and add card payments later.

Email

No order confirmation emails are being sent

Cause 1 — RESEND_API_KEY is not set. Email is optional in ShipCommerce. With no key configured, order processing continues normally and no mail is sent.

Fix: Create an API key at resend.com and set RESEND_API_KEY in your environment, then redeploy.

Cause 2 — you are sending from an unverified domain. Resend rejects sends from a domain you have not verified, so RESEND_FROM_EMAIL=orders@yourstore.com fails until the DNS records are in place.

Fix: In the Resend dashboard add your domain, publish the DNS records it gives you, and wait for it to show as verified. Then set RESEND_FROM_EMAIL to an address on that domain.

onboarding@resend.dev is Resend's shared testing sender. It is fine for checking that the plumbing works, but it is not yours, it is heavily rate-limited, and it should never be the from-address on a live store. Verify your own domain before you take real orders.

Before assuming your store is at fault: open the Emails section of the Resend dashboard. If a send is listed there, ShipCommerce did its part and the problem is delivery — a rejection, a bounce, or a spam folder. If nothing is listed, the send never happened and the cause is one of the two above.

Images and uploads

Product image uploads fail in the admin panel

Cause: Uploads go to Vercel Blob when BLOB_READ_WRITE_TOKEN is set, and fall back to Supabase Storage when it is not. The fallback path is a supported way to run — but it needs the storage migration to have been applied. If db:setup stopped before the storage step, there is no bucket for the fallback to write to, and every upload fails.

Fix — option A (stay on Supabase Storage): re-run the setup so both migrations are applied.

npm run db:setup

It runs 00000000000000_baseline.sql and then 00000000000001_storage.sql, which creates the storage bucket and its policies. It is safe to re-run. You can confirm the bucket exists under Supabase → Storage.

Fix — option B (use Vercel Blob): create a Blob store in your Vercel project, set BLOB_READ_WRITE_TOKEN, and redeploy. Uploads then bypass Supabase Storage entirely.

BLOB_READ_WRITE_TOKEN is optional. Leaving it unset is a valid configuration — uploads work through Supabase Storage, just more slowly.

Login and redirects

After deploying, logging in sends people to localhost or to the wrong domain

Cause: Two separate settings still describe your development machine rather than your live site. Both have to be updated, and fixing only one leaves the redirect broken.

  1. Supabase auth URLs. Supabase builds the post-login redirect from its own configuration, so it keeps sending people to http://localhost:3000 until you change it.
  2. NEXT_PUBLIC_SITE_URL. The app builds links and callback URLs from this value, so it must be your real domain.

Fix:

  1. In Supabase, open Authentication → URL Configuration
  2. Set Site URL to your live domain, e.g. https://yourstore.com
  3. Under Redirect URLs, add both of these:
    https://yourstore.com/
    https://yourstore.com/**
  4. In Vercel, set NEXT_PUBLIC_SITE_URL to the same domain — no trailing slash
  5. Redeploy, because NEXT_PUBLIC_ values are baked in at build time

The variable is NEXT_PUBLIC_SITE_URL. There is no NEXT_PUBLIC_APP_URL in ShipCommerce — if you copied that name from an older guide, the value is simply being ignored. Running npm run post-deploy checks your critical variables and reminds you about the Supabase auth URLs.

Scheduled jobs (cron)

Abandoned orders pile up and are never cleaned out

Cause: Nothing is calling the cleanup endpoint. ShipCommerce exposes /api/cron/cleanup-abandoned-orders, but no vercel.json ships with the product, so nothing schedules it for you. This is a missing schedule, not a bug.

Fix: Add a vercel.json at the root of your project and redeploy:

{
  "crons": [
    { "path": "/api/cron/cleanup-abandoned-orders", "schedule": "0 * * * *" }
  ]
}

That runs it hourly. If you host somewhere other than Vercel, point any external scheduler at the same path and send the Authorization: Bearer header yourself.

The cron endpoint returns 401 — or 500

The two responses mean different things, and telling them apart saves you a lot of guessing.

ResponseWhat it means
500 "CRON_SECRET not configured"The app has no CRON_SECRET at all. Set it in your environment and redeploy.
401 "Unauthorized"The secret exists, but the caller sent the wrong one — or no Authorization header. Usually a stale value on the caller's side after the secret was rotated.

Fix: Generate a secret, set it, and redeploy.

openssl rand -hex 32

Save that as CRON_SECRET. On Vercel, Vercel Cron sends Authorization: Bearer $CRON_SECRET automatically once the variable is set on the project — which is why it is required in production. To check it by hand:

curl -i -H "Authorization: Bearer $CRON_SECRET" \
  https://yourstore.com/api/cron/cleanup-abandoned-orders

If you rotate CRON_SECRET, update it everywhere at once. Changing it in Vercel but not in an external scheduler turns a working job into a silent stream of 401s.

Still stuck?

Email hello@shipcommerce.io. Include these four things and you will usually get a fix in the first reply instead of a round of questions:

  1. The exact command you ran — copied and pasted, not described
  2. The complete error output — the whole thing, not the last line. The useful detail is usually above the part that looks important.
  3. Which step you were on — for example "Step 2: Payments, running npm run stripe:setup"
  4. Test mode or live mode — and whether the problem happens locally, on the deployed site, or both

Never paste secrets. Redact anything starting with sk_, whsec_, re_, your SUPABASE_SERVICE_ROLE_KEY, and your database password. The last few characters of a key are enough for anyone to help you tell two keys apart.

Support coverage depends on your plan — see Purchase Plans.