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
+106
View File
@@ -0,0 +1,106 @@
# Quick-start: docker compose up
# Uses prebuilt Spree image — no local build needed.
# To customize the backend, run: docker compose -f docker-compose.dev.yml up
x-app: &app
image: ghcr.io/spree/spree:${SPREE_VERSION_TAG:-latest}
depends_on:
postgres:
condition: service_healthy
# Uncomment when enabling the optional meilisearch service below.
# meilisearch:
# condition: service_healthy
mailpit:
condition: service_started
env_file: .env
environment:
DATABASE_URL: postgres://postgres@postgres:5432/spree_production
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
RAILS_FORCE_SSL: ${RAILS_FORCE_SSL:-false}
RAILS_ASSUME_SSL: ${RAILS_ASSUME_SSL:-false}
# Public host for generated URLs (image/attachment URLs in API responses,
# email links). Override in .env when serving from a real domain or IP.
RAILS_HOST: ${RAILS_HOST:-localhost:${SPREE_PORT:-3000}}
# Product search runs on the database by default. To switch to Meilisearch,
# uncomment this together with the meilisearch depends_on/service/volume
# blocks, then reindex: docker compose exec web bin/rails spree:search:reindex
# MEILISEARCH_URL: http://meilisearch:7700
# All outgoing mail is captured by Mailpit — read it at http://localhost:8025.
# Set SMTP_HOST (and friends) in .env to deliver through a real provider instead.
SMTP_HOST: ${SMTP_HOST:-mailpit}
SMTP_PORT: ${SMTP_PORT:-1025}
services:
postgres:
image: postgres:18-alpine
environment:
POSTGRES_HOST_AUTH_METHOD: trust
PGDATA: /var/lib/postgresql/18/docker
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: pg_isready -U postgres
interval: 5s
timeout: 5s
retries: 5
# Optional Meilisearch search provider (typo tolerance, instant facets).
# Uncomment together with the meilisearch lines under x-app's depends_on and
# environment, and the meilisearch_data volume at the bottom.
# meilisearch:
# image: getmeili/meilisearch:latest
# volumes:
# - meilisearch_data:/meili_data
# ports:
# # Loopback-only: the app reaches Meilisearch over the compose network;
# # the host port is for debugging and must not be LAN-reachable (Docker's
# # NAT rules bypass host firewalls on Linux, and no API key is set).
# - "127.0.0.1:${SPREE_MEILISEARCH_PORT:-7700}:7700"
# healthcheck:
# test: curl -f http://localhost:7700/health || exit 1
# interval: 5s
# timeout: 5s
# retries: 5
mailpit:
image: axllent/mailpit:latest
ports:
# Loopback-only, matching meilisearch: reachable from the host browser
# but not the LAN (Docker's NAT rules bypass host firewalls on Linux).
# 8025 = web UI — open http://localhost:8025 to read captured mail.
# 1025 = SMTP endpoint the app delivers to (in-cluster as mailpit:1025).
- "127.0.0.1:${MAILPIT_UI_PORT:-8025}:8025"
- "127.0.0.1:${MAILPIT_SMTP_PORT:-1025}:1025"
environment:
MP_MAX_MESSAGES: "500"
MP_SMTP_AUTH_ACCEPT_ANY: "true"
MP_SMTP_AUTH_ALLOW_INSECURE: "true"
# One app container: Puma serves the web AND runs Solid Queue's job
# supervisor in-process (SOLID_QUEUE_IN_PUMA, on by default). To split job
# processing out, add a service from the same image with
# `command: bin/jobs` and set SOLID_QUEUE_IN_PUMA: "false" here.
web:
<<: *app
ports:
- "${SPREE_PORT:-3000}:3000"
volumes:
- storage_data:/rails/storage
- ./config/initializers/api_body_limit.rb:/rails/config/initializers/api_body_limit.rb:ro
- ./config/initializers/image_studio.rb:/rails/config/initializers/image_studio.rb:ro
- ./config/initializers/address_label.rb:/rails/config/initializers/address_label.rb:ro
- ./config/initializers/customer_preferences.rb:/rails/config/initializers/customer_preferences.rb:ro
- ./config/initializers/easyparcel_calculator.rb:/rails/config/initializers/easyparcel_calculator.rb:ro
- ./config/routes.rb:/rails/config/routes.rb:ro
- ./app/controllers/spree:/rails/app/controllers/spree:ro
- ./app/views/spree:/rails/app/views/spree:ro
healthcheck:
test: curl -f http://localhost:3000/up || exit 1
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
volumes:
postgres_data:
# meilisearch_data:
storage_data: