docs/superpowers/plans/2026-04-20-issue-7570-ueberdb2-drivers.md
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Restore Etherpad Docker production startup for every supported DB backend (broken by [email protected] moving drivers to optional peer deps), and add CI that would have caught it.
Architecture: Two coupled PRs. Upstream PR to ether/ueberDB moves the 10 DB drivers back from peerDependencies (optional) to dependencies and publishes 5.0.46. Downstream PR to ether/etherpad-lite bumps ueberdb2, declares the same 10 drivers as direct deps as a defensive safety net, and adds a build-test-db-drivers CI job with a require-each-driver presence check plus MySQL + Postgres smoke tests that gate publish.
Tech Stack: Node.js / pnpm / TypeScript, ueberdb2 KV abstraction, Docker Buildx, GitHub Actions service containers, vitest + testcontainers (upstream tests).
Spec: docs/superpowers/specs/2026-04-20-issue-7570-ueberdb2-drivers-design.md
Conventions:
johnmclear/ forks — never ether/* directly.fix/issue-7570-ueberdb2-drivers already exists in /home/jose/etherpad/etherpad-lite with the design spec committed./home/jose/etherpad/etherpad-lite/home/jose/etherpad/ueberDBether/ueberDB)Files: none (git/gh only)
Run:
gh repo fork ether/ueberDB --clone=false --default-branch-only
Expected: ✓ Created fork johnmclear/ueberDB.
If it already exists the command prints johnmclear/ueberDB already exists — that is fine, continue.
Run:
git clone https://github.com/johnmclear/ueberDB.git /home/jose/etherpad/ueberDB
cd /home/jose/etherpad/ueberDB
git remote add upstream https://github.com/ether/ueberDB.git
git fetch upstream
Expected: clone succeeds, git remote -v shows both origin (johnmclear) and upstream (ether).
Run:
git -C /home/jose/etherpad/ueberDB remote show upstream | grep 'HEAD branch'
Expected: prints either HEAD branch: develop or HEAD branch: main. Record the name — subsequent steps refer to it as <default>.
Run:
git -C /home/jose/etherpad/ueberDB checkout <default>
git -C /home/jose/etherpad/ueberDB pull upstream <default>
git -C /home/jose/etherpad/ueberDB push origin <default>
Expected: fork's default branch now matches upstream.
Run:
git -C /home/jose/etherpad/ueberDB checkout -b fix/bundle-driver-deps
Expected: switched to a new branch.
Files: none — read-only validation
Run:
cd /home/jose/etherpad/ueberDB && pnpm install
Expected: install succeeds, no unusual errors.
Run:
pnpm run ts-check && pnpm run lint
Expected: both pass with exit 0.
Do not run pnpm test here — it uses testcontainers and spins up every database, which is slow and requires Docker. CI will run it. If Docker is available and the engineer wants to sanity-run it:
pnpm test
Expected: all driver suites pass.
Files:
Modify: /home/jose/etherpad/ueberDB/package.json
Step 1: Read current package.json
Open /home/jose/etherpad/ueberDB/package.json. Locate the three relevant blocks: dependencies, peerDependencies, peerDependenciesMeta. Current state (as of 5.0.45):
"dependencies": {
"async": "^3.2.6",
"dirty-ts": "^1.1.8",
"rusty-store-kv": "^1.3.1",
"simple-git": "^3.36.0"
},
"peerDependencies": {
"@elastic/elasticsearch": "^9.3.4",
"cassandra-driver": "^4.8.0",
"mongodb": "^7.1.1",
"mssql": "^12.2.1",
"mysql2": "^3.22.0",
"nano": "^11.0.5",
"pg": "^8.20.0",
"redis": "^5.12.1",
"rethinkdb": "^2.4.2",
"surrealdb": "^2.0.3"
},
"peerDependenciesMeta": {
"@elastic/elasticsearch": {"optional": true},
"cassandra-driver": {"optional": true},
"mongodb": {"optional": true},
"mssql": {"optional": true},
"mysql2": {"optional": true},
"nano": {"optional": true},
"pg": {"optional": true},
"redis": {"optional": true},
"rethinkdb": {"optional": true},
"surrealdb": {"optional": true}
},
Replace the three blocks with:
"dependencies": {
"@elastic/elasticsearch": "^9.3.4",
"async": "^3.2.6",
"cassandra-driver": "^4.8.0",
"dirty-ts": "^1.1.8",
"mongodb": "^7.1.1",
"mssql": "^12.2.1",
"mysql2": "^3.22.0",
"nano": "^11.0.5",
"pg": "^8.20.0",
"redis": "^5.12.1",
"rethinkdb": "^2.4.2",
"rusty-store-kv": "^1.3.1",
"simple-git": "^3.36.0",
"surrealdb": "^2.0.3"
},
Delete the peerDependencies and peerDependenciesMeta blocks entirely.
Keys must be alphabetically sorted in the merged dependencies block (matches existing convention).
In the same file, change "version": "5.0.45" to "version": "5.0.46".
Run:
cd /home/jose/etherpad/ueberDB && pnpm install
Expected: pnpm-lock.yaml updates. No errors. No warnings about missing peer deps (since there are now none).
Run:
pnpm run ts-check && pnpm run lint
Expected: exit 0.
Run:
cd /tmp && rm -rf uberdb-smoke && mkdir uberdb-smoke && cd uberdb-smoke
npm init -y >/dev/null
npm install file:/home/jose/etherpad/ueberDB 2>&1 | tail -3
node -e "
const mods = [
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
];
for (const m of mods) {
try { require(m); console.log('ok', m); }
catch (e) { console.error('MISSING', m, e.message); process.exit(1); }
}
"
Expected: prints ok ten times, exits 0.
This is the direct repro test for the issue.
Files:
Stage: /home/jose/etherpad/ueberDB/package.json, /home/jose/etherpad/ueberDB/pnpm-lock.yaml
Step 1: Review staged diff
Run:
cd /home/jose/etherpad/ueberDB
git diff package.json
git status
Expected: only package.json and pnpm-lock.yaml modified. Confirm the diff matches Task A3.
Run:
git add package.json pnpm-lock.yaml
git commit -m "$(cat <<'EOF'
fix: bundle DB drivers as dependencies (restore pre-5.0.45 behavior)
Moves the ten DB drivers back from optional peerDependencies to
dependencies so consumers (notably Etherpad Docker production images)
get them installed automatically.
Fixes Etherpad issue ether/etherpad-lite#7570:
"Cannot find module 'mysql2'" at startup when pnpm production install
skips optional peer deps.
EOF
)"
Expected: one commit created on fix/bundle-driver-deps.
Run:
git push -u origin fix/bundle-driver-deps
Expected: branch pushed to johnmclear/ueberDB.
Files: none
Run:
gh pr create \
--repo ether/ueberDB \
--base <default> \
--head johnmclear:fix/bundle-driver-deps \
--title "fix: bundle DB drivers as dependencies (fix Etherpad #7570)" \
--body "$(cat <<'EOF'
## Summary
- Moves all ten DB drivers (`@elastic/elasticsearch`, `cassandra-driver`, `mongodb`, `mssql`, `mysql2`, `nano`, `pg`, `redis`, `rethinkdb`, `surrealdb`) from `peerDependencies` + `peerDependenciesMeta.optional` back to `dependencies`.
- Deletes `peerDependenciesMeta`.
- Bumps version to 5.0.46.
## Why
`[email protected]` declared the drivers as optional peer deps. Production installs (e.g., `pnpm install --prod`) skip optional peer deps, so consumers hit `Error: Cannot find module 'mysql2'` at first `require` of a driver.
Reported against Etherpad Docker: https://github.com/ether/etherpad-lite/issues/7570
## Test plan
- [ ] CI green (vitest + testcontainers exercises every driver)
- [ ] Local smoke: `npm install ueberdb2@next` into a fresh project, then `require()` each of the ten driver modules — all resolve
EOF
)"
Replace <default> with whatever Task A1 step 3 recorded (develop or main).
Expected: PR URL printed.
Note the PR URL — downstream task (closing #7571) references it.
Files: none — gating task
Once review passes on the PR opened in Task A5, merge it. Then:
cd /home/jose/etherpad/ueberDB
git checkout <default>
git pull upstream <default>
Expected: local default branch is at the merge commit.
Run the project's existing release workflow. Typically:
cd /home/jose/etherpad/ueberDB
npm publish
(Or trigger the CI publish workflow if one exists — check .github/workflows/ first.)
Expected: [email protected] is live on npm. Verify:
npm view ueberdb2 version
Expected output: 5.0.46.
Do not start Phase B until this step completes — downstream pnpm install must be able to resolve ueberdb2@^5.0.46 from the public registry.
ether/etherpad-lite)Files: none — GitHub issue action
Run:
gh pr close 7571 \
--repo ether/etherpad-lite \
--comment "Superseded by the upstream ueberdb2 fix (<Task A5 PR URL>) plus a clean downstream replacement PR incoming on branch \`fix/issue-7570-ueberdb2-drivers\`. See design spec: docs/superpowers/specs/2026-04-20-issue-7570-ueberdb2-drivers-design.md."
Expected: PR #7571 closed with comment.
Files: none — validation
Run:
cd /home/jose/etherpad/etherpad-lite
git status
Expected: On branch fix/issue-7570-ueberdb2-drivers.
Run:
cd /home/jose/etherpad/etherpad-lite
docker build --target production -t etherpad:pre-fix .
Expected: build succeeds.
Run:
docker run --rm etherpad:pre-fix node -e "try { require('mysql2'); console.log('HAS mysql2'); } catch(e) { console.log('MISSING mysql2:', e.message); }"
Expected output: MISSING mysql2: Cannot find module 'mysql2'.
This confirms the bug reproduces in the current prod image. Keep this evidence — the same command run in Task B5 Step 3 against the fixed image should print HAS mysql2.
src/package.jsonFiles:
Modify: /home/jose/etherpad/etherpad-lite/src/package.json
Step 1: Read current dependencies block
Open /home/jose/etherpad/etherpad-lite/src/package.json. Locate the dependencies object.
Within dependencies, add these keys (inserted in alphabetical position; do not touch any other key):
"@elastic/elasticsearch": "^9.3.4",
"cassandra-driver": "^4.8.0",
"mongodb": "^7.1.1",
"mssql": "^12.2.1",
"mysql2": "^3.22.0",
"nano": "^11.0.5",
"pg": "^8.20.0",
"redis": "^5.12.1",
"rethinkdb": "^2.4.2",
"surrealdb": "^2.0.3",
And change the existing ueberdb2 entry from its current value to:
"ueberdb2": "^5.0.46",
Do not touch any other dependency, devDependency, script, or metadata field. Specifically the following must remain unchanged vs origin/develop:
typescript, oidc-provider, eslint-config-etherpad, @types/express-session, resolve versions
repository.url
test-admin npm script and its project list
Every other field.
Step 3: Verify diff is minimal
Run:
cd /home/jose/etherpad/etherpad-lite
git diff src/package.json
Expected: exactly 11 lines added (10 new drivers + no other lines) and exactly 1 line modified (the ueberdb2 version bump). If anything else shows up, revert it.
pnpm-lock.yamlFiles:
Modify: /home/jose/etherpad/etherpad-lite/pnpm-lock.yaml
Step 1: Install
Run:
cd /home/jose/etherpad/etherpad-lite/src
pnpm install
Expected: install succeeds, resolves [email protected] plus the ten drivers. No missing-peer-dep warnings.
Run:
cd /home/jose/etherpad/etherpad-lite
git diff --stat pnpm-lock.yaml
Expected: only pnpm-lock.yaml modified. No unrelated package.json files touched.
Files: none — validation
Run:
cd /home/jose/etherpad/etherpad-lite
docker build --target production -t etherpad:post-fix .
Expected: build succeeds.
Run:
docker run --rm etherpad:post-fix node -e "
const mods = [
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
];
for (const m of mods) {
try { require(m); console.log('ok', m); }
catch (e) { console.error('MISSING', m, e.message); process.exit(1); }
}
"
Expected output: ten lines starting with ok , exit code 0.
Create /tmp/et7570-compose.yml:
services:
app:
image: etherpad:post-fix
depends_on:
- mariadb
environment:
NODE_ENV: production
ADMIN_PASSWORD: admin
DB_CHARSET: utf8mb4
DB_HOST: mariadb
DB_NAME: etherpad
DB_PASS: password
DB_PORT: 3306
DB_TYPE: mysql
DB_USER: user
DEFAULT_PAD_TEXT: "Test "
TRUST_PROXY: "true"
ports:
- "9001:9001"
mariadb:
image: mariadb:11.4
environment:
MYSQL_DATABASE: etherpad
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: root
MARIADB_AUTO_UPGRADE: 1
Run:
docker compose -f /tmp/et7570-compose.yml up -d
sleep 30
curl -sf http://localhost:9001/ >/dev/null && echo "OK: Etherpad serves / over MySQL"
docker compose -f /tmp/et7570-compose.yml logs app | grep -c "Cannot find module 'mysql2'" || true
docker compose -f /tmp/et7570-compose.yml down -v
Expected: OK: Etherpad serves / over MySQL, and the grep count is 0.
If curl fails, inspect docker compose logs app — startup may just be slow on the first run; wait 30 more seconds and retry curl. If the missing-module error still appears, Task B3/B4 were not applied cleanly.
build-test-db-drivers CI jobFiles:
Modify: /home/jose/etherpad/etherpad-lite/.github/workflows/docker.yml
Step 1: Read the current workflow end-to-end
Open /home/jose/etherpad/etherpad-lite/.github/workflows/docker.yml and identify:
jobs: key and the existing job(s) under it (likely docker and/or publish after PR #7569).env.TEST_TAG (expected: etherpad/etherpad:test).needs: on a publish job.Do not restructure existing jobs. Only append the new job and extend needs:.
Append the following job under jobs: (same indentation as existing jobs), placed after the existing build-test / docker job:
build-test-db-drivers:
runs-on: ubuntu-latest
permissions:
contents: read
services:
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: etherpad
MYSQL_USER: etherpad
MYSQL_PASSWORD: password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -ppassword"
--health-interval=5s
--health-timeout=5s
--health-retries=20
postgres:
image: postgres:16
env:
POSTGRES_DB: etherpad
POSTGRES_USER: etherpad
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U etherpad -d etherpad"
--health-interval=5s
--health-timeout=5s
--health-retries=20
steps:
- name: Check out
uses: actions/checkout@v6
with:
path: etherpad
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build production image
uses: docker/build-push-action@v7
with:
context: ./etherpad
target: production
load: true
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Driver presence test (all 10 drivers must resolve)
run: |
docker run --rm "$TEST_TAG" node -e "
const mods = [
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
];
let fail = false;
for (const m of mods) {
try { require(m); console.log('ok', m); }
catch (e) { console.error('MISSING', m, e.message); fail = true; }
}
if (fail) process.exit(1);
"
- name: MySQL smoke — start Etherpad against mysql service
run: |
docker run --rm -d \
--network host \
-e NODE_ENV=production \
-e ADMIN_PASSWORD=admin \
-e DB_TYPE=mysql \
-e DB_HOST=127.0.0.1 \
-e DB_PORT=3306 \
-e DB_NAME=etherpad \
-e DB_USER=etherpad \
-e DB_PASS=password \
-e DB_CHARSET=utf8mb4 \
-e DEFAULT_PAD_TEXT="Test " \
--name et-mysql "$TEST_TAG"
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
echo "mysql smoke: Etherpad is serving /"
docker rm -f et-mysql
exit 0
fi
sleep 2
done
echo "mysql smoke: timed out waiting for Etherpad"
docker logs et-mysql || true
docker rm -f et-mysql || true
exit 1
- name: Postgres smoke — start Etherpad against postgres service
run: |
docker run --rm -d \
--network host \
-e NODE_ENV=production \
-e ADMIN_PASSWORD=admin \
-e DB_TYPE=postgres \
-e DB_HOST=127.0.0.1 \
-e DB_PORT=5432 \
-e DB_NAME=etherpad \
-e DB_USER=etherpad \
-e DB_PASS=password \
-e DEFAULT_PAD_TEXT="Test " \
--name et-postgres "$TEST_TAG"
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
echo "postgres smoke: Etherpad is serving /"
docker rm -f et-postgres
exit 0
fi
sleep 2
done
echo "postgres smoke: timed out waiting for Etherpad"
docker logs et-postgres || true
docker rm -f et-postgres || true
exit 1
Notes:
$TEST_TAG comes from workflow-level env.TEST_TAG — no need to redeclare.
Port 9001 is Etherpad's default bind port; --network host lets us hit the service containers directly.
Stage order (presence → mysql → postgres) means the fastest, clearest failure mode runs first.
Step 3: Extend publish job's needs: to gate on this new job
Locate the publish job (added in PR #7569). Its needs: currently looks like either:
needs: build-test
or
needs: [build-test]
Change to:
needs: [build-test, build-test-db-drivers]
If no publish job exists yet (shouldn't happen post-#7569), the existing job that pushes images (probably containing docker/login-action) is the one to gate. Apply the same change to its needs:.
Run:
cd /home/jose/etherpad/etherpad-lite
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/docker.yml'))"
Expected: exit 0, no output.
docker.ymlRun:
git diff .github/workflows/docker.yml | head -50
git diff --stat .github/workflows/docker.yml
Expected: only additions (the new job + the needs: change). No other lines modified.
Files:
Stage: src/package.json, pnpm-lock.yaml, .github/workflows/docker.yml
Step 1: Verify final diff scope
Run:
cd /home/jose/etherpad/etherpad-lite
git status
git diff --stat
Expected (modified files only):
.github/workflows/docker.yml | 90+ additions
pnpm-lock.yaml | large change
src/package.json | 11 additions, 1 change
The spec commit from earlier (branch fix/issue-7570-ueberdb2-drivers) is already present from previous work — git log --oneline should show it as the tip.
Run:
cd /home/jose/etherpad/etherpad-lite
git add src/package.json pnpm-lock.yaml .github/workflows/docker.yml
git commit -m "$(cat <<'EOF'
fix(#7570): bundle DB drivers, add regression CI
- Bump ueberdb2 to ^5.0.46 (upstream now re-bundles drivers).
- Declare all 10 ueberdb2 DB drivers as direct src dependencies as a
defensive safety net against a future upstream drift.
- Add build-test-db-drivers CI job that blocks the publish job:
* all-10-drivers presence check in the built prod image
* end-to-end MySQL smoke (reproduces #7570)
* end-to-end Postgres smoke
Any stage failure blocks Docker Hub / GHCR publish.
EOF
)"
Expected: one commit on fix/issue-7570-ueberdb2-drivers.
Run:
cd /home/jose/etherpad/etherpad-lite
git remote -v | grep -q '^fork' || git remote add fork https://github.com/johnmclear/etherpad-lite.git
Expected: fork remote exists.
Run:
git push -u fork fix/issue-7570-ueberdb2-drivers
Expected: branch pushed to johnmclear/etherpad-lite.
Files: none
Run:
gh pr create \
--repo ether/etherpad-lite \
--base develop \
--head johnmclear:fix/issue-7570-ueberdb2-drivers \
--title "fix(#7570): bundle DB drivers, add regression CI" \
--body "$(cat <<'EOF'
## Summary
- Bumps `ueberdb2` to `^5.0.46` — upstream PR `<Task A5 PR URL>` restored drivers as real dependencies.
- Declares all 10 ueberdb2 DB drivers as direct `src/package.json` dependencies as a defensive safety net against any future upstream drift.
- Adds a new `build-test-db-drivers` CI job that blocks `publish`:
- presence test for all 10 drivers in the built production image
- MySQL service-container smoke (reproduces #7570)
- Postgres service-container smoke
- Supersedes #7571 (which mixed scope).
## Test plan
- [ ] `build-test` passes on this PR (existing coverage)
- [ ] `build-test-db-drivers` passes — specifically the MySQL stage is the live reproduction of #7570
- [ ] Local: `docker compose up` with reporter's MySQL config reaches healthy and serves `/`
EOF
)"
Expected: PR URL printed. Record it.
Files: none — validation + iteration
Run:
gh pr checks <PR number from B8> --repo ether/etherpad-lite --watch
Expected: all checks green. The two critical ones:
build-test (pre-existing)
build-test-db-drivers (new, with its four stages)
Step 2: If a stage fails, read the logs and fix
Run:
gh run view --log-failed --repo ether/etherpad-lite
Common failure modes and fixes:
src/package.json missing that driver. Re-check Task B3.--health-retries on the service, and push again. Do not skip the stage.Iterate until green. Per project rule, update PR title/description after every push and post /review as a comment to trigger Qodo re-review.
Once CI is green, notify the user the PR is ready for review. No further automated action.
Spec coverage:
ueberdb2 bump + driver safety-net list → B3, B4Placeholders: One deliberate placeholder remains (<default> for ueberDB's default branch name in A1/A4/A5, and <Task A5 PR URL> referenced from B1/B8). These are values the engineer fills in once observed from gh/git; they are not "TBD implementation details."
Consistency: build-test-db-drivers job name used identically in B6, B7 commit message, and B8 PR body. Driver list of ten appears identically in A3, B3, B5, and B6. Version 5.0.46 used consistently across A3, A5, A6, B3.