What it was
Stage 3 was the most ambitious individual task in the internship. The goal was to build a complete authentication system across three separate applications that all shared one PostgreSQL database:
A backend API — handling GitHub OAuth, JWT tokens, and role-based access control
A CLI tool — so developers could log in from their terminal and query profiles
A web portal — a server-side rendered dashboard for the same data
Three repos. One database. One deadline.
The problem it was solving
The system needed to let users log in with their GitHub account, get a short-lived access token, and interact with profile data depending on their role — admin could do everything, analyst could only read.
The CLI had an extra layer: it had to complete an OAuth flow in a browser but hand the result back to a terminal. That's not a typical web login — it required implementing PKCE (Proof Key for Code Exchange), a security mechanism that prevents auth code interception.
How I approached it
I started with the backend — OAuth first, then tokens, then RBAC. The backend generated JWT access tokens with a 3-minute expiry and opaque refresh tokens with single-use rotation. Every time a refresh token was used, a new one was issued and the old one was invalidated.
For the CLI, I wrote a local HTTP server that spun up on port 9876 to catch the OAuth callback. The user's browser completed the GitHub login, GitHub redirected to localhost:9876, the CLI captured the code, exchanged it for tokens, stored them at ~/.insighta/credentials.json, and shut itself down.
The web portal was EJS — server-side rendered pages that called the backend on every request.
What broke
The cross-domain cookie problem. The backend was setting cookies on its own domain after OAuth. The web portal lived on a different domain. Browsers blocked those cookies. The login worked but the session never persisted.
The fix took longer than I expected: instead of the backend setting cookies directly, I made it redirect to the frontend with tokens as query parameters — ?at=...&rt=.... The frontend's /auth/callback route received them and set the cookies on its own domain. Two lines of redirect logic saved the entire auth flow.
There were smaller things too — upsertUser was inserting users as inactive, so returning users couldn't log in after their first session. Fixed with ON CONFLICT DO UPDATE SET is_active = true. The ejs package was missing from package.json. UUID import was using v4 instead of v7.
What I took away
OAuth sounds simple until you implement it. The PKCE flow for the CLI taught me that browser-based auth and terminal-based auth have fundamentally different security models. The cross-domain cookie problem taught me that knowing where your code runs matters as much as what it does.
Why I picked it
Because it broke the most times. Every fix revealed a new problem. By the end I had touched OAuth, JWTs, refresh token rotation, RBAC, CLI tooling, and server-side rendering — in one stage.
Task 2 — Personal Trainer Booking Lifecycle (Team Task)
What it was
The team built a backend system for managing personal training sessions, trainers, clients, bookings, and subscriptions. The stack was Go with the Gin framework and PostgreSQL, structured in clean layers: handlers, services, repositories.
My role was the discovery call feature — the initial consultation endpoint that starts the relationship between a trainer and a new client before any session is booked.
The problem it was solving
A client can't just book a session with a trainer without context. The discovery call is the first touchpoint, a short consultation where the trainer understands the client's goals before committing to a program. The system needed to track this lifecycle: discovery call → booking → session → optional cancellation.
How I approached it
I worked within the team's existing architecture. The project used a strict layered pattern no business logic in handlers, no database calls in services. Every feature followed the same structure. I mapped out the discovery call flow, wrote the database migration, added the repository methods, wired up the service layer, and exposed the handler.
One thing the team enforced strictly: atomic database transactions. Any operation that touched multiple tables bookings, availability slots, subscription credits had to succeed or fail together. No partial updates.
What broke
The cancellation feature (which I also contributed to) had a subtle bug in the refund window calculation. The policy was: full refund if cancelled more than 12 hours before the session, no refund within 12 hours. The initial implementation calculated the window from the time of cancellation request, not from the scheduled session start time. A client cancelling 13 hours before a session that was scheduled 1 hour from now would incorrectly get a full refund.
Fixing it meant recalculating relative to scheduled_start_time, not NOW(). One line change, but it required understanding the business rule precisely first.
What I took away
Working in Go after Node.js was a shift. Go's type system catches things JavaScript lets you ignore. The team's clean architecture meant I could add a feature without touching anything I shouldn't — which also meant I couldn't take shortcuts. The refund bug taught me that business logic has to be specified clearly before writing a single line. "12 hours before" is ambiguous. "12 hours before the session start time" is not.
Why I picked it
Because it was a real team codebase, not a solo project. Reading someone else's architecture, following conventions you didn't set, and getting reviews from teammates is a different kind of hard than building something alone.
Top comments (0)