Chaindustry
Senior Full-Stack Engineer
Web3 engagement platform · Since 2024 · Live
Chaindustry runs across four deployed surfaces: a REST API, a user client, an internal admin console, and a marketing site. It serves live users and moves real money on chain. I inherited the codebase from earlier developers, led a team of five through a rebuild of most of the application, and migrated the bulk of it to TypeScript. For the last five months I have been the only engineer on it, working across the full stack. I designed the role and permission model the platform authorises against, and the academy engine, its largest module, is where most of my backend work sits.
The problem
The platform pays strangers for verifiable work, so most of its difficulty is in not paying them wrongly. Withdrawals, address blocklists, transaction records, and a permission system deciding which staff member may approve what are the parts of the codebase that carry real risk.
Architecture
- Client
- Two Next.js frontends on different generations: the admin console on 13 with the Pages Router, 66 pages against 228 hand-rolled components; the user client on 16 with the App Router and TypeScript, 137 routes. Tailwind throughout, SCSS modules in the older one.
- State
- Redux Toolkit with RTK Query. 268 endpoints in the admin console alone, with tag-based invalidation across more than 40 tag types.
- API
- REST, 639 handlers split across a legacy JavaScript v1 surface and a TypeScript v2 surface that are mounted side by side. Integrations for Paystack, Twitter, Firebase push, Resend, S3, and on-chain disbursement through ethers.
- Data
- MongoDB with Mongoose, 82 models across three locations. No migration framework; schema changes run as unordered one-shot backfill scripts.
- Auth
- HttpOnly cookie sessions with a CSRF double-submit token; Bearer headers were removed outright. A money route passes six middleware layers, ending at a 96-permission RBAC gate across 18 namespaces and 6 staff roles.
- Infra
- cPanel over FTP with standalone Next builds, PM2 running the API and a worker, Bull over Redis, 11 background workers, Sentry, and Socket.IO. CI deploys only; no workflow runs tests or lint.
Decisions
Parallel 401s were logging users out of every device
The backend rotates refresh tokens single-use and treats a replayed one as theft, revoking every session. An admin page fires many requests at once, so when the access token expired each 401 independently posted to the refresh endpoint: one won and the rest replayed a spent token. Concurrent callers now await a single in-flight refresh promise rather than starting their own, with a loop guard for a retry that also fails. The cost is mutable module state inside Redux middleware, invisible to devtools, and a reset that is a timing heuristic rather than a guarantee. It only holds because the backend added a matching grace path, so correctness depends on a contract in another repository.
A role string cannot express who may approve a payout
Six staff roles across 18 namespaces, where some actions are irreversible: approving an on-chain payout, or blocklisting an address in a way that blocks every user from withdrawing to it. Each permission is its own document carrying a description, group, and impact level, referenced by roles and overridable per user, with the impact level driving type-to-confirm dialogs as data rather than UI code. Enforcement is middleware, and the frontend gate is explicitly a mirror of it. The price is an extra populated lookup on every gated request and a three-step deploy to add a permission.
Migrating a client that could not stop serving
The client was inherited JavaScript, running against a platform already live with real balances, so converting it in place meant refactoring under production traffic. It was rebuilt instead as a separate application, App Router and TypeScript throughout, with five engineers working into it while the original kept serving and the replacement caught up feature by feature. The cost is a long stretch with two clients to reason about and a backend that has to satisfy both, and 137 routes to reach before the original can be retired.
Outcomes
- Admin withdrawal processing shipped end to end, from the client mutation through a six-stage guard chain to on-chain submission, with the notification path deliberately non-throwing so an email outage cannot roll back a completed payout
- Bearer-header auth replaced with HttpOnly cookie sessions and CSRF double-submit, cookies namespaced per app so the console and the user client cannot share a session
- Concurrent-refresh race eliminated, removing an all-device logout that fired whenever a token expired mid-page
- User client rebuilt across 137 routes on the App Router, TypeScript throughout, replacing the JavaScript original
- Academy engine, the platform's largest module: cohort scheduling, gated progression, server-timed assessments, certification, and commitment payments settled across four currencies
Built with
- Next.js
- React
- TypeScript
- Redux Toolkit
- RTK Query
- Tailwind CSS
- Node.js
- Express
- MongoDB
- Mongoose
- Redis
- Bull
- Socket.IO
- ethers
- AWS S3
- Playwright
Screens



