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:
+97
@@ -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.*
|
||||
Reference in New Issue
Block a user