A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.
What is here:
storefront/ the Next.js app
backend/ ONLY the custom Spree code. Spree itself is a prebuilt image
that mounts no app code, so every file here needs an explicit
volume entry in docker-compose.yml or it is silently ignored.
catalogue/ the scripts that loaded and maintain the products
docs/ manifest, mock registry, napkins
Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.
The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.3 KiB
🧨 LANDMINE — where every key lives and what it opens
This repo is on the house's private Gitea (gitmirah:3000, Tailscale-only),
so real .env files are committed on purpose. That is a deliberate trade: no
.env.example drift, no "works on my box", and anyone cloning has a running
stack. It also means a clone of this repo is a full set of production
credentials. Treat it that way.
Before this repo goes anywhere else — a public mirror, GitHub, a zip to a client — every secret below has to be rotated first. Deleting the file later does not help; git keeps history.
What each key opens
| Key | Lives in | Opens | Blast radius if leaked |
|---|---|---|---|
SPREE_ADMIN_KEY |
catalogue/.env |
Full Spree admin API | 🔴 Total. Create/delete/reprice every product, read every order and customer. |
SECRET_KEY_BASE |
backend/.env |
Rails signing | 🔴 Forges sessions and signed URLs. Rotating breaks every signed image URL and the admin API key — see below. |
NEXT_PUBLIC_SPREE_API_KEY |
storefront/.env.production |
Spree store API | 🟡 Low. Public by design — it ships to every browser. Read products, create carts. Not admin. |
EASYPARCEL_CLIENT_ID / _SECRET |
storefront/.env.production |
EasyParcel account | 🟠 Book real shipments, spend real money. |
PHOTOROOM_API_KEY |
backend/.env |
Photoroom sandbox | 🟢 Low. Watermarked output, 1000/month free. The live key is not in this repo. |
Not in this repo
| Thing | Where it actually is |
|---|---|
| Photoroom live key | /root/.photoroom/credentials on the house box (chmod 600). 10 images/month. |
| Spree admin password | Nowhere written down. One shared account — see below. |
| GCloud SSH key | /root/.ssh/google_compute_engine on the house box. |
storefront/.easyparcel-token.json |
A cached OAuth token, committed with the rest. It expires and regenerates itself; harmless, but it is a live credential while valid. |
🔴 Do not rotate SECRET_KEY_BASE casually
It signs more than sessions. Changing it:
- breaks every ActiveStorage image URL — 3,228 blobs, the whole catalogue goes blank
- invalidates the admin API key, so every script in
catalogue/stops working
To force everyone out of the admin, change the admin password instead — that invalidates sessions without touching signed URLs.
🔴 One shared admin account
Every admin session is indistinguishable. This already cost an investigation: an
unknown IP (42.188.122.106, Windows/Brave, valid session cookie) was browsing
/admin/api_keys and /admin/admin_users, and there was no way to attribute it.
It turned out to be a colleague.
~5 people are expected to use the admin. Give each one their own account (Settings → Users) before that number grows.
Where the boxes are
| Host | Access | |
|---|---|---|
| Storefront | celest-proto 34.21.217.116 |
ssh ijat@34.21.217.116 |
| Backend + DB | pak-jabit-backend 35.247.170.3 |
ssh -i ~/.ssh/google_compute_engine ijat@35.247.170.3 |
Public: shop.jux-lab.com · api.jux-lab.com · admin.jux-lab.com
🔴 The custom backend code exists ONLY as bind mounts
ghcr.io/spree/spree is prebuilt and mounts no app code. Everything under
backend/config/ and backend/app/ needs an explicit volume entry in
docker-compose.yml, or the container simply never loads it — no error, no
warning, dead file. The 10MB body-limit initializer sat on the host for a whole
day doing nothing before anyone noticed.
None of it is in the database. A rebuilt box without these files comes back with no Image Studio, no address labels, no region preference, and no EasyParcel rates.
Sandbox things that are NOT real
Full list in docs/MOCKS.md. The ones that matter most:
Test Card (sandbox)— a Bogus gateway that approves any card ending in the right digits. Deactivate before launch.Test Payment— offline placeholder, no money moves.Standard Delivery (South Africa)R99 andFree ShippingUS $0 — placeholder rates, not quotes. (Malaysia is real: live EasyParcel.)studio-test-chinos— a fake product, kept deliberately as the worked example for the Image Studio.
Written 2026-09-15, the night the stack got its region anchor and a customer-owned cart.