DEV Community

EMMANUEL UMEH
EMMANUEL UMEH

Posted on

Two HNG Tasks That Taught Me More Than the Spec: OAuth for Three Clients, and Shipping AI on a Team Deadline

Two HNG Tasks That Taught Me More Than the Spec

This is my Stage 9B write-up for the HNG internship. No new code just two tasks that stuck: one I owned solo across multiple repos, and one I shipped inside a team product under real deadline pressure.

If you've ever had auth work almost done for three days straight, or watched an LLM politely ignore your JSON schema, you'll recognize these stories.


Task 1 (Individual): Insighta Labs — One API, Three Clients, One Auth System

Stage: 3 (Technical Requirements Document / TRD track)

Why I picked it: Auth looked "done" on paper. It wasn't. Web portal, CLI, and graders all needed to log in differently, and every environment (localhost, Railway, preview URLs) found a new way to break.

What it was

Insighta Labs is a queryable profile-intelligence API I built during HNG. By Stage 3 the backend wasn't just CRUD anymore it needed GitHub OAuth with PKCE, JWT access + refresh with rotation, RBAC (admin vs analyst), rate limits, API versioning, and three first-class clients:

Client Repo How it authenticates
Backend API HNG_STAGE-1 Issues tokens, sets cookies
Web portal Insighta-WebPortal HTTP-only cookies + CSRF
CLI Insighta-Cli PKCE + local callback + Bearer tokens

Every /api/* route required X-API-Version: 1 and a valid session. Access tokens expired in 3 minutes; refresh tokens in 5 minutes with rotation. That sounds harsh, it was intentional, and it surfaced bugs fast.

The problem it was solving

Reviewers and real users had to prove identity without sharing one login mechanism. Browsers should never see raw tokens in JavaScript. The CLI can't use cookie redirects the same way a React app does. Automated graders needed a test path that didn't depend on GitHub's OAuth exchange.

One auth design. Three runtimes. Zero "works on my machine only."

How I approached it

I split auth into explicit paths instead of one generic "login" handler:

Web flow

  1. GET /auth/github — server stores PKCE verifier, redirects to GitHub
  2. GET /auth/github/callback — exchanges code, sets HTTP-only cookies, redirects to OAUTH_SUCCESS_REDIRECT
  3. GET /auth/csrf-token — double-submit CSRF for unsafe methods
  4. Portal sends X-CSRF-Token on POST / DELETE when using cookies

CLI flow

  1. insighta login starts PKCE with code_challenge + local http://127.0.0.1:<port>/callback
  2. POST /auth/github/token completes the exchange and returns JSON tokens
  3. CLI stores credentials at ~/.insighta/credentials.json and refreshes before expiry

Grader / test path

  • Optional stub exchange for test_code so automated checks could get real JWTs without hitting GitHub

Middleware order mattered: rate limits on /auth, JWT validation on /api, CSRF only when the request used cookies (Bearer-only CLI calls skip CSRF).

I documented every env var in the README — WEB_ORIGIN, GITHUB_WEB_REDIRECT_URI, OAUTH_SUCCESS_REDIRECT, TRUST_PROXY_HOPS — because auth failures in production are almost always configuration, not logic.

What broke (and how I fixed it)

1. OAuth callback URL pointed at the portal, not the API

Symptom: after GitHub login, Cannot GET /auth/github/... or a blank error page.

Cause: the GitHub OAuth app's callback was set to the web portal hostname instead of the API callback (https://<api-host>/auth/github/callback).

Fix: one canonical callback on the API. Portal is only the post-login redirect (OAUTH_SUCCESS_REDIRECT), never the OAuth callback itself. Obvious in hindsight; painful at 1 a.m.

2. CORS + credentials = silent failure

Symptom: portal login succeeded, then every /api/* call failed in the browser with no useful UI error.

Cause: credentials: true requires an exact Access-Control-Allow-Origin match. A missing preview URL in WEB_ORIGIN, or a trailing slash mismatch, blocks the preflight.

Fix: comma-separated origins in WEB_ORIGIN, no trailing slashes, and echo Origin on auth routes when graders hit unexpected hosts.

3. CSRF on cookie sessions

Symptom: GET worked after login; DELETE /api/profiles/:id returned 403.

Cause: cookie sessions need CSRF; the portal wasn't fetching and sending X-CSRF-Token.

Fix: portal calls GET /auth/csrf-token after auth responses and attaches the header on unsafe methods. CLI unchanged — Bearer only.

4. CLI vs web PKCE divergence

Symptom: CLI login hung or GitHub returned redirect_uri mismatch.

Cause: web stores code_verifier server-side; CLI must pass code_challenge, state, and register http://127.0.0.1:8765/callback in the GitHub app.

Fix: treat them as two documented flows in one controller, not one code path with flags sprinkled everywhere.

5. Rate limits saw the proxy, not the user

Symptom: /auth throttled everyone equally on Railway.

Fix: TRUST_PROXY_HOPS=1 so Express reads the real client IP from X-Forwarded-For.

What I took away

  • Auth is a product surface. If three clients can't log in reliably, the API doesn't ship — no matter how clean your controllers are.
  • Write the failure modes down. Wrong callback URL, missing CSRF, CORS typos — I added these to the README so the next person (or grader) doesn't rediscover them.
  • Short token TTL is a feature. Painful locally; invaluable for catching refresh bugs before users do.

Why this task?

It took longer than the feature list suggested because "implement OAuth" on a ticket is not the same as "three clients can authenticate in production." That's the kind of gap HNG is good at exposing — and the kind of work I want on a backend résumé.

Repos: Backend · CLI · Web Portal


Task 2 (Team): Flowbrand / SEIL — Marketing Service with AI Under a Shared Deadline

Stage: Team product track (SEIL / Flowbrand)

Why I picked it: Solo tasks teach depth. Team tasks teach contracts, communication, and what happens when your dependency is an LLM that doesn't read the spec.

What it was

SEIL is a guided marketing-strategy product for small businesses where you tell it what you sell, get a step-by-step plan instead of guessing on Instagram. The wider team worked across a larger Flowbrand codebase; my deliverable was the marketing microservice: a NestJS API that accepts business documents and returns a structured funnel.

Stack: NestJS 11, TypeORM, PostgreSQL, JWT, pdf-parse / mammoth for text extraction, Anthropic Claude for generation, Swagger at /api/v1/docs, Docker Compose for local Postgres + API.

Core flow:

  1. POST /api/v1/auth/register or login → JWT
  2. POST /api/v1/funnels/upload → PDF/DOCX (≤ 5 MiB), extract text, return uploadId + status
  3. GET /api/v1/funnels/upload/progress/:uploadId → poll until ready
  4. POST /api/v1/funnels/generate-from-upload → Claude returns awareness → engagement → conversion → retention
  5. Persist result in PostgreSQL

The problem it was solving

Small business owners often have context in documents — pitch decks, one-pagers, notes — but not a marketing framework. The team needed a backend-owned pipeline: upload → extract → generate → store, with a fixed JSON shape the frontend could render without parsing prose.

My job was to make that pipeline boringly predictable for teammates consuming the API.

How I approached it

Contract-first. Swagger became the handshake with frontend and reviewers. Global ValidationPipe (whitelist, forbid unknown fields) so bad payloads fail early with clear 400s, not mysterious 500s.

Upload before AI. Text extraction runs in the upload handler; generation refuses uploads that aren't ready. That split kept slow AI calls out of multipart handling and gave the UI a poll endpoint.

Schema enforcement at the boundary. Claude fills four string slots — nothing else. The product defines the funnel; the model doesn't get to invent nested JSON or markdown essays.

Deploy like someone else will run it. Render for the API, env vars documented in README, TYPEORM_SYNC=false + migrations mindset for anything beyond a throwaway demo.

Team-wise, I treated the marketing service as a bounded context: my repo, my deploy, my docs — but aligned with SEIL's product language and the live site the rest of the squad was building toward.

What broke (and how we fixed it)

1. Claude returned JSON inside markdown fences

Symptom: 502 Bad Gateway, logs showing Unexpected token ''`.

Cause: despite the system prompt saying "raw JSON only," the model sometimes wrapped output in

json ...

.

Fix: coerceJsonFromModelText() strips fences before JSON.parse, then parseMarketingFunnelResult() validates exactly four non-empty strings: awareness, engagement, conversion, retention. Unit tests in funnel-ai.service.spec.ts for parsing edge cases.

2. Generate called before upload was ready

Symptom: frontend showed a spinner forever or users hit generate immediately after upload.

Fix: explicit UPLOAD_NOT_READY (422) with uploadId and status in the error body. Frontend could branch on that instead of guessing.

3. Missing ANTHROPIC_API_KEY in staging

Symptom: opaque failures in demo environment.

Fix: dedicated 503 with code AI_NOT_CONFIGURED — reviewers and teammates know it's env, not logic.

4. Team integration friction — "what's the base URL?"

Symptom: duplicate bug reports about CORS or 404s that were really wrong VITE_API_URL.

Fix: README section for portal dev (VITE_API_URL = origin only, no path) and production Render vars. Swagger "Try it out" with PUBLIC_URL set correctly.

5. Ephemeral disk on Render for uploads

Symptom: uploads worked until redeploy; then uploadId existed but file didn't.

Lesson documented: UPLOAD_STORAGE_ROOT on ephemeral hosts is fine for demos; production needs persistent storage or object storage. We scoped MVP honestly rather than pretending the filesystem was durable.

What I took away

  • On a team, your API errors are UX. Structured codes (UPLOAD_NOT_READY, AI_NOT_CONFIGURED) save Slack threads.
  • LLMs need a bouncer at the door. Prompts aren't contracts — validation is.
  • Own the slice end-to-end. I didn't merge every SEIL module into one repo; I shipped a service with docs, tests, and a live URL teammates could hit. That's how parallel work actually finishes.

Why this task?

Insighta taught me to survive my own complexity. SEIL taught me to make complexity survivable for other people — frontend, reviewers, teammates — while an AI sits in the middle of the pipeline. That's closer to how real companies ship.

Repo: github.com/Nuel-09/Flowbrand-marketing-service

Swagger: flowbrand-marketing-service.onrender.com/api/v1/docs


Closing thought

HNG doesn't reward the prettiest controller. It rewards systems that hold up when auth, CORS, CSRF, proxies, teammates, and LLMs all show up on the same Tuesday.

Top comments (0)