DEV Community
Grade 7
10h ago
Two Tasks That Broke Me (And What I Took From It)
A backend engineering internship retrospective I'm not going to write one of those blog posts where everything goes smoothly and the developer learns a neat lesson at the end. That's not what happened. This is about two tasks from my internship that were genuinely hard — one individual, one team — and the honest version of how they went. Part 1 — Stage 3: Build Insighta Labs+ What it was Stage 3 was the biggest individual task I'd faced up to that point. The assignment: take the demographic intelligence platform from Stage 2 — filtering, sorting, pagination, natural language search, all of it — and turn it into something real users could actually log into. Three separate repos. One backend. GitHub OAuth with PKCE, JWT access and refresh tokens, role-based access control (admin vs analyst), a globally installable CLI tool, and a web portal with HTTP-only cookies and CSRF protection. Break anything from Stage 2 and it counts against you. The problem it was solving The platform had no auth at all. Anyone could hit any endpoint. No ownership, no access control, no sessions. Stage 3's job was to fix that — not just slap a login button on it, but build a system where the CLI and the web portal could both authenticate against the same backend through completely different flows, and still behave consistently. How I approached it — the actual build I started with the backend and worked outward. The first thing I had to figure out was that the CLI and web portal OAuth flows are fundamentally different, even though they both use GitHub. The web portal flow is what most people think of as OAuth — user clicks a button, gets redirected to GitHub, GitHub redirects back to the backend callback, backend sets HTTP-only cookies, done. The tokens never touch JavaScript because they live in cookies the browser sends automatically. The CLI flow is weirder. There's no browser session to set a cookie on, so I had to think about this differently. What I ended up doing: when you run insighta login , the CLI generates a state , a code_verifier , and a code_challenge using PKCE, then spins up a temporary local HTTP server on port 9876, then opens your browser to the GitHub OAuth page. GitHub redirects to localhost:9876/callback with a code . The CLI's local server catches that, validates the state, sends the code and verifier to the backend's POST /auth/cli/callback , and the backend exchanges it with GitHub and returns tokens. The CLI saves those to ~/.insighta/credentials.json and shuts down the local server. The whole temporary server exists for maybe three seconds. const server = http . createServer ( async ( req , res ) => { const url = new URL ( req . url , ' http://localhost:9876 ' ) const code = url . searchParams . get ( ' code ' ) const returnedState = url . searchParams . get ( ' state ' ) if ( returnedState !== state ) { /* reject */ } // Send code + verifier to backend, get tokens back const response = await api . post ( ' /auth/cli/callback ' , { code , code_verifier : codeVerifier }) saveCredentials ( response . data ) server . close () }) server . listen ( 9876 , () => { // Open browser to GitHub OAuth open ( authUrl ) }) Once auth was working, I built the token layer. Access tokens expire in 10 minutes, refresh tokens in 15. Every time a refresh token is used, it gets deleted from the database immediately and a new pair is issued — token rotation. On logout, the refresh token is deleted server-side so even if someone had the token string it would be rejected. For RBAC I kept it structured rather than scattered. Instead of checking roles inside individual controllers, I put it in the middleware chain at the router level: requireApiVersion → authenticate → apiLimiter → requireRole (admin-only routes) → controller authenticate verifies the JWT and attaches req.user . requireRole checks req.user.role . Clean, predictable, easy to audit. The web portal I built in plain HTML/CSS/JS — no framework. The api.js fetch wrapper intercepts 401s automatically, tries a token refresh, and retries the original request. If the refresh also fails, it redirects to login. That meant every page just calls checkAuth() on load and never has to think about token expiry. For Redis I added caching on getAllProfiles and searchProfiles with a 60-second TTL. One thing that tripped me up: two queries with the same intent but different word order — "young males from nigeria" vs "males from nigeria who are young" — would produce different cache keys because the raw query string was being used. I wrote normalizeFilters.js to sort the parsed filter keys alphabetically before generating the cache key, so equivalent queries always hit the same cache entry. I also replaced the two-query pattern (one for data, one for COUNT(*) ) with a single COUNT(*) OVER() window function query — one round trip to the database instead of two. What broke — the 20/60 moment Then the automated grader ran. 20 out of 60. I stared at that number for a while. The failures were
A backend engineering internship retrospective I'm not going to write one of those blog posts where everything goes smoothly and the developer learns a neat lesson at the end. That's not what happened. This is about two tasks from my internship that were genuinely hard — one individual, one team — and the honest version of how they went. Part 1 — Stage 3: Build Insighta Labs+ What it was Stage 3 was the biggest individual task I'd faced up to that point. The assignment: take the demographic intelligence platform from Stage 2 — filtering, sorting, pagination, natural language search, all of it — and turn it into something real users could actually log into. Three separate repos. One backend. GitHub OAuth with PKCE, JWT access and refresh tokens, role-based access control (admin vs analyst), a globally installable CLI tool, and a web portal with HTTP-only cookies and CSRF protection. Break anything from Stage 2 and it counts against you. The problem it was solving The platform had no auth at all. Anyone could hit any endpoint. No ownership, no access control, no sessions. Stage 3's job was to fix that — not just slap a login button on it, but build a system where the CLI and the web portal could both authenticate against the same backend through completely different flows, and still behave consistently. How I approached it — the actual build I started with the backend and worked outward. The first thing I had to figure out was that the CLI and web portal OAuth flows are fundamentally different, even though they both use GitHub. The web portal flow is what most people think of as OAuth — user clicks a button, gets redirected to GitHub, GitHub redirects back to the backend callback, backend sets HTTP-only cookies, done. The tokens never touch JavaScript because they live in cookies the browser sends automatically. The CLI flow is weirder. There's no browser session to set a cookie on, so I had to think about this differently. What I ended up doing: when you run insighta login , the CLI generates a state , a code_verifier , and a code_challenge using PKCE, then spins up a temporary local HTTP server on port 9876, then opens your browser to the GitHub OAuth page. GitHub redirects to localhost:9876/callback with a code . The CLI's local server catches that, validates the state, sends the code and verifier to the backend's POST /auth/cli/callback , and the backend exchanges it with GitHub and returns tokens. The CLI saves those to ~/.insighta/credentials.json and shuts down the local server. The whole temporary server exists for maybe three seconds. const server = http.createServer(async (req, res) => { const url = new URL(req.url, 'http://localhost:9876') const code = url.searchParams.get('code') const returnedState = url.searchParams.get('state') if (returnedState !== state) { /* reject */ } // Send code + verifier to backend, get tokens back const response = await api.post('/auth/cli/callback', { code, code_verifier: codeVerifier }) saveCredentials(response.data) server.close() }) server.listen(9876, () => { // Open browser to GitHub OAuth open(authUrl) }) Once auth was working, I built the token layer. Access tokens expire in 10 minutes, refresh tokens in 15. Every time a refresh token is used, it gets deleted from the database immediately and a new pair is issued — token rotation. On logout, the refresh token is deleted server-side so even if someone had the token string it would be rejected. For RBAC I kept it structured rather than scattered. Instead of checking roles inside individual controllers, I put it in the middleware chain at the router level: requireApiVersion → authenticate → apiLimiter → requireRole (admin-only routes) → controller authenticate verifies the JWT and attaches req.user . requireRole checks req.user.role . Clean, predictable, easy to audit. The web portal I built in plain HTML/CSS/JS — no framework. The api.js fetch wrapper intercepts 401s automatically, tries a token refresh, and retries the original request. If the refresh also fails, it redirects to login. That meant every page just calls checkAuth() on load and never has to think about token expiry. For Redis I added caching on getAllProfiles and searchProfiles with a 60-second TTL. One thing that tripped me up: two queries with the same intent but different word order — "young males from nigeria" vs "males from nigeria who are young" — would produce different cache keys because the raw query string was being used. I wrote normalizeFilters.js to sort the parsed filter keys alphabetically before generating the cache key, so equivalent queries always hit the same cache entry. I also replaced the two-query pattern (one for data, one for COUNT(*) ) with a single COUNT(*) OVER() window function query — one round trip to the database instead of two. What broke — the 20/60 moment Then the automated grader ran. 20 out of 60. I stared at that number for a while. The failures were across almost every category: auth flow, role enforcement, token lifecycle, rate limiting. I had a defense/interview session coming up too, so I had to debug fast and understand what I'd built well enough to explain it live. Here's what I found when I actually dug in: The test_code ordering bug. The grader hits GET /auth/github/callback directly with code=test_code to retrieve tokens without going through a real OAuth flow. My githubCallback function was validating state before checking for test_code , which meant the grader's request was getting rejected before it even reached the shortcut. The state it sent was never registered in my in-memory store because it never went through /auth/github first. Fix: move the test_code check before state validation, with a comment explaining why. // test_code shortcut BEFORE state validation — grader hits this directly // without going through /auth/github, so state is never in the store if (code === 'test_code') { // return tokens for first active admin user } // Only then validate state for real OAuth flow if (!validateAndConsumeState(state)) { /* reject */ } The wrong JWT secret. In utils/token.js , both verifyAccessToken and verifyRefreshToken were using process.env.JWT_SECRET . The refresh token was being signed with JWT_SECRET and also verified with JWT_SECRET . That's the same secret — so it technically "worked" in the sense that tokens could be verified, but the whole point of having JWT_REFRESH_SECRET as a separate secret is security isolation. The grader was probably checking for the correct env variable usage. One line fix, but it required actually reading the token utility file carefully instead of assuming it was fine. The missing /api/users/me alias. The grader expected GET /api/users/me to exist. I had GET /auth/me — same handler, different path. Added an alias in index.js : app.get('/api/users/me', authenticate, getMe) The CLI auto-refresh that never refreshed. This one was the most embarrassing. In utils/api.js in the CLI, the refresh interceptor had: if (credentials?.refresh_token) { clearCredentials() console.error('Session expired. Please run: insighta login') process.exit(1) } See it? If a refresh_token exists — meaning the user is logged in — it clears credentials and exits. The logic is completely backwards. It should be if (!credentials?.refresh_token) — only exit if there's no refresh token. The try/catch below that was supposed to do the actual refresh was never reached. The entire auto-refresh feature was dead and it looked completely fine at a glance. What I took from it Automated graders are ruthless. They don't care that your code almost works. If the endpoint path is wrong or the check runs in the wrong order, you get zero for that category. It's a forcing function for being genuinely precise, not just approximately correct. The JWT secret bug and the missing alias are the kind of things you'd only catch by reading the actual code methodically, not by testing the happy path. The CLI bug is even worse — it's logic that's inverted, so it compiles fine, runs fine, and silently does the opposite of what you intended. The defense session is what actually made the whole thing click though. Having to explain the OAuth flow from memory, trace exactly why the grader was failing, describe what each middleware does in sequence — that's what moved my understanding from "I built this" to "I understand this." I knew my project better after it broke than I did when I submitted it. Why I picked it Because 20/60 is humbling in a way a passing score never is. And because fixing it taught me more than building it did. Part 2 — Seil (Team Project) What it was Seil is a NestJS + TypeScript backend for a marketing platform. User auth, account management, onboarding wizards, funnel generation, file uploads. The stack is TypeORM + PostgreSQL on Supabase, Redis via Upstash, Bull queues for background jobs, JWT, and Resend for email. This wasn't a solo project — there were several of us: sage-ali, thaArcadeGuy, elijaharhinful, and others, all working in parallel on different tickets against the same dev branch. That context matters for almost everything difficult that happened. The problem it was solving A real marketing platform backend. Not a tutorial project, not a demo — a production codebase with PR reviews, CI pipelines, and a team lead who would actually push back on your code. How I approached it I joined the repo, read the existing code before touching anything, picked up tickets, and started building. My contributions ranged from POST /auth/register to GET /onboarding/session to PATCH /users/me/password to eventually writing the entire test suite solo. What broke — where do I even start Before I wrote a single line of code, the repo was compromised. Someone with write access had embedded an obfuscated malicious payload at the bottom of eslint.config.js . It wasn't obvious — it was hidden using the JavaScript comma operator to make scanning harder, designed to execute on npm install , npm run lint , or any git com
Comments
No comments yet. Start the discussion.