feat: full stack modernization — Docker, dependency upgrades, Express→Next.js migration#8
Merged
dpuscher merged 32 commits intoproductionfrom Feb 20, 2026
Merged
Conversation
Add GitHub Actions workflows for staging and production branches that build and push Docker images to GHCR, then trigger Dokploy redeployment via webhook. Also ignore .mcp.json.
- Remove deprecated --production flag from yarn install (Yarn 4) - Drop redundant prod-deps stage; reuse deps stage in runner - Set NODE_OPTIONS=--openssl-legacy-provider for webpack 4 / OpenSSL 3 compatibility
Dokploy server runs on ARM64; add QEMU and Buildx setup steps and set platforms: linux/amd64,linux/arm64 so the manifest covers both architectures.
- ESLint 6→9: migrate to flat config format (eslint.config.js), replace babel-eslint with @babel/eslint-parser, add @eslint/eslintrc FlatCompat - Jest 24→29: add jest-environment-jsdom, update transform pattern for ts/tsx - Stylelint 12→16: drop abandoned styled-components processor, new config file - Add TypeScript: typescript, @types/node, @types/react, @types/react-dom, tsconfig.json with allowJs:true for incremental migration, next-env.d.ts - Remove babel-plugin-transform-class-properties from .babelrc (in next/babel)
- Remove sw-precache-webpack-plugin: strip webpack plugin from next.config.js and the entry point manipulation for offline/polyfills; service worker support can be re-added later with next-pwa - Remove react-lazy: replace styled(Lazy) with styled.div in QueryRelease.styles.js since IntersectionObserver is now native in all modern browsers - Remove object-dig: replace dig() calls with optional chaining (?.) in config/passport.js and app/discogs.js - Remove intersection-observer polyfill from lib/polyfills.js (native now) - Remove redux-devtools-extension: replace with @redux-devtools/extension (drop-in replacement); update import in client/reduxStore.js Updated safe packages (no API changes): - dotenv 8→16, compression latest, cookie-parser latest, disconnect 1.2.2, lodash latest, morgan latest, prop-types latest, react-icons 3→4, react-timeago 4→8, styled-icons 9→10
Redis v4+ rewrote the API to be promise-based; v5 follows the same interface:
- createClient() no longer auto-connects; must call client.connect() explicitly
- All methods return Promises natively (no more util.promisify needed)
- client.set() now accepts an options object ({EX: ttl}) instead of
positional 'EX', ttl arguments
Changes:
- app/cache.js: rewrite to use promise-based API directly
- config/express.js: update RedisStore initialization for connect-redis v9
(named export, new RedisStore({ client }) instead of factory function)
- app/__mocks__/redis.js: update manual mock to match v5 promise-based API
- app/spec/cache.spec.js: update assertions for new set() options format
Mongoose 6 removed deprecated connection options; Mongoose 7 removed callbacks;
Mongoose 8 finalizes async-first API.
- server.js: remove deprecated useNewUrlParser/useUnifiedTopology options and
mongoose.set('useCreateIndex', true) — all are now defaults
- config/passport.js: convert deserializeUser and strategy callback from
callback-based to async/await (callbacks removed in Mongoose 7)
- app/routes/api/scrobble.js: convert Release.findOne callback to async/await
- app/routes/api/user/history.js: convert Release.find callback to async/await
- app/routes/api/user/autoscrobbles.js: convert Release.find callback to
async/await; replace deprecated Model.update() with Model.updateOne()
Express 5 (released 2024) adds native async error propagation and stricter route path parsing (wildcards require explicit patterns). - express 4→5, express-session 1.17→1.19, helmet 3→8 - Remove body-parser package: use built-in express.json() instead - config/express.js: replace bodyParser.json() with express.json(), update Helmet to disable CSP for now (to be configured at Next.js layer), Helmet v8 has stricter defaults and a new API for some options - server.js: replace '*' wildcard (invalid in Express 5) with /(.*)/ regex
- react/react-dom 16→18, react-test-renderer 16→18 - react-redux 7→9 - next-redux-wrapper 4→8: convert _app.js from class component HOC to function component using useWrappedStore hook; export wrapper from client/reduxStore.js via createWrapper() - styled-components 5→6: add babel-plugin-styled-components as explicit devDependency (no longer bundled in v6) - redux 4→5, redux-thunk 2→3: named export (thunk vs default export); update import in reduxStore.js and all test files - Remove enzyme + enzyme-adapter-react-16 (not compatible with React 18); replace with @testing-library/react; clean up setupTests.js - Add @testing-library/dom peer dependency
Next.js 14 uses webpack 5 natively (supports OpenSSL 3), React 18, and the Pages Router (stable). Key changes: - Remove NODE_OPTIONS=--openssl-legacy-provider from Dockerfile and docker-compose.yml (no longer needed with webpack 5 + OpenSSL 3) - next.config.js: add reactStrictMode:true, remove legacy options, add JSDoc type annotation for IntelliSense - next/link API changes (Next.js 13+): <Link> now renders as <a> directly; add legacyBehavior prop where child is a styled.a or styled.button to prevent nested <a> elements; remove redundant inner <a> in CircleLayout - _document.js: add null check for ctx.req (may be absent in static pages) - @next/bundle-analyzer updated to v14 to match Next.js version - Add @babel/runtime-corejs2 devDep (jest-fetch-mock compiled with it; previously provided transitively by next@9)
Rename all .js/.jsx files to .ts/.tsx across pages, components, lib, styles, config, and client. Add TypeScript type annotations to resolve compiler errors: generic props for styled-components, React.Component<any,any> for class components, (Class as any).propTypes for external static assignments, and module augmentation for styled-components css prop. - Add types/styled-components.d.ts for css prop support - Update tsconfig.json to exclude spec files (handled by babel-jest) - Add @types/jest for test infrastructure types - Fix styled-icons import paths (named exports from package root) - Make Spinner size prop optional to support styled extension without size
Replace the custom Express server routing layer with Next.js API routes. Add iron-session for encrypted-cookie session management (replaces Redis-backed express-session + Passport). Redis is retained for Discogs/Last.fm API response caching. New API routes mirror the old Express routes: - pages/api/session.ts - pages/api/barcode/[id].ts - pages/api/search/[query].ts - pages/api/scrobble.ts - pages/api/user/history.ts - pages/api/user/autoscrobbles.ts - pages/api/auth/lastfm.ts (redirect to Last.fm OAuth) - pages/api/auth/callback/lastfm.ts (exchange token, create session) - pages/api/auth/logout.ts (destroy session) Infrastructure helpers: lib/session.ts, lib/mongodb.ts, lib/withAuth.ts Update /auth/lastfm and /logout links to /api/auth/lastfm and /api/auth/logout. Update profile page SSR to read session via iron-session.
Delete server.js, config/express.js, config/passport.js, all Express route handlers (app/routes/), and Express middleware (app/middlewares/). Remove dependencies: express, express-session, compression, morgan, cookie-parser, passport, passport-lastfm, helmet, connect-redis. Update package.json scripts to use next dev / next start. Update Dockerfile CMD to node_modules/.bin/next start. The app now runs entirely on Next.js: API routes handle all server-side logic, iron-session handles auth, Redis handles Discogs/LastFM caching, and mongoose models remain in app/models/.
- Remove defaultProps from Logo and LogoSmall (use JS default params) - Use transient prop $visible in Camera styled component to prevent DOM forwarding - Move viewport meta tag from _document.tsx to _app.tsx (Next.js 14 requirement)
Use mongoose.models.User/Release guard so models aren't re-registered on hot module replacement, which was causing 500 errors on /api/session.
- fetchSession now checks response.ok and dispatches setErrorState on non-2xx responses instead of dispatching the error JSON as session data - Session component redirects to /login when error state is set - Add Next.js middleware to redirect cookie-less requests to /login before the page renders, matching the old Express auth guard behavior
The old Express server rewrote /detected/:barcode and /scrobbled/:barcode to ?barcode= query params. Without it, direct navigation 404s. Convert to proper Next.js dynamic routes pages/detected/[barcode].tsx and pages/scrobbled/[barcode].tsx and update Router.push calls accordingly.
next-redux-wrapper v8's useWrappedStore expects the full _app props
({ pageProps, router, ... }) so it can reconstruct props.pageProps.
Passing only pageProps caused props.pageProps to be undefined, silently
dropping all getInitialProps results (e.g. barcode in detected/[barcode]).
Remove all prop-types usage across 21 components and pages. Replace with named TypeScript interfaces (ComponentNameProps pattern) and default values in parameter destructuring. Remove prop-types package.
…rops Replace deprecated getInitialProps with getServerSideProps using next-redux-wrapper for proper server-side Redux store hydration.
The detected page was converted from query-param routing (pages/detected.tsx) to a dynamic route (pages/detected/[barcode].tsx). Update the Link href to match the new route structure so history item clicks don't 404.
Initial state has data: null. REMOVE_AUTO_SCROBBLE could be dispatched before RECEIVED_AUTO_SCROBBLES arrives, causing null.filter() to throw.
…e actions 4xx/5xx responses resolve successfully with error bodies, which were being dispatched as valid data. This caused .map() to be called on an error object, crashing the app. Now throws on non-ok responses so the error handler runs.
Error objects have non-enumerable properties; JSON.stringify({err}) produces {}.
Use err.message string instead, consistent with other routes using { error: '...' }.
Typo caused misleading names in React DevTools and stack traces.
Same issue as ProfileHistoryItem — the detected page was converted to a dynamic route but the old query-param href was left in QueryRelease.
- Pass actual Discogs error to reject() instead of calling reject() with no argument, so the error propagates correctly through the promise chain - Fix error serialization in barcode API handler (same issue as history.ts) - Add response.ok check in releaseActions before parsing JSON
React 18 StrictMode double-invokes componentDidMount, causing two concurrent requests for the same barcode. Both findOne calls return null before either saves, leading to two createFromDiscogs calls and an E11000 duplicate key error. Catch the duplicate key error (code 11000) and retry the findOne to return the record that was created by the winning request.
777a0e0 to
c0f0ef2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full modernization of the stack: Docker setup, CI/CD, major dependency upgrades across the board, migration from a custom Express server to Next.js API routes, TypeScript adoption, and a round of bug fixes to stabilize the migrated app.
Docker & CI/CD
public/--productionflag, drop redundant stage)Dependency Upgrades
Removed abandonware:
sw-precache-webpack-plugin,react-lazy,object-dig,intersection-observer,redux-devtools-extension(replaced with@redux-devtools/extension).Express → Next.js Migration
pages/api/)express-session+ Passport withiron-session(encrypted cookie sessions)server.js,config/express.js,config/passport.js, and allapp/routes/+app/middlewares//logindetectedandscrobbledpages to dynamic routes (pages/detected/[barcode].tsx,pages/scrobbled/[barcode].tsx)TypeScript Migration
.js/.jsxfiles to.ts/.tsxacross pages, components, lib, styles, config, and clienttsconfig.jsonwithallowJs: truefor incremental migrationtypes/styled-components.d.tsforcssprop supportpropTypes/defaultPropswith TypeScript interfaces across all 21 components and pageseslint.config.js)Bug Fixes
mongoose.models.X ||guard so models aren't re-registered on hot reloadfetchSessionnow checksresponse.ok; Session component redirects to/loginon error; middleware guards all authenticated pagesuseWrappedStoreprops — pass full_appprops (not justpageProps) sonext-redux-wrapperv8 can reconstructprops.pagePropscorrectlydefaultPropsfrom Logo/LogoSmall, use transient prop$visiblein Camera, move viewport meta to_app.tsxProfileHistoryItemandQueryReleasehad stale query-paramhrefafter the dynamic route migration; clicks produced 404sautoScrobbleReducercalled.filter()onstate.data(initiallynull) before data arrivedhistoryActions,autoScrobbleActions, andreleaseActionsdispatched error response bodies as valid data (missingresponse.okchecks), causing.map is not a functioncrashes{}—history.tsandbarcode/[id].tsused{ err }in JSON responses;Errorproperties are non-enumerablegetReleasecalledreject()with no argument; actual Discogs errors were invisiblecomponentDidMount, sending two concurrent requests for the same barcode before either saves; fixed by catching E11000 infirstOrCreateand retryingfindOneProfileHistorys→ProfileHistorygetInitialPropstogetServerSidePropswithnext-redux-wrapperTest plan
yarn devstarts without errors/profiledirectly → redirects to/login/detected/<barcode>, release info loads/detected/id:<id>without 404 or duplicate key error/scrobbled/<barcode>/profileare clickable and navigate correctly