6.8 KiB
6.8 KiB
Redefined Designs — Project Context
What this is
A storefront for one-of-a-kind items (each item has quantity 1 — once sold, it's gone). Built solo, deployed to a personal Synology NAS homelab via Docker/Portainer, behind Nginx Proxy Manager + authentik SSO for the admin panel.
Repo: https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs
Production URL: https://redefined-designs.bermudalamb.synology.me
Local dev: see README.md
Tech stack
- Backend: Express + TypeScript, Postgres (
pg), single Docker image - Frontend: React + TypeScript + antd (Vite build), served as static files by the backend in production
- Auth: two separate systems —
- Customers: email/password, bcrypt, session cookie (
rd_session), custom-built (not authentik) - Admin: authentik SSO via NPM forward-auth, gated only on
/adminand/api/admin/*— the storefront itself is public
- Customers: email/password, bcrypt, session cookie (
- Payments: PayPal Orders v2 API (multi-item cart checkout, single transaction)
- Shipping validation: USPS Addresses API (OAuth2/REST, not the deprecated Web Tools XML API) — optional, degrades gracefully if unconfigured
- Email: nodemailer via Gmail SMTP — optional, degrades gracefully if unconfigured (logs a warning, skips sending)
- Testing: Jest (unit + integration against a disposable tmpfs Postgres), Playwright (e2e)
- CI: Gitea Actions — two workflows,
sonarqube.yml(static analysis + TS build check) andtests.yml(unit/integration/e2e with job summaries)
Data model (key tables)
items— one-of-a-kind inventory.status:available | reserved | solditem_images— multiple images per item (front/back/etc.), orderedcustomers— accounts, withmarketing_consent(explicit opt-in, GDPR-style — unchecked by default, timestamped, unsubscribe token)carts/cart_items— one cart per customer, items reserved with an admin-configurable expiry (admin_settings.cart_expiry_hours, default 24h)shipping_addresses— per-customer, USPS-validated when configuredcheckouts/checkout_items— one row per multi-item PayPal/demo transaction, snapshotting item+price at purchase timeorders— per-item purchase records, linked tocheckout_idandcustomer_id
Key architectural decisions
- Cart replaced the old single-item "Buy Now" flow entirely. Old
routes/paypal.ts/routes/demo.ts(single-item) exist on disk but are unmounted — superseded byroutes/cartCheckout.tswhich does real multi-item PayPal orders (onepurchase_unitwith an itemizeditems[]breakdown, single transaction, single capture). - One-of-a-kind + double-sell prevention: adding to cart immediately flips
item.statustoreservedinside a DB transaction withFOR UPDATElocking — no two customers can hold the same item. Expired cart holds are swept every 5 minutes back toavailable. - Demo mode (
DEMO_MODE=trueenv var): lets the whole site — including checkout — work with zero PayPal/USPS credentials configured, useful for local dev and just generally having a working fallback. This "degrade gracefully when a third-party integration isn't configured" pattern is used consistently: PayPal, USPS, SMTP all no-op safely if unset rather than crashing. - GDPR/consent handling: marketing consent is never pre-checked, is timestamped with the exact consent text shown, and customers have working self-service data export + account deletion endpoints (
/api/customers/me/export,DELETE /api/customers/me). - Admin panel security boundary: NPM's Advanced nginx config only wraps
auth_requestaroundlocation ~ ^/(admin|api/admin)— everything else (storefront, cart, checkout, PayPal webhook) bypasses authentik entirely. This is a deliberate split, not an oversight — don't accidentally widen or narrow that regex without checking both directions. - Cookie
secureflag is gated onNODE_ENV === 'production', not hardcodedtrue— otherwise integration tests (plain HTTP, no TLS) silently fail to persist sessions. Learned this the hard way once already.
Conventions (apply to all future work on this repo)
- Every commit message follows Conventional Commits:
feat:,fix:,chore:,docs:,test:,ci:,refactor:, etc. - Never commit directly to
main. Always branch:feature/<short-description>orfix/<short-description>. Open a PR, merge, branch auto-deletes (repo setting is on). - File edits are made directly in VS Code (project is cloned locally) and pushed via PowerShell
git, not the old NAS-SSH-with-Docker-wrapped-git workflow from earlier in this project's history (that was only used before the repo existed locally in VS Code). - Production deploys are manual:
git pullon the NAS copy →docker build --no-cache→ stop/rm the container → redeploy the Portainer stack. No CD pipeline exists yet.
Known gaps / natural next steps
- Tests don't cover the cart/checkout/shipping feature yet — only the pre-cart backend surface has unit/integration tests. Worth adding before trusting this in front of real customers.
- No CD — the Gitea Actions workflows run tests/analysis but don't deploy. Manual NAS rebuild is still required after every merge.
- SonarQube CI still uses the
admintoken, not a dedicatedgitea-ciuser (that was flagged as a to-do on the original .NET project this pattern was copied from, never circled back to for this repo). - USPS/PayPal credentials: check whether these are actually populated in the Portainer stack env vars before assuming checkout works end-to-end in production —
DEMO_MODEmay still be the only thing actually exercised. - Double opt-in for marketing email was discussed and deliberately deferred (single opt-in + easy unsubscribe was judged sufficient for now) — revisit if EU customer volume grows.
- Daily cart reminder emails fire via
node-croninside the app process at 9am container-local time — if the container restarts frequently or memory pressure causes crashes (a known NAS constraint per the broader homelab), reminders could silently stop firing with no alerting on that failure mode.
Where to look first for common tasks
- Add/change a DB table →
backend/init.sql(fresh deploys) and a one-off migration run manually viadocker exec ... psqlagainst the live DB (no migration framework — seebackend/migrations/for the pattern used so far) - Change cart/checkout behavior →
backend/src/routes/cartCheckout.ts(the whole reserve → checkout → complete lifecycle lives here) - Change admin-configurable settings →
admin_settingstable +backend/src/routes/adminSettings.ts+frontend/src/admin/Settings.tsx - NPM/authentik/DSM reverse-proxy config for this app → not in this repo; documented in the homelab's broader Claude Project knowledge base, not here