This document explains how the full platform works in production and how the main request flows move through the code.
interview-bank is the main product users browse and submit to.
- The
ui/folder is a Vite React app deployed to Vercel. - The
service/folder is a Spring Boot API that can be deployed to Render. - The API stores company and interview data in Postgres.
- Submission uses a contributor token issued by the separate
token-generatorrepo.
One important architectural detail:
interview-bank does not call the token-generator backend when a user submits an experience. It validates the JWT locally using the shared JWT_SECRET, expected issuer, expected audience, and one-time jti.
flowchart LR
U[User Browser]
subgraph Vercel
IUI[Interview Bank UI]
TUI[Token Generator UI]
end
subgraph Render
IAPI[Interview Bank Service]
TAPI[Token Generator Service]
PG[(Postgres)]
RD[(Redis)]
end
RESEND[Resend API]
U --> IUI
U --> TUI
IUI -->|/api/v1/* rewrite| IAPI
TUI -->|/api/v1/token/* rewrite| TAPI
IAPI --> PG
TAPI --> RD
TAPI --> RESEND
| Repo | Main job | Persistent store |
|---|---|---|
interview-bank |
Browse companies, read experiences, predict question categories, accept verified submissions | Postgres |
token-generator |
Validate email domains, send OTPs, verify OTPs, issue signed JWTs | Redis for OTP state |
ui/src/App.tsx: route shellui/src/pages/HomePage.tsx: homepage company browsingui/src/pages/CompanyPage.tsx: company details, experiences, prediction panelui/src/pages/SubmitPage.tsx: token paste + submission wizardui/src/services/api.ts: Axios client and UI-to-API callsui/vercel.json: Vercel rewrite to the deployed API
service/src/main/java/com/interviewbank/controller/ApiController.java: REST entry pointsservice/src/main/java/com/interviewbank/service/CompanyService.java: company list/detail logicservice/src/main/java/com/interviewbank/service/ExperienceService.java: read/write experience flowservice/src/main/java/com/interviewbank/service/PredictionService.java: probability estimationservice/src/main/java/com/interviewbank/service/TopicClassifierService.java: keyword-based taggingservice/src/main/java/com/interviewbank/security/ContributorTokenValidator.java: local JWT verificationservice/src/main/resources/application.yml: ports, profiles, JWT, DB, CORS
When a user opens the homepage:
- The browser loads the Vercel-hosted React app.
ui/src/pages/HomePage.tsxuses React Query to callgetTrendingCompanies()orgetCompanies().ui/src/services/api.tssends the request to/api/v1/....- Vercel rewrites
/api/v1/*to the deployedinterview-bankservice. ApiControllerreceives the request.CompanyServicereads fromCompanyRepositoryandExperienceRepository.- Postgres returns the data.
- Spring maps entities to DTOs and returns JSON.
- React Query caches the response and renders the company cards.
sequenceDiagram
participant B as Browser
participant UI as Interview Bank UI
participant V as Vercel Rewrite
participant API as ApiController
participant CS as CompanyService
participant DB as Postgres
B->>UI: Open /
UI->>V: GET /api/v1/companies/trending
V->>API: Forward to backend service
API->>CS: getTrending()
CS->>DB: Query top companies
DB-->>CS: Rows
CS-->>API: DTO list
API-->>UI: JSON response
UI-->>B: Render cards
When a user opens /companies/:slug:
CompanyPage.tsxrequests:GET /api/v1/companies/{slug}GET /api/v1/companies/{slug}/experiences
PredictionPanelseparately requests:GET /api/v1/companies/{slug}/predict?role=...
PredictionServicepulls historic question counts from Postgres.- It applies Laplace smoothing and returns ranked category probabilities plus top topics.
Prediction is local to interview-bank; no external ML service is called.
This starts in the other repo, but it matters because interview-bank depends on the output.
- The user clicks the token-generator link from
SubmitPage.tsx. - The token-generator UI loads
?app=interview-bank. - It fetches client metadata from
GET /api/v1/token/client/interview-bank. - The user types an email.
- The UI performs instant client-side blocking for personal/disposable domains.
- On blur or submit, the UI calls
POST /api/v1/token/validate-email. - The token-generator service runs
EmailValidationService:- personal provider block
- disposable provider block
- MX lookup
- When the user requests an OTP,
OtpServicestores the OTP and attempt counter in Redis. EmailServicesends the OTP email via Resend.- When the user verifies the OTP,
TokenIssuerServicecreates a signed JWT with:
sub = emailiss = interview-bank-token-generatoraud = interview-bankjti = UUID
This is the most important end-to-end flow.
sequenceDiagram
participant B as Browser
participant UI as SubmitPage
participant V as Vercel Rewrite
participant API as ApiController
participant ES as ExperienceService
participant TV as ContributorTokenValidator
participant TC as TopicClassifierService
participant DB as Postgres
B->>UI: Submit form with X-Contributor-Token
UI->>V: POST /api/v1/experiences
V->>API: Forward to backend service
API->>ES: submitExperience(request, token)
ES->>TV: validateAndExtract(token)
TV-->>ES: claims(sub, aud, iss, jti, exp)
ES->>DB: Check existsByTokenJti(jti)
DB-->>ES: false
ES->>DB: Find company by slug
DB-->>ES: company
loop each submitted question
ES->>TC: classify(text) / extractTopic(text)
TC-->>ES: category + topic
end
ES->>DB: Save experience + questions
DB-->>ES: saved entity
ES-->>API: ExperienceResponse
API-->>UI: 201 Created
UI-->>B: Navigate to /experiences/{id}
service/src/main/java/com/interviewbank/service/ExperienceService.java
- Validate the JWT locally with
ContributorTokenValidator. - Extract the submitter email and token
jti. - Reject the request if that
jtialready exists in Postgres. - Resolve the target company by slug.
- Build the
InterviewExperienceentity. - For each question:
- use the provided category if one exists
- otherwise run
TopicClassifierService.classify(...) - infer a topic with
extractTopic(...)
- Save the experience and questions in one transaction.
- Return the created experience DTO.
At submit time, interview-bank only needs the token string.
It checks the token locally using:
- the shared
JWT_SECRET - expected issuer
- expected audience
- expiration
- one-time
jtireuse protection
That is why token-generator can be thought of as an issuing service, while interview-bank is the consuming and enforcing service.
- User opens the Interview Bank site.
- Home page fetches trending or searched companies.
- User clicks a company.
- Company page fetches:
- company detail
- approved experiences
- prediction data
- User reads public interview experiences.
- User opens
/submit. - UI asks for a contributor token.
- User opens token-generator in a new tab with
?app=interview-bank. - Token-generator loads the
interview-bankclient config. - User enters a company email.
- Token-generator validates the domain.
- User requests OTP.
- Token-generator stores OTP in Redis and sends it through Resend.
- User enters the OTP.
- Token-generator verifies the OTP and returns a JWT.
- User pastes the JWT into Interview Bank.
- User fills in company, role, round details, and questions.
- Interview Bank verifies the JWT locally and checks one-time use.
- Experience and questions are saved in Postgres.
- User is redirected to the newly created experience page.
GETendpoints are public.POST /api/v1/experiencesis allowed through Spring Security, but the real contributor check happens insideExperienceService.- Trending companies and prediction responses are cached.
- Production routing is:
- Vercel for
ui/ - Render for
service/ - Postgres for persistence
- Vercel for