A second, self-contained stack for reviewing merged-but-undeployed changes online before they reach production. Brought up only when a review is needed and stopped afterwards — it is not expected to be running most of the time.
Prompted by the deploy of #23, where a schema/code mismatch was only discovered in production. A QA stack is where that should have surfaced.
Decisions
Question
Decision
Data
Empty database plus a seed script. No production data is ever copied, so no customer PII leaves production. Reviews run against known fixtures.
Access
Own subdomain, fully authentik-gated — forward-auth on location /, not just /admin. Nothing unreleased is publicly reachable or indexable.
Both alternatives were considered and rejected:
Copying production data (anonymised or not) puts real emails, bcrypt hashes, and shipping addresses into a second environment. Anonymisation would make the scrub script security-critical — any column it misses is a leak, and every future PII column has to be remembered.
Matching production's gating (only /admin behind auth) would leave an unreleased storefront publicly reachable.
Note this means QA deliberately differs from production in one respect: the whole site is gated. The production auth boundary (^/(admin|api/admin) only) is therefore not exercised by QA and still needs care when changed.
Shape
Separate Portainer stack, normally stopped:
Production
QA
App container
redefined-designs-syn
redefined-designs-qa-syn
DB container
redefined-designs-db-syn
redefined-designs-qa-db-syn
Database
redefined
redefined_qa
Image tag
redefined-designs:latest
redefined-designs:qa
Hostname
redefined-designs.bermudalamb.synology.me
qa-redefined-designs.bermudalamb.synology.me
Gating
^/(admin|api/admin)
/ — everything
Data
Real
Seeded fixtures, disposable
QA runs with DEMO_MODE=true, so no PayPal or USPS credentials are needed and checkout is exercisable end to end.
Migrations already run at container start (added in the #23 follow-up), so a QA deploy cannot land ahead of its schema.
Work
In this repo:
docker-compose.qa.yml — the stack definition for Portainer, with its own volume and container names
backend/seed-qa.js plus an npm run seed:qa script — creates a small representative catalogue: a nested category tree, several colour-coded tags, and items across all three statuses (available / reserved / sold), plus one known test customer
A guard on the seed script: it must refuse to run unless the target database is explicitly a QA/test database, so it can never be pointed at production by accident. Same reasoning as the destructive-test guard — a seeding script that runs against the wrong database is exactly the failure mode this issue exists to prevent
Idempotency: re-running the seed must not duplicate fixtures
README section covering how to bring the stack up, seed it, review, and tear it down
.claude/project-context.md — QA stack added to the environment description and deploy workflow
On the NAS (manual, outside this repo):
New Portainer stack from docker-compose.qa.yml
NPM proxy host for qa-redefined-designs, with authentik forward-auth on location /
DNS/local record for the QA hostname
Review workflow once it exists
Merge the change to main
On the NAS: pull, build redefined-designs:qa, start the QA stack
docker exec redefined-designs-qa-syn node seed-qa.js if fresh fixtures are wanted
Review at the QA hostname, logged in through authentik
Stop the QA stack; promote to production using the normal deploy sequence
Out of scope
Automated deployment to QA — the NAS has no CD, and adding it is its own piece of work
Running QA continuously
Any copying of production data
## Goal
A second, self-contained stack for reviewing merged-but-undeployed changes online before they reach production. Brought up only when a review is needed and stopped afterwards — it is not expected to be running most of the time.
Prompted by the deploy of #23, where a schema/code mismatch was only discovered in production. A QA stack is where that should have surfaced.
## Decisions
| Question | Decision |
| --- | --- |
| Data | **Empty database plus a seed script.** No production data is ever copied, so no customer PII leaves production. Reviews run against known fixtures. |
| Access | **Own subdomain, fully authentik-gated** — forward-auth on `location /`, not just `/admin`. Nothing unreleased is publicly reachable or indexable. |
Both alternatives were considered and rejected:
- *Copying production data* (anonymised or not) puts real emails, bcrypt hashes, and shipping addresses into a second environment. Anonymisation would make the scrub script security-critical — any column it misses is a leak, and every future PII column has to be remembered.
- *Matching production's gating* (only `/admin` behind auth) would leave an unreleased storefront publicly reachable.
Note this means QA deliberately differs from production in one respect: the whole site is gated. The production auth boundary (`^/(admin|api/admin)` only) is therefore **not** exercised by QA and still needs care when changed.
## Shape
Separate Portainer stack, normally stopped:
| | Production | QA |
| --- | --- | --- |
| App container | `redefined-designs-syn` | `redefined-designs-qa-syn` |
| DB container | `redefined-designs-db-syn` | `redefined-designs-qa-db-syn` |
| Database | `redefined` | `redefined_qa` |
| Image tag | `redefined-designs:latest` | `redefined-designs:qa` |
| Hostname | `redefined-designs.bermudalamb.synology.me` | `qa-redefined-designs.bermudalamb.synology.me` |
| Gating | `^/(admin\|api/admin)` | `/` — everything |
| Data | Real | Seeded fixtures, disposable |
QA runs with `DEMO_MODE=true`, so no PayPal or USPS credentials are needed and checkout is exercisable end to end.
Migrations already run at container start (added in the #23 follow-up), so a QA deploy cannot land ahead of its schema.
## Work
**In this repo:**
- [ ] `docker-compose.qa.yml` — the stack definition for Portainer, with its own volume and container names
- [ ] `backend/seed-qa.js` plus an `npm run seed:qa` script — creates a small representative catalogue: a nested category tree, several colour-coded tags, and items across all three statuses (`available` / `reserved` / `sold`), plus one known test customer
- [ ] **A guard on the seed script**: it must refuse to run unless the target database is explicitly a QA/test database, so it can never be pointed at production by accident. Same reasoning as the destructive-test guard — a seeding script that runs against the wrong database is exactly the failure mode this issue exists to prevent
- [ ] Idempotency: re-running the seed must not duplicate fixtures
- [ ] README section covering how to bring the stack up, seed it, review, and tear it down
- [ ] `.claude/project-context.md` — QA stack added to the environment description and deploy workflow
**On the NAS (manual, outside this repo):**
- [ ] New Portainer stack from `docker-compose.qa.yml`
- [ ] NPM proxy host for `qa-redefined-designs`, with authentik forward-auth on `location /`
- [ ] DNS/local record for the QA hostname
## Review workflow once it exists
1. Merge the change to `main`
2. On the NAS: pull, build `redefined-designs:qa`, start the QA stack
3. `docker exec redefined-designs-qa-syn node seed-qa.js` if fresh fixtures are wanted
4. Review at the QA hostname, logged in through authentik
5. Stop the QA stack; promote to production using the normal deploy sequence
## Out of scope
- Automated deployment to QA — the NAS has no CD, and adding it is its own piece of work
- Running QA continuously
- Any copying of production data
bermudalamb
added this to the Initial Build project 2026-08-17 11:18:04 -05:00
docker-compose.qa.yml is committed on branch fix/deploy-migration-safety, derived from the current production stack. Validated with docker compose config, and checked programmatically that no production container name, host port, volume path, database name, or image tag appears in it.
What differs from production, and why
Production
QA
Why
App container
redefined-designs-syn
redefined-designs-qa-syn
Name collision would replace the running container
DB container
redefined-designs-db-syn
redefined-designs-qa-db-syn
Same
Host port
32750
32751
Both must bind simultaneously
Postgres volume
/volume1/configs/redefined-designs/postgres
/volume1/configs/redefined-designs-qa/postgres
The critical one — a shared data directory means QA writing into production's database files
Uploads volume
/volume1/configs/redefined-designs/uploads
/volume1/configs/redefined-designs-qa/uploads
A QA teardown would otherwise delete real product images
Database / user
redefined
redefined_qa
Separation even if a host is ever misconfigured
Image
redefined-designs:latest
redefined-designs:qa
QA can run a build production isn't on
Secret variable
DB_PASSWORD
QA_DB_PASSWORD
Renamed so pasting production's stack variables here fails visibly instead of silently working
PayPal
Live credentials
None, DEMO_MODE=true
The full cart and checkout path stays exercisable with no route to live PayPal
SMTP
Brevo relay
None
The mailer degrades gracefully when unset — a QA run cannot email anyone if a fixture ever holds a real address
Restart policy
unless-stopped
"no"
QA should be up only during a review; unless-stopped would silently bring it back after every NAS reboot
Portainer stack name matters
Name the stack redefined-designs-qa, not redefined-designs. The stack name becomes the compose project name — reusing production's would make compose treat this as the same project and reconcile the two against each other, removing the production containers because they aren't declared in this file. Noted at the top of the file as well.
One-time NAS setup
sudo mkdir -p /volume1/configs/redefined-designs-qa/postgres
sudo mkdir -p /volume1/configs/redefined-designs-qa/uploads
# The official postgres image runs as uid/gid 999. Without this the DB# container exits immediately with a data-directory permissions error.
sudo chown -R 999:999 /volume1/configs/redefined-designs-qa/postgres
sudo chmod 700 /volume1/configs/redefined-designs-qa/postgres
# Uploads are written by the app container, which runs as root.
sudo chown -R root:root /volume1/configs/redefined-designs-qa/uploads
sudo chmod 755 /volume1/configs/redefined-designs-qa/uploads
# Confirm the two environments are separate before going further.
ls -la /volume1/configs/redefined-designs-qa/
ls -la /volume1/configs/redefined-designs/
Full workflow — build, review, and reset — is in the README.
Checklist status
docker-compose.qa.yml
README section covering bring-up, review, and teardown
backend/seed-qa.js plus npm run seed:qa
Guard so the seed script cannot target production
Seed idempotency
.claude/project-context.md updated with the QA stack
Remaining NAS-side work: create the Portainer stack, add the NPM proxy host for qa-redefined-designs on port 32751 with authentik forward-auth on location /, and the DNS record.
## Stack definition added
`docker-compose.qa.yml` is committed on branch `fix/deploy-migration-safety`, derived from the current production stack. Validated with `docker compose config`, and checked programmatically that no production container name, host port, volume path, database name, or image tag appears in it.
### What differs from production, and why
| | Production | QA | Why |
| --- | --- | --- | --- |
| App container | `redefined-designs-syn` | `redefined-designs-qa-syn` | Name collision would replace the running container |
| DB container | `redefined-designs-db-syn` | `redefined-designs-qa-db-syn` | Same |
| Host port | `32750` | `32751` | Both must bind simultaneously |
| Postgres volume | `/volume1/configs/redefined-designs/postgres` | `/volume1/configs/redefined-designs-qa/postgres` | **The critical one** — a shared data directory means QA writing into production's database files |
| Uploads volume | `/volume1/configs/redefined-designs/uploads` | `/volume1/configs/redefined-designs-qa/uploads` | A QA teardown would otherwise delete real product images |
| Database / user | `redefined` | `redefined_qa` | Separation even if a host is ever misconfigured |
| Image | `redefined-designs:latest` | `redefined-designs:qa` | QA can run a build production isn't on |
| Secret variable | `DB_PASSWORD` | `QA_DB_PASSWORD` | Renamed so pasting production's stack variables here fails visibly instead of silently working |
| PayPal | Live credentials | **None**, `DEMO_MODE=true` | The full cart and checkout path stays exercisable with no route to live PayPal |
| SMTP | Brevo relay | **None** | The mailer degrades gracefully when unset — a QA run cannot email anyone if a fixture ever holds a real address |
| Restart policy | `unless-stopped` | `"no"` | QA should be up only during a review; `unless-stopped` would silently bring it back after every NAS reboot |
### Portainer stack name matters
Name the stack **`redefined-designs-qa`**, not `redefined-designs`. The stack name becomes the compose project name — reusing production's would make compose treat this as the same project and reconcile the two against each other, removing the production containers because they aren't declared in this file. Noted at the top of the file as well.
### One-time NAS setup
```bash
sudo mkdir -p /volume1/configs/redefined-designs-qa/postgres
sudo mkdir -p /volume1/configs/redefined-designs-qa/uploads
# The official postgres image runs as uid/gid 999. Without this the DB
# container exits immediately with a data-directory permissions error.
sudo chown -R 999:999 /volume1/configs/redefined-designs-qa/postgres
sudo chmod 700 /volume1/configs/redefined-designs-qa/postgres
# Uploads are written by the app container, which runs as root.
sudo chown -R root:root /volume1/configs/redefined-designs-qa/uploads
sudo chmod 755 /volume1/configs/redefined-designs-qa/uploads
# Confirm the two environments are separate before going further.
ls -la /volume1/configs/redefined-designs-qa/
ls -la /volume1/configs/redefined-designs/
```
Full workflow — build, review, and reset — is in the README.
### Checklist status
- [x] `docker-compose.qa.yml`
- [x] README section covering bring-up, review, and teardown
- [ ] `backend/seed-qa.js` plus `npm run seed:qa`
- [ ] Guard so the seed script cannot target production
- [ ] Seed idempotency
- [ ] `.claude/project-context.md` updated with the QA stack
Remaining NAS-side work: create the Portainer stack, add the NPM proxy host for `qa-redefined-designs` on port `32751` with authentik forward-auth on `location /`, and the DNS record.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Goal
A second, self-contained stack for reviewing merged-but-undeployed changes online before they reach production. Brought up only when a review is needed and stopped afterwards — it is not expected to be running most of the time.
Prompted by the deploy of #23, where a schema/code mismatch was only discovered in production. A QA stack is where that should have surfaced.
Decisions
location /, not just/admin. Nothing unreleased is publicly reachable or indexable.Both alternatives were considered and rejected:
/adminbehind auth) would leave an unreleased storefront publicly reachable.Note this means QA deliberately differs from production in one respect: the whole site is gated. The production auth boundary (
^/(admin|api/admin)only) is therefore not exercised by QA and still needs care when changed.Shape
Separate Portainer stack, normally stopped:
redefined-designs-synredefined-designs-qa-synredefined-designs-db-synredefined-designs-qa-db-synredefinedredefined_qaredefined-designs:latestredefined-designs:qaredefined-designs.bermudalamb.synology.meqa-redefined-designs.bermudalamb.synology.me^/(admin|api/admin)/— everythingQA runs with
DEMO_MODE=true, so no PayPal or USPS credentials are needed and checkout is exercisable end to end.Migrations already run at container start (added in the #23 follow-up), so a QA deploy cannot land ahead of its schema.
Work
In this repo:
docker-compose.qa.yml— the stack definition for Portainer, with its own volume and container namesbackend/seed-qa.jsplus annpm run seed:qascript — creates a small representative catalogue: a nested category tree, several colour-coded tags, and items across all three statuses (available/reserved/sold), plus one known test customer.claude/project-context.md— QA stack added to the environment description and deploy workflowOn the NAS (manual, outside this repo):
docker-compose.qa.ymlqa-redefined-designs, with authentik forward-auth onlocation /Review workflow once it exists
mainredefined-designs:qa, start the QA stackdocker exec redefined-designs-qa-syn node seed-qa.jsif fresh fixtures are wantedOut of scope
Stack definition added
docker-compose.qa.ymlis committed on branchfix/deploy-migration-safety, derived from the current production stack. Validated withdocker compose config, and checked programmatically that no production container name, host port, volume path, database name, or image tag appears in it.What differs from production, and why
redefined-designs-synredefined-designs-qa-synredefined-designs-db-synredefined-designs-qa-db-syn3275032751/volume1/configs/redefined-designs/postgres/volume1/configs/redefined-designs-qa/postgres/volume1/configs/redefined-designs/uploads/volume1/configs/redefined-designs-qa/uploadsredefinedredefined_qaredefined-designs:latestredefined-designs:qaDB_PASSWORDQA_DB_PASSWORDDEMO_MODE=trueunless-stopped"no"unless-stoppedwould silently bring it back after every NAS rebootPortainer stack name matters
Name the stack
redefined-designs-qa, notredefined-designs. The stack name becomes the compose project name — reusing production's would make compose treat this as the same project and reconcile the two against each other, removing the production containers because they aren't declared in this file. Noted at the top of the file as well.One-time NAS setup
Full workflow — build, review, and reset — is in the README.
Checklist status
docker-compose.qa.ymlbackend/seed-qa.jsplusnpm run seed:qa.claude/project-context.mdupdated with the QA stackRemaining NAS-side work: create the Portainer stack, add the NPM proxy host for
qa-redefined-designson port32751with authentik forward-auth onlocation /, and the DNS record.