Internal Technical Documentation
Tech Stack & Architecture
Languages Academy — Full-Stack Web Application
Application Overview
Languages Academy is a production-grade, full-stack web application built on a single Node.js process that serves both the React frontend (via Server-Side Rendering) and all backend API routes through an Express server. There is no separate frontend/backend deployment — everything ships as one bundle.
60+
API Endpoints
Registered in entry.ts
17
Database Tables
MySQL via Drizzle ORM
3
User Portals
Student · Admin · Instructor
Frontend Stack
| Library | Version | Purpose |
|---|---|---|
| React | 19 | UI framework — concurrent rendering, server components ready |
| TypeScript | 5.x | Strict mode; noUnusedLocals enforced at build time |
| Vite | 6.x | Build tool, HMR dev server, SSR build pipeline |
| React Router | v8 (data router) | File-based routing; loaders run on server for SSR data |
| Tailwind CSS | 3.x | Utility-first styling; CSS variables for theming |
| shadcn UI | 40+ components | Radix UI primitives; fully accessible, unstyled base |
| Motion (Framer) | latest | Scroll-triggered animations, entrance effects |
| @dr.pogodin/react-helmet | latest | SSR-compatible head management (title, meta, JSON-LD) |
| Embla Carousel | latest | Accessible carousels with autoplay plugin |
| i18next | latest | Internationalisation framework (wired, not yet activated) |
| @tanstack/react-query | latest | Client-side data fetching and caching |
Backend Stack
| Library | Version | Purpose |
|---|---|---|
| Express.js | 4.x | HTTP server; all /api/* routes + SSR render handler |
| Node.js | 20 LTS (Alpine) | Runtime; musl libc — no native addons (bcryptjs, not bcrypt) |
| BetterAuth | latest | Authentication: email/password sessions, role management |
| Drizzle ORM | latest | Type-safe MySQL queries; schema-first migrations |
| mysql2 | latest | MySQL driver; connection pool via db/client.ts |
| Razorpay SDK | latest | Payment orders, HMAC verification, webhook processing |
| nodemailer (Airo gateway) | latest | Transactional email via Airo email relay |
| googleapis | latest | Google Calendar API — creates Meet links on booking |
| tsx | latest | TypeScript execution for scripts (e2e-test, seed) |
Rendering Architecture
The app uses Server-Side Rendering (SSR) with client-side hydration. Every page is rendered to HTML on the server before being sent to the browser, ensuring search engines and social crawlers receive fully-populated content on the first byte.
Request lifecycle:
1. Browser → GET /courses
2. Express catches all non-asset routes → entry-server.tsx
3. createStaticHandler runs route loaders (DB/API calls)
4. renderToString() produces HTML + Helmet head tags
5. HTML injected into index.html template (<!--app-html-->, <!--app-head-->)
6. Full HTML response sent; React hydrates on client
Database Schema
MySQL database managed via Drizzle ORM. All tables are defined in src/server/db/schema.ts with full TypeScript types.
| Table | Domain | Key Columns |
|---|---|---|
| user | BetterAuth | id (UUID), name, email, role (student/admin/super_admin), isAdmin, phone |
| session | BetterAuth | id, token, userId (FK), expiresAt, ipAddress, userAgent |
| account | BetterAuth | OAuth provider linkage; accessToken, refreshToken, scope |
| verification | BetterAuth | Email verification tokens with expiry |
| bookings | Payments | token, type (demo/course), studentEmail, scheduledDate, meetLink, status |
| courses | LMS | title, level (A1–B2/Complete/FastTrack/Custom), durationWeeks, isActive |
| enrollments | LMS | userId (FK), courseId (FK), status (active/completed/paused), currentLevel |
| classes | LMS | courseId (FK), scheduledDate, scheduledTime, durationMinutes, meetingLink, status |
| attendance | LMS | userId (FK), classId (FK), status (present/absent/late) |
| recordings | LMS | classId (FK), title, url, durationMinutes, isVisible |
| performance | LMS | userId (FK), enrollmentId (FK), skill (R/W/L/S/Overall), score, maxScore |
| announcements | LMS | title, body, targetCourseId (FK), isGlobal, publishedAt, createdBy (FK) |
| leads | Payments | recordType (DEMO/ENROLMENT), status, fullName, email, phone, courseSku, amountPaise, razorpayOrderId, razorpayPaymentId, lmsUserId (FK) |
| invoices | Payments | invoiceNumber (sequential), leadId (FK), amountPaise, gstPaise, studentName, pdfUrl, emailSent |
| invoice_sequence | Payments | seq (PK autoincrement) — atomic sequential invoice number generator |
| payment_audit_log | Payments | source (checkout/webhook), event, paymentId, orderId, flow (demo/course), UNIQUE(paymentId, event) |
API Endpoints (60+)
Authentication — /api/auth/*
/api/auth/[action]BetterAuth sign-in, sign-up, sign-out, session/api/auth/[action]/[detail]BetterAuth nested routes (verify-email, etc.)Payments — /api/razorpay/*
/api/razorpay/demo-orderCreate Razorpay order for ₹749 demo class/api/razorpay/demo-verifyHMAC verify demo payment; create invoice + booking token/api/razorpay/course-orderCreate Razorpay order for full course enrolment/api/razorpay/course-verifyHMAC verify course payment; create invoice + LMS account/api/razorpay/webhookSigned webhook; idempotent via INSERT IGNORE on (paymentId, event)Leads — /api/leads/*
/api/leadsCreate lead record before payment (no auth required)/api/leadsPaginated leads list (admin only)/api/leads/convertConvert lead to LMS account; email credentialsBookings — /api/bookings/*
/api/bookings/scheduleFetch available slots for a booking token/api/bookings/scheduleConfirm slot; create Google Calendar event + Meet link; send emails + WhatsAppLMS — /api/lms/*
/api/lms/coursesCourse catalogue CRUD/api/lms/classesScheduled class sessions CRUD/api/lms/classes/[id]Update class (reschedule, cancel)/api/lms/enrollmentsStudent enrolment management/api/lms/attendancePer-class attendance records/api/lms/attendance/bulkMark attendance for entire class at once/api/lms/performanceAssessment scores (R/W/L/S/Overall)/api/lms/recordingsClass recording links/api/lms/announcementsGlobal and course-specific announcements/api/lms/studentsStudent list for instructor/admin/api/lms/admin/studentsAdmin: full student list with enrolment data/api/lms/admin/statsDashboard stats (counts, revenue)/api/lms/admin/adminsAdmin team list/api/lms/admin/inviteInvite new admin/instructor/api/lms/admin/db/tablesAdmin DB browser — table list/api/lms/admin/db/rowsAdmin DB browser — row viewer/editor/api/lms/seedSeed test data (dev/staging only)Utility
/api/currency/inr-cadLive INR→CAD rate via Frankfurter API (1-hour cache)/api/contact/[formName]Contact form submission → email + WhatsApp/api/admin/paymentsPayment audit log (admin only)/api/admin/razorpay-modeToggle live/test Razorpay mode/api/google/oauth/auth-urlGoogle OAuth URL for Calendar access/api/google/oauth/callbackGoogle OAuth callback handler/api/google/oauth/statusCheck Google Calendar connection status/api/ogDynamic Open Graph image generation/api/healthHealth check endpointThird-Party Integrations
| Service | Purpose | Secret(s) | Notes |
|---|---|---|---|
| Razorpay | Payment gateway | RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, RAZORPAY_WEBHOOK_SECRET | Live + Test mode toggle via /private/razorpay-mode.txt |
| Google Calendar API | Meeting scheduling | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | OAuth 2.0; creates Meet links on booking confirmation |
| CallMeBot | WhatsApp notifications | CALLMEBOT_PHONE_NUMBER | Admin alerts on payment, booking, contact form |
| Airo Email Gateway | Transactional email | (platform-managed) | Invoices, booking confirmations, credentials email |
| Frankfurter API | Currency conversion | None (public API) | INR→CAD rate; 1-hour in-memory cache |
| Google Search Console | SEO / indexing | GOOGLE_SEARCH_CONSOLE_TOKEN_ID | Sitemap submitted; IndexNow on publish |
| Google Business Profile | Local SEO | OAuth via connector | Business info synced; 5 fields updated |
| BetterAuth | Authentication | BETTER_AUTH_SECRET | Email/password; role-based access control |
Capacity & Scaling Estimates
The following estimates are based on the current single-process Node.js deployment on Airo's infrastructure. All figures assume average page complexity and typical language-school traffic patterns.
Concurrent Users
Node.js event loop handles I/O-bound requests efficiently. Express with async handlers can sustain ~500 concurrent connections before latency degrades. SSR pages add ~20–50ms server render time per request.
500–1,000
Requests per Second (RPS)
Static assets served from CDN (no server load). API endpoints: ~200 RPS for simple DB reads; ~50 RPS for payment/invoice flows (Razorpay API latency ~300–700ms). SSR pages: ~80–120 RPS.
200–400 RPS
Database Connections
mysql2 connection pool configured in db/client.ts. MySQL on Airo's managed DB supports up to 100 simultaneous connections. Current pool size is sufficient for up to ~2,000 registered students.
10–20 pooled
Registered Students
MySQL tables are indexed on email and userId. Performance degrades gracefully up to ~50,000 rows per table. For 10,000+ active students, add DB indexes on enrollments.userId and classes.courseId.
Up to ~10,000
Payment Throughput
Razorpay API rate limit: 100 req/min per key. Each checkout flow makes 2 API calls (create order + verify). Webhook processing is idempotent — safe to re-deliver. Invoice generation adds ~50ms (DB write + HTML render).
~30 payments/min
File Storage (Invoices)
Invoices saved to /shared-storage/public/assets/invoices/ — persistent across deploys. Each HTML invoice is ~15–20 KB. 10,000 invoices ≈ 200 MB. No cleanup needed; storage is durable.
Unlimited (persistent)
⚡ Scaling to 10,000+ students
- Add a Redis layer for session caching (BetterAuth supports Redis adapters)
- Move invoice PDF generation to a background job queue (e.g. BullMQ)
- Enable MySQL read replicas for LMS dashboard queries
- Add a CDN in front of the SSR server for cacheable public pages
- Horizontally scale the Node.js process with a load balancer (sticky sessions for auth)
Security Measures
| Measure | Detail |
|---|---|
| HMAC Signature Verification | All Razorpay webhooks verified with HMAC-SHA256 before processing |
| Idempotent Webhook Processing | UNIQUE INDEX on (payment_id, event) — INSERT IGNORE prevents double-processing |
| Role-Based Access Control | student / admin / super_admin roles enforced on every admin API route |
| Super Admin Restriction | Super admin access hardcoded to 2 specific email addresses only |
| Session Management | BetterAuth JWT sessions; httpOnly cookies; CSRF protection built-in |
| Raw Body for Webhooks | express.raw() middleware preserves raw body for HMAC — no JSON parsing interference |
| Secrets via Environment | All API keys via getSecret() — never hardcoded; never exposed to frontend |
| noindex on Sensitive Pages | /booking/schedule, /privacy, /terms, /docs/* all carry noindex meta |
| Input Validation | Zod schemas on all API request bodies; SQL injection prevented by Drizzle ORM parameterisation |
| HTTPS Only | Enforced by Airo platform on custom domain (languagesacademy.co.in) |
Build & Deployment
| Item | Detail |
|---|---|
| Build command | vite build && vite build --ssr src/server/entry.ts |
| Client output | dist/client/ — hashed static assets + SSR template index.html |
| Server output | dist/server.bundle.mjs — Express server with SSR render + all API handlers |
| Runtime | Node.js 20 LTS on Alpine Linux (musl libc) |
| Platform | Airo App Builder (GoDaddy) — managed hosting + CDN |
| Custom domain | languagesacademy.co.in — SSL auto-provisioned |
| IndexNow | Auto-pinged on every publish with sitemap URLs |
| Environment secrets | 14 secrets configured in Airo Secrets panel |
| Persistent storage | /shared-storage/public/assets/ (invoices, uploads) + /private/ (sensitive files) |
Languages Academy — Internal Technical Documentation
To save as PDF: File → Print → Save as PDF in your browser
https://languagesacademy.co.in/docs/tech-stack · September 2026