Skip to content

Build your first store

By the end of this you will have a store with a product you made, a completed test payment, and an order in the admin that Stripe’s webhook marked paid. It takes about twenty minutes, most of it waiting for npm install.

You need Node 22, a Stripe account in test mode, and the Stripe CLI.

Terminal window
git clone https://github.com/binx/beluga-v2.git my-store
cd my-store
nvm use
npm install
npm run setup

Setup asks for a database (accept the SQLite default), your Stripe test secret key and publishable key, a public URL (leave the default while developing), an admin email and password, and a store name. Say yes to the demo catalogue; you will delete it in a moment, and it is useful to see a populated store first.

Terminal window
npm run dev:all

Open http://localhost:5173. That is the demo store. Open http://localhost:5173/admin and sign in.

In a second terminal:

Terminal window
stripe listen --forward-to localhost:4000/api/webhooks/stripe

Copy the whsec_… it prints into .env:

STRIPE_WEBHOOK_SECRET=whsec_...

Restart npm run dev:all. Without this step everything will look like it works and no order will ever be marked paid, because only the webhook does that.

Admin → Products → New product. The editor is one form that saves itself as you type; the indicator in the corner says when. Fill in:

  • A name and a description. The slug is generated from the name.
  • Type: physical.
  • One option axis, say Size with values Small and Large. The variant table below fills itself in with one row per value. Give each a price and a weight.
  • An image. Drop a JPEG or PNG; it is re-encoded to WebP, stripped of EXIF, and resized copies are written for srcset.

Set the product live, then click Publish. That is the first and only time anything is written to Stripe: a Product and one Price per variant appear in your test dashboard. Until you publish, a live product shows in the storefront but cannot be bought, and the dashboard says so.

Delete the demo products now if you like. Products → ⋯ → Delete names what is about to go and refuses an unknown id.

A store with no rates ships everything free and nothing at checkout says so. The admin Overview warns you. Admin → Shipping → Add rate: name it Standard, give it a price, leave the zone and bounds empty. An unpinned, unbounded rate applies everywhere, which is the whole configuration a flat-rate store needs.

Back in the storefront, add your product to the cart. The cart asks which country you are shipping to; that is how a zone-priced store knows the postage before Stripe’s page exists. Check out, and on Stripe’s page use:

4242 4242 4242 4242 any future expiry any CVC any postcode

You land on /confirm, which polls the order’s status until the webhook has recorded payment. Your stripe listen terminal shows checkout.session.completed arriving and a 200 going back.

Admin → Orders. The order is paid, with the shipping address Stripe collected, the line as it was bought, and stock decremented on the variant. Mark it processing, then shipped with a carrier and tracking number. Each transition sends an email, or logs one if SMTP_URL is not set yet.

Replay the webhook and prove idempotency to yourself:

Terminal window
stripe events resend <event id from the listen output>

Stock does not move a second time.