Pak Jabit: full stack — storefront, Spree customisations, catalogue tooling

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>
This commit is contained in:
2026-09-15 22:09:55 +08:00
co-authored by Claude Opus 5
commit a58a61b98c
98 changed files with 15524 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
# 🧨 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** and **`Free Shipping` US $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.*