Technical Documentation

This document describes the technical internals of EduNest LMS — a complete Learning Management System built as a single Next.js 16 App Router application with React 19, TypeScript, Tailwind CSS 3, and serverless PostgreSQL (Neon). It is written for developers extending or maintaining the platform and is grounded in the actual codebase (route handlers, the self-healing schema layer, the core/product split, and the auth/storage helpers).

Architecture

Unlike a split client/server stack, EduNest is one deployable Next.js application. The same project renders the public website, the admin panel, the learner portal, and the instructor portal, and also exposes the HTTP API through route handlers (route.ts files under src/app/api). There is no separate API server, no ORM, and no message broker.

Layer Implementation
UI + Pages Next.js App Router (server & client components) under src/app
API Route handlers (route.ts) under src/app/api/v1/*256 handlers
Data access Raw SQL via tagged templates (src/lib/db.ts) over @neondatabase/serverless
Schema Self-healing ensure*() functions (src/lib/*-schema.ts) — no migration tool
Auth Custom JWT with jose (admin) + jsonwebtoken/jose (learner/instructor)
Storage Cloudflare R2 / S3-compatible via the AWS S3 SDK (src/lib/r2.ts)
Secrets AES-256-GCM at rest (src/lib/secrets.ts)
Extensibility Core/product layer split (src/core + src/product) with 93 built-in add-ons

Route Groups

src/app is organised into App Router route groups (parenthesised folders do not appear in the URL):

Group / segment URL Audience
(site) /, /courses, /blogs, /checkout, … Public storefront + website
(auth) /login, /register, /forgot-password, … Learner/instructor account auth
admin/(panel) /admin/* Admin dashboard (behind admin login)
learner /learner/* Enrolled-learner portal
instructor /instructor/* Instructor portal
learn /learn/[slug] Course player (lesson viewer)
install /install First-run setup wizard
api /api/v1/*, /api/install, /api/license HTTP API (route handlers)
docs, api-docs, files /docs, /api-docs, /files In-app docs, OpenAPI viewer, file proxy

Request Lifecycle

A typical API request flows like this:

TEXT
Browser (site / admin / learner / instructor)
   │  fetch  /api/v1/<resource>
   ▼
Next.js Route Handler  (src/app/api/v1/<resource>/route.ts)
   ├─ ensure<Table>()                 # idempotent CREATE TABLE IF NOT EXISTS
   ├─ auth check                      # getSession() (admin) / getCustomerId() (portal)
   ├─ requirePermission(...)          # admin routes only, per-route RBAC
   ├─ business logic + raw SQL        # sql`SELECT … FROM …`  (src/lib/db.ts)
   └─ NextResponse.json(...)          # JSON response
   ▼
PostgreSQL (Neon, serverless driver over HTTPS)
   +  Cloudflare R2 (uploads)  +  License server  +  Integration providers

Because each handler calls its ensure*() schema function before querying, a fresh database never throws relation does not exist — the schema heals itself on first use.

Cross-cutting Concerns

  • Schema healing — every table is defined by an ensure*() function that runs CREATE TABLE IF NOT EXISTS plus ALTER TABLE … ADD COLUMN IF NOT EXISTS for later additions. An in-memory guard runs each one only once per server process.
  • Secrets — third-party credentials (SMTP, payment keys, storage keys) are stored encrypted with AES-256-GCM (encryptSecret / decryptJson in src/lib/secrets.ts) and never returned to the client in plaintext.
  • Storage — uploads go to Cloudflare R2 (or any S3-compatible bucket) through the AWS S3 SDK; images can be resized with sharp before upload.
  • Public IDs — admin-facing entities get a non-sequential public_id (cuid-like) via ensurePublicId() so URLs don't leak integer counters.
  • Extensibility — features and integration providers are add-ons under src/product, aggregated into generated indexes and surfaced through the src/core registries (see Core / Product Layer Split).

Core / Product Layer Split

EduNest separates stable framework code from swappable features and providers so the same engine can power many products. Two top-level folders cooperate:

TEXT
src/core/      # contracts + registries (stable, shipped with every build)
src/product/   # add-ons (features + integration providers) + GENERATED indexes
  • src/core defines the contracts and the registries that the app reads at runtime:

    • core/channels/registry.tsCHANNELS — the catalog of integration channels (email, payments, storage, analytics, video, calendar, captcha, sms, chat…), each with its free built-in providers, merged with provider add-ons.
    • core/plugins/registry.tsFEATURES / FeaturePlugin — the feature-plugin contract (nav items, admin/learner/public pages, API handlers, lifecycle hooks like course.completed, order.paid, ensureSchema()).
    • core/payments/registry.tsPAYMENT_DRIVERS — checkout drivers, with built-in free drivers (cod, bank, paypal) plus premium gateway add-ons.
    • core/adapters/{client,server}.tsclientAdapters() / serverAdapters() — runtime dispatch that resolves a provider's implementation by kind + provider.
  • src/product/addons/<id>/ holds the 93 built-in add-ons. Each has an addon.json manifest plus an index.ts that exports a channel, a plugin, a driver, and/or client/server adapters. Feature add-ons (e.g. certificates, gamification, exams, assignments) own their database tables through an ensureSchema() and ship their own React pages and API handlers.

  • Generated indexes are produced by scripts/bundle-addons.mjs and must not be edited by hand: src/product/addons/index.ts (ADDON_CHANNELS), src/product/features/index.ts (ADDON_FEATURES), src/product/adapters-server/index.ts, and src/product/adapters-client/index.ts. The core registries import these to merge add-ons in at build time.

This means new features and new integration providers are added by dropping an add-on folder (or importing its zip from docs/add-ons) and regenerating the indexes — core code is not touched.

Data Access Pattern

There is no ORM and no migration framework. Data access is raw SQL through a thin Neon wrapper.

src/lib/db.ts lazily initialises a Neon client and re-exposes the tagged-template API so callers keep writing sql\…``:

TS
import sql, { rawSql } from "@/lib/db";

const rows = await sql`SELECT id, title FROM courses WHERE status = ${"published"}`;
// dynamic/parameterised form:
const dyn = await rawSql("SELECT 1 FROM courses WHERE slug = $1 LIMIT 1", [slug]);

Schema is defined in code, not in .sql files. Each domain area has a self-healing ensure*() function:

TS
// src/lib/lms-schema.ts (abridged)
let coursesReady = false;
export async function ensureCourses(): Promise<void> {
  if (coursesReady) return;        // in-memory guard → runs once per process
  await ensureInstructors();
  await ensureCourseCategories();
  await sql`CREATE TABLE IF NOT EXISTS courses ( … )`;
  await sql`ALTER TABLE courses ADD COLUMN IF NOT EXISTS tags JSONB DEFAULT '[]'::jsonb`;
  await sql`CREATE INDEX IF NOT EXISTS idx_courses_published ON courses(status, published_at DESC)`;
  coursesReady = true;
}

Conventions throughout the schema:

  • SERIAL integer primary keys; snake_case columns.
  • Money stored as integer cents (*_cents) with a sibling currency column.
  • Arrays/objects stored as JSONB.
  • Common columns is_active, sort_order, created_at, updated_at.
  • Idempotent CREATE TABLE IF NOT EXISTS + ALTER TABLE … ADD COLUMN IF NOT EXISTS.

The schema is split across files: src/lib/vendor-schema.ts (commerce/store/users/ settings), src/lib/lms-schema.ts (courses, lessons, enrollments, instructors, commerce), plus scripts/init-db.ts (CMS tables), and per-add-on schema.ts files. The init scripts (scripts/init-db.ts, init-vendor.ts, init-lms.ts) simply call the same ensure*() functions in bulk so a fresh database is fully provisioned.

Authentication & Authorization

EduNest has two independent identities, both custom JWT, no refresh tokens.

Admin

src/lib/auth.ts issues an HS256 JWT signed with JWT_SECRET, stored in the admin_token httpOnly cookie. Admins live in the users table (email + bcrypt password, optional role_idroles).

TS
export interface JWTPayload { id: number; email: string; name: string; }
export async function signToken(p: JWTPayload): Promise<string> { … }   // jose, HS256
export async function getSession(): Promise<JWTPayload | null> { … }     // reads cookie
  • Lifetime: JWT_EXPIRES_IN (default 7d).
  • Admin API routes are gated per route with permission checks (RBAC via the roles.permissions JSONB list + permissions catalog). There is no global middleware guard.

Learner / Instructor (Portal)

src/lib/customer-auth.ts issues a separate JWT (payload CustomerPayload with id, email, name, accountType), delivered as the member_session httpOnly cookie, and also accepted as an Authorization: Bearer <jwt> header for mobile clients. Accounts live in the customers table; account_type is 'student' or 'instructor'.

  • Passwords are bcrypt-hashed (password_hash).
  • Email/phone verification and password reset use bcrypt-hashed OTP codes (otp_code, otp_expires_at, otp_purpose).
  • getCustomerId() resolves the current learner/instructor (bearer first, then cookie).

Storage & Secrets

  • src/lib/r2.ts wraps the AWS S3 SDK against any S3-compatible bucket. Active storage config is read from the integration_connections table (channel STORAGE, providers r2 / aws_s3 / gcs / do_spaces), falling back to R2_* env vars. It exposes helpers such as putObject, getUploadUrl (presigned PUT), deleteByUrl, listAllObjects, and getPublicBaseUrl.
  • src/lib/secrets.ts encrypts integration credentials with AES-256-GCM using a key derived (SHA-256) from SECRET_KEY (falling back to JWT_SECRET). Encrypted values are tagged enc:v1:<base64>. Exposed via encryptSecret, decryptSecret, encryptJson, decryptJson.

Licensing

The domain license is verified offline with a bundled RS256 public key. A single app_license row (JSONB blob) stores the activation record. src/lib/license provides activateLicense, recheckLicense, getLicenseStatus, and isLicensed. A daily background heartbeat re-validates against LICENSE_SERVER_URL with a grace period, and localhost / private-LAN hosts bypass licensing for development. See the License Guide for details.

Key Directories

Path Purpose
src/app App Router pages + route handlers (the API)
src/app/api/v1 REST API resource handlers (route.ts)
src/lib DB client, schema (*-schema.ts), auth, r2, secrets, queries, helpers
src/core Reusable contracts + registries (channels, plugins, payments, adapters)
src/product 93 add-ons + generated aggregation indexes
src/components Shared UI (admin, learn, dashboard, form, ui, data-table, …)
src/views Page-level view components for the public site
scripts DB init + seed scripts, add-on bundler/catalog generators
docs Buyer docs + add-on packages (docs/add-ons)

Configuration

Runtime configuration is environment-driven, with most behaviour also editable at runtime through admin-managed settings tables (app_settings, theme_settings, integration_connections).

Variable Used by Purpose
DATABASE_URL src/lib/db.ts Neon PostgreSQL connection string
JWT_SECRET / JWT_EXPIRES_IN src/lib/auth.ts, customer-auth.ts Token signing & lifetime
SECRET_KEY src/lib/secrets.ts AES-256-GCM key for encrypting stored secrets
R2_* src/lib/r2.ts Fallback storage credentials (DB config preferred)
LICENSE_SERVER_URL src/lib/license License activation / heartbeat endpoint

© CreativeCape Solutions · creative-cape.com · support@creative-cape.com