<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: EMMANUEL UMEH</title>
    <description>The latest articles on DEV Community by EMMANUEL UMEH (@nuel99).</description>
    <link>https://dev.to/nuel99</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3709236%2F4e9ea96d-8ef2-4a3f-8303-2c3b8775331e.jpg</url>
      <title>DEV Community: EMMANUEL UMEH</title>
      <link>https://dev.to/nuel99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nuel99"/>
    <language>en</language>
    <item>
      <title>Two HNG Tasks That Taught Me More Than the Spec: OAuth for Three Clients, and Shipping AI on a Team Deadline</title>
      <dc:creator>EMMANUEL UMEH</dc:creator>
      <pubDate>Fri, 12 Jun 2026 15:59:04 +0000</pubDate>
      <link>https://dev.to/nuel99/two-hng-tasks-that-taught-me-more-than-the-spec-oauth-for-three-clients-and-shipping-ai-on-a-team-50cl</link>
      <guid>https://dev.to/nuel99/two-hng-tasks-that-taught-me-more-than-the-spec-oauth-for-three-clients-and-shipping-ai-on-a-team-50cl</guid>
      <description>&lt;h1&gt;
  
  
  Two HNG Tasks That Taught Me More Than the Spec
&lt;/h1&gt;

&lt;p&gt;This is my Stage 9B write-up for the &lt;a href="https://hng.tech/" rel="noopener noreferrer"&gt;HNG internship&lt;/a&gt;. 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.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Task 1 (Individual): Insighta Labs — One API, Three Clients, One Auth System
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stage:&lt;/strong&gt; 3 (Technical Requirements Document / TRD track)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why I picked it:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it was
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/Nuel-09/HNG_STAGE-1" rel="noopener noreferrer"&gt;Insighta Labs&lt;/a&gt; is a queryable profile-intelligence API I built during HNG. By Stage 3 the backend wasn't just CRUD anymore it needed &lt;strong&gt;GitHub OAuth with PKCE&lt;/strong&gt;, &lt;strong&gt;JWT access + refresh with rotation&lt;/strong&gt;, &lt;strong&gt;RBAC&lt;/strong&gt; (&lt;code&gt;admin&lt;/code&gt; vs &lt;code&gt;analyst&lt;/code&gt;), rate limits, API versioning, and &lt;strong&gt;three first-class clients&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;How it authenticates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend API&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Nuel-09/HNG_STAGE-1" rel="noopener noreferrer"&gt;HNG_STAGE-1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Issues tokens, sets cookies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web portal&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Nuel-09/Insighta-WebPortal" rel="noopener noreferrer"&gt;Insighta-WebPortal&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;HTTP-only cookies + CSRF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Nuel-09/Insighta-Cli" rel="noopener noreferrer"&gt;Insighta-Cli&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;PKCE + local callback + Bearer tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h3&gt;
  
  
  The problem it was solving
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;One auth design. Three runtimes. Zero "works on my machine only."&lt;/p&gt;

&lt;h3&gt;
  
  
  How I approached it
&lt;/h3&gt;

&lt;p&gt;I split auth into explicit paths instead of one generic "login" handler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web flow&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GET /auth/github&lt;/code&gt; — server stores PKCE verifier, redirects to GitHub
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /auth/github/callback&lt;/code&gt; — exchanges code, sets HTTP-only cookies, redirects to &lt;code&gt;OAUTH_SUCCESS_REDIRECT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /auth/csrf-token&lt;/code&gt; — double-submit CSRF for unsafe methods
&lt;/li&gt;
&lt;li&gt;Portal sends &lt;code&gt;X-CSRF-Token&lt;/code&gt; on &lt;code&gt;POST&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt; when using cookies
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;CLI flow&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;insighta login&lt;/code&gt; starts PKCE with &lt;code&gt;code_challenge&lt;/code&gt; + local &lt;code&gt;http://127.0.0.1:&amp;lt;port&amp;gt;/callback&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /auth/github/token&lt;/code&gt; completes the exchange and returns JSON tokens
&lt;/li&gt;
&lt;li&gt;CLI stores credentials at &lt;code&gt;~/.insighta/credentials.json&lt;/code&gt; and refreshes before expiry
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Grader / test path&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optional stub exchange for &lt;code&gt;test_code&lt;/code&gt; so automated checks could get real JWTs without hitting GitHub
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I documented every env var in the README — &lt;code&gt;WEB_ORIGIN&lt;/code&gt;, &lt;code&gt;GITHUB_WEB_REDIRECT_URI&lt;/code&gt;, &lt;code&gt;OAUTH_SUCCESS_REDIRECT&lt;/code&gt;, &lt;code&gt;TRUST_PROXY_HOPS&lt;/code&gt; — because auth failures in production are almost always configuration, not logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke (and how I fixed it)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. OAuth callback URL pointed at the portal, not the API&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: after GitHub login, &lt;code&gt;Cannot GET /auth/github/...&lt;/code&gt; or a blank error page.&lt;/p&gt;

&lt;p&gt;Cause: the GitHub OAuth app's callback was set to the &lt;strong&gt;web portal hostname&lt;/strong&gt; instead of the &lt;strong&gt;API&lt;/strong&gt; callback (&lt;code&gt;https://&amp;lt;api-host&amp;gt;/auth/github/callback&lt;/code&gt;).&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;2. CORS + credentials = silent failure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: portal login succeeded, then every &lt;code&gt;/api/*&lt;/code&gt; call failed in the browser with no useful UI error.&lt;/p&gt;

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

&lt;p&gt;Fix: comma-separated origins in &lt;code&gt;WEB_ORIGIN&lt;/code&gt;, no trailing slashes, and echo &lt;code&gt;Origin&lt;/code&gt; on auth routes when graders hit unexpected hosts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. CSRF on cookie sessions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: &lt;code&gt;GET&lt;/code&gt; worked after login; &lt;code&gt;DELETE /api/profiles/:id&lt;/code&gt; returned 403.&lt;/p&gt;

&lt;p&gt;Cause: cookie sessions need CSRF; the portal wasn't fetching and sending &lt;code&gt;X-CSRF-Token&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;4. CLI vs web PKCE divergence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: CLI login hung or GitHub returned &lt;code&gt;redirect_uri mismatch&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Fix: treat them as two documented flows in one controller, not one code path with flags sprinkled everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Rate limits saw the proxy, not the user&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: &lt;code&gt;/auth&lt;/code&gt; throttled everyone equally on Railway.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;TRUST_PROXY_HOPS=1&lt;/code&gt; so Express reads the real client IP from &lt;code&gt;X-Forwarded-For&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Why this task?
&lt;/h3&gt;

&lt;p&gt;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é.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repos:&lt;/strong&gt; &lt;a href="https://github.com/Nuel-09/HNG_STAGE-1" rel="noopener noreferrer"&gt;Backend&lt;/a&gt; · &lt;a href="https://github.com/Nuel-09/Insighta-Cli" rel="noopener noreferrer"&gt;CLI&lt;/a&gt; · &lt;a href="https://github.com/Nuel-09/Insighta-WebPortal" rel="noopener noreferrer"&gt;Web Portal&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Task 2 (Team): Flowbrand / SEIL — Marketing Service with AI Under a Shared Deadline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stage:&lt;/strong&gt; Team product track (SEIL / Flowbrand)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why I picked it:&lt;/strong&gt; Solo tasks teach depth. Team tasks teach &lt;strong&gt;contracts, communication, and what happens when your dependency is an LLM that doesn't read the spec.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it was
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://flowbrand.hng14.com/" rel="noopener noreferrer"&gt;SEIL&lt;/a&gt; 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; &lt;strong&gt;my deliverable was the marketing microservice&lt;/strong&gt;: a NestJS API that accepts business documents and returns a structured funnel.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;My service&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Nuel-09/Flowbrand-marketing-service" rel="noopener noreferrer"&gt;Flowbrand-marketing-service&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live Swagger&lt;/td&gt;
&lt;td&gt;&lt;a href="https://flowbrand-marketing-service.onrender.com/api/v1/docs" rel="noopener noreferrer"&gt;flowbrand-marketing-service.onrender.com/api/v1/docs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product context&lt;/td&gt;
&lt;td&gt;&lt;a href="https://flowbrand.hng14.com/" rel="noopener noreferrer"&gt;flowbrand.hng14.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Core flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/auth/register&lt;/code&gt; or &lt;code&gt;login&lt;/code&gt; → JWT
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/funnels/upload&lt;/code&gt; → PDF/DOCX (≤ 5 MiB), extract text, return &lt;code&gt;uploadId&lt;/code&gt; + status
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v1/funnels/upload/progress/:uploadId&lt;/code&gt; → poll until &lt;code&gt;ready&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/funnels/generate-from-upload&lt;/code&gt; → Claude returns &lt;strong&gt;awareness → engagement → conversion → retention&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Persist result in PostgreSQL
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The problem it was solving
&lt;/h3&gt;

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

&lt;p&gt;My job was to make that pipeline boringly predictable for teammates consuming the API.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I approached it
&lt;/h3&gt;

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

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

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

&lt;p&gt;&lt;strong&gt;Deploy like someone else will run it.&lt;/strong&gt; Render for the API, env vars documented in README, &lt;code&gt;TYPEORM_SYNC=false&lt;/code&gt; + migrations mindset for anything beyond a throwaway demo.&lt;/p&gt;

&lt;p&gt;Team-wise, I treated the marketing service as a &lt;strong&gt;bounded context&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke (and how we fixed it)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Claude returned JSON inside markdown fences&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: &lt;code&gt;502 Bad Gateway&lt;/code&gt;, logs showing &lt;code&gt;Unexpected token '&lt;/code&gt;'`.&lt;/p&gt;

&lt;p&gt;Cause: despite the system prompt saying "raw JSON only," the model sometimes wrapped output in &lt;code&gt;&lt;br&gt;
&lt;br&gt;
&lt;/code&gt;&lt;code&gt;json ...&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&lt;br&gt;
 &lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;2. Generate called before upload was &lt;code&gt;ready&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: frontend showed a spinner forever or users hit generate immediately after upload.&lt;/p&gt;

&lt;p&gt;Fix: explicit &lt;code&gt;UPLOAD_NOT_READY&lt;/code&gt; (422) with &lt;code&gt;uploadId&lt;/code&gt; and &lt;code&gt;status&lt;/code&gt; in the error body. Frontend could branch on that instead of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Missing &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; in staging&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: opaque failures in demo environment.&lt;/p&gt;

&lt;p&gt;Fix: dedicated &lt;code&gt;503&lt;/code&gt; with code &lt;code&gt;AI_NOT_CONFIGURED&lt;/code&gt; — reviewers and teammates know it's env, not logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Team integration friction — "what's the base URL?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: duplicate bug reports about CORS or 404s that were really wrong &lt;code&gt;VITE_API_URL&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;5. Ephemeral disk on Render for uploads&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symptom: uploads worked until redeploy; then &lt;code&gt;uploadId&lt;/code&gt; existed but file didn't.&lt;/p&gt;

&lt;p&gt;Lesson documented: &lt;code&gt;UPLOAD_STORAGE_ROOT&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On a team, your API errors are UX.&lt;/strong&gt; Structured codes (&lt;code&gt;UPLOAD_NOT_READY&lt;/code&gt;, &lt;code&gt;AI_NOT_CONFIGURED&lt;/code&gt;) save Slack threads.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLMs need a bouncer at the door.&lt;/strong&gt; Prompts aren't contracts — validation is.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own the slice end-to-end.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why this task?
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Nuel-09/Flowbrand-marketing-service" rel="noopener noreferrer"&gt;github.com/Nuel-09/Flowbrand-marketing-service&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Swagger:&lt;/strong&gt; &lt;a href="https://flowbrand-marketing-service.onrender.com/api/v1/docs" rel="noopener noreferrer"&gt;flowbrand-marketing-service.onrender.com/api/v1/docs&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

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

</description>
      <category>hng</category>
      <category>backend</category>
      <category>oauth</category>
      <category>nestjs</category>
    </item>
  </channel>
</rss>
