Deep Technical Case Study

bnusa.krd

The largest Kurdish publishing platform — a monorepo with a Next.js 16 frontend, Express API with 16 middleware layers, 36 Mongoose models, 38 route files, real-time Socket.IO, Backblaze B2 media pipeline, Puppeteer PDF generation, and a badge achievement engine.

36 models
38 route files
16 middleware
90+ components
4 apps
Software Engineer & Designer
Table of Contents — 12 Sections
  1. Project Overview & Monorepo Structure
  2. End-to-End Architecture & Boot Sequence
  3. Technology Stack & Dependencies
  4. Mongoose Models — 36 Schemas Deep-Dive
  5. Middleware Pipeline — 16 Layers Explained
  6. API Routes — 38 Route Files & Security Groups
  7. Real-Time: Socket.IO & Notification Engine
  8. Media Pipeline: Image Upload, Sharp & B2
  9. Badge Achievement Engine & PDF Generation
  10. Frontend: Next.js 16, Contexts & Components
  11. Frontend API Client: Caching, Signatures & Offline
  12. Conclusion & Future Work

Project Overview & Monorepo Structure

bnusa.krd is a production Kurdish publishing platform connecting readers, writers, and publishers across books, articles, reviews, collaborative writing (KtebNus), social feeds, and community activities. The codebase is a monorepo containing four distinct applications.

Monorepo layout

DirectoryRuntimePurpose
bnusa/Next.js 16 + React 18Main web app: reading, writing, profiles, feeds, notifications
backend/Express 4 + Node.jsREST API: 38 route files, 16 middleware layers, 36 Mongoose models
admin/Next.js (separate)Admin dashboard: user management, content moderation, badge grants
account/Next.js (separate)Account settings: profile editing, security, connected accounts

Backend folder anatomy

backend/ — directory tree
backend/
  server.js          // 674 lines — Express boot, middleware ordering, route groups
  socket.js          // 126 lines — Socket.IO server, user tracking, emitters
  config/
    db.js            // MongoDB connection with ServerApiVersion.v1
    badges.js        // Badge definitions: normal, luxurious, achievement
    validateEnv.js   // Fail-fast env validation + placeholder detection
  middleware/         // 6 files: auth, csrf, rateLimiter, sanitizer, security, replay
  models/            // 36 Mongoose schemas
  routes/            // 38 Express routers
  services/          // emailService.js, pdfGenerationService.js
  utils/             // imageUpload, badgeAchievements, notificationHelper, redisCache

Frontend folder anatomy

bnusa/src/ — directory tree
bnusa/src/
  app/               // Next.js App Router: ~30 route folders
    layout.tsx       // Root layout: fonts, metadata, 5 context providers
    globals.css      // Global styles
    [feature]/       // page.tsx per route (articles/[slug], books/[id]...)
  components/        // 90+ React components
  contexts/          // AuthContext, SocketContext, ToastContext
  utils/             // api.ts, imageUpload.ts, indexedDB.ts, sanitize.ts
  lib/               // firebase.ts, fetchPublicMeta.ts, seo.ts, slugify.ts
36
Mongoose Models
38
Route Files
16
Middleware Layers
90+
React Components
674
Lines in server.js
319
Lines in api.ts

Key features

📚
Books & KtebNus
Book catalog, collaborative writing with co-writers, chapters, drafts, PDF export via Puppeteer, reading bookmarks with text-position tracking.
✍️
Articles & Reviews
Rich text articles with read-time calculation, inline comments, YouTube/resource links, moderation status, and literary reviews with ratings.
🧶
Activities & Feed
Typed activities (help, research, debate, general) and a social feed with embedded comment threads, reposts, writers-only mode.
🏅
Badges & Achievements
Auto badge unlocks at milestones (1, 5, 10, 25, 50), admin-grantable luxurious badges, real-time unlock notifications via Socket.IO.
🛡️
Security Stack
CSRF, request signatures (multiple formats), replay protection with nonce TTL, per-endpoint rate limiting, HTML sanitization (strict/content-safe).
📷
Media Pipeline
Multer → magic-byte sniffing → Sharp resize → SHA1 naming → Backblaze B2 storage → CDN delivery at cdn.bnusa.krd.
☀️
Design principle: bnusa.krd is built around explicit boundaries. Public routes are rate limited and method-restricted. Private routes require authentication. The middleware ordering in server.js determines what the server considers “trusted input” at each pipeline stage.

End-to-End Architecture & Boot Sequence

The platform is a layered system: UX layer (Next.js), API layer (Express), persistence (MongoDB via Mongoose), external services (B2, Firebase, Brevo), and a real-time channel (Socket.IO). Every layer fails safely and independently.

System data flow

Browser
Next.js 16
Express API
Mongoose
MongoDB Atlas
Image upload
Multer
Sharp
Backblaze B2
cdn.bnusa.krd
Firebase Auth
ID token
/auth/firebase
JWT cookie
auth middleware

Fail-fast environment validation

Before Express starts, validateEnv() checks 7 required env vars (MONGODB_URI, JWT_SECRET, B2 keys, API_KEY), adds 2 more in production (CORS_ALLOWED_ORIGINS, COOKIE_DOMAIN), and rejects placeholder values like “changeme” for secret keys. If anything is missing, it calls process.exit(1).

backend/config/validateEnv.js — Boot guard
const required = [
  'MONGODB_URI', 'JWT_SECRET', 'B2_KEY_ID',
  'B2_APP_KEY', 'B2_BUCKET_NAME', 'B2_ENDPOINT', 'API_KEY',
];
// In production, reject placeholder values for secrets
const placeholders = ['your-secret-key', 'changeme', 'secret'];
const insecure = secretKeys.filter(key => {
  return placeholders.some(p => val === p || val.includes('your'));
});

Deterministic middleware pipeline

backend/server.js — Middleware ordering (674 lines, excerpt)
// 1. Parse
app.use(express.json({ limit: '10mb' }));
app.use(cookieParser());

// 2. Compress (skip binary/streaming)
app.use(compression({ filter: customFilter }));

// 3. Security headers
app.use(helmet(helmetConfig));

// 4. CORS (subdomain-aware)
app.use(cors(corsOptions));

// 5. Sanitize all input early
app.use(sanitizer());

// 6. Auth routes (before CSRF so login can issue tokens)
app.use('/auth', authRouteLimiter, authRoutes);

// 7. CSRF boundary — all routes below require valid CSRF token
app.use(validateCsrf);

// 8-11. Public → Special → Auth-only → Private route groups

MongoDB connection

backend/config/db.js
await mongoose.connect(process.env.MONGODB_URI, {
  serverApi: { version: ServerApiVersion.v1, strict: true, deprecationErrors: true },
  serverSelectionTimeoutMS: 15000,
  socketTimeoutMS: 45000,
});
Result: the server defines a predictable “safe request” boundary. Breakage happens in known stages — env validation, parsing, sanitization, CSRF, security middleware, or routing — never as a hidden per-endpoint inconsistency.

Technology Stack & Dependencies

The stack reflects a practical constraint: build fast UI with Next.js and keep server-side logic explicit with Express + Mongoose. Operational middleware is included because publishing platforms are exposed to untrusted traffic.

Full dependency snapshot

LayerPackageWhy it matters
Webnext@^16App Router, server components, fast page transitions
Web[email protected]Client components, hooks, context API
Websocket.io-client@4Real-time notification delivery to browser
API[email protected]Explicit middleware pipeline and routing
API[email protected]Schema validation, embedded subdocs, compound indexes
API[email protected]Security headers + CSP in production
APIexpress-rate-limit@7Per-endpoint rate limiting with Kurdish error messages
APIsanitize-html@2XSS prevention: strict and content-safe allowlists
APIfirebase-admin@13Server-side Firebase ID token verification
API[email protected]Image resize, format conversion, metadata strip
API@aws-sdk/client-s3@3S3-compatible API for Backblaze B2 storage
APIpuppeteer@24Headless Chromium for KtebNus book PDF generation
API@sib-api-v3-sdkBrevo transactional email delivery
APIredis@4Optional cache for like counts and user like status
Next.js 16 + React 18
App Router for reading/writing flows, social activity UI, profile pages, SEO rendering.
🧠
Express 4
API runtime with explicit middleware ordering and route-group security model.
🗃️
MongoDB + Mongoose 8
Document modeling, schema validation, compound indexes, embedded subdocuments, virtuals, text search.
🛰
Socket.IO 4
Real-time notification delivery, user presence tracking, iOS PWA reconnect handling.
🔥
Firebase Auth
Email/password + Google OAuth. Frontend gets ID token, backend verifies and issues JWT session cookie.
📷
Sharp + Backblaze B2
Server-side image resize/optimize, magic-byte MIME validation, S3-compatible cloud storage.

Mongoose Models — 36 Schemas Deep-Dive

The backend defines 36 Mongoose models spanning user identity, content types, social interactions, moderation, and system internals. Every model uses schema-level validation, compound indexes, and deliberate denormalization.

Core identity models

ModelKey FieldsNotable Features
UserfirebaseUid, name, username, email, password, role, followers[], badgesPre-save bcrypt hashing (salt=12), comparePassword method, social links, compound indexes
Writeruser (ref), bio, featured, articlesCount, followers[], categoriesText index on bio+categories, virtual for full profile
UserImageuserId (unique), profileImage, bannerImageStatic syncWithUser() upsert method
backend/models/User.js — Password hashing
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  const salt = await bcrypt.genSalt(12);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

Content models

ModelKey FieldsNotable Features
Booktitle, writer, description, genre, year, image, slugPre-save slug generation from title, text indexes for search
Articletitle, content, author (ref), categories[], likes[], status, readTimePre-save readTime calculation (wordCount/200), array normalization
KtebNusBookauthor (ref), coWriters[], genre[], publisher, status, coverImage, pdfUrlMulti-genre, revealOwner toggle, draft/published, PDF export metadata, counters
Reviewtitle, content, genre, rating, recommended, author (ref), statusModeration flow (pending/accepted/rejected), edit count tracking
backend/models/Article.js — Read-time calculation
articleSchema.pre('save', function(next) {
  if (this.content) {
    const plainText = this.content.replace(/<[^>]*>/g, '');
    const wordCount = plainText.split(/\s+/).filter(Boolean).length;
    this.readTime = Math.ceil(wordCount / 200);
  }
  next();
});

Social & interaction models

ModelKey FieldsNotable Features
ActivityauthorId, type (help/research/debate), title, comments[Comment], likes[]Embedded CommentSchema with nested ReplySchema, virtual commentCount/likeCount
FeedPostauthor (ref), content, comments[], likes[], writersOnly, isDeletedEmbedded comments with replies, writers-only visibility, soft delete
CommentarticleId, userId, parentId, content, replyingToThreaded via parentId, virtual isTopLevel + directRepliesCount
BoardPostprofileOwnerId, authorId, content, likes[], replies[], isPinnedProfile board with pinning, compound index on pinned+createdAt
NotificationuserId, fromUserId, type, relatedId, relatedType, relatedSlug, isReadType enum: follow, like_article, comment, mention, badge_unlock, new_book, etc.
ReadingBookmarkuserId, bookId, chapterId, paragraphId, startOffset, endOffset, selectedTextText-position tracking for “Continue Reading” widget, one active per user
backend/models/Activity.js — Embedded threads + virtuals
const CommentSchema = new mongoose.Schema({
  authorId: { type: ObjectId, ref: 'User', required: true },
  content: { type: String, required: true, maxlength: 10000, trim: true },
  replies: [ReplySchema],
});

activitySchema.virtual('commentCount').get(function() {
  let count = this.comments.length;
  for (const c of this.comments) count += (c.replies?.length || 0);
  return count;
});
☀️
Embedded vs. referenced: Activities use embedded comments (one fetch = whole thread, good for read-heavy detail views). Articles use a separate Comment collection with parentId threading (better for high-volume independent queries and pagination).

Middleware Pipeline — 16 Layers Explained

The backend applies middleware in a strict, deterministic order. Each layer has a single responsibility, and the ordering ensures security checks happen before business logic.

Complete middleware stack (in order)

#MiddlewareFileResponsibility
1JSON parserexpress.json()Parse request bodies up to 10MB limit
2URL-encodedexpress.urlencoded()Parse form data with extended mode
3Cookie parsercookie-parserParse cookies for session JWT and CSRF tokens
4CompressioncompressionGzip responses >1KB, skip socket.io/images/pdf paths
5HelmethelmetSecurity headers, CSP with CDN + Firebase allowlists
6CORScorsSubdomain-aware: *.bnusa.krd + configured origins
7Sanitizersanitizer.jsRecursive HTML sanitization (strict + content-safe modes)
8Auth routesauthRoutes.jsLogin/signup/OAuth — mounted BEFORE CSRF
9CSRFcsrf.jsCookie-header token matching for state-changing requests
10SecuritysecurityMiddleware.jsAPI key + timestamp + signature validation + IP rate limiting
11Method restrictormethodRestrictor.jsOnly GET on public routes (explicit POST allowlist)
12Rate limitersrateLimiter.js8 distinct configs: public, auth, comments, likes, reviews, etc.
13Auth verifierauth.jsFirebase token / JWT cookie verification, user lookup
14Replay protectionreplayProtection.jsIn-memory nonce tracking with 5-minute TTL
15Role check(inline)Admin/writer role verification on protected routes
16MulterimageUpload.jsMemory storage file upload with type filtering

Security middleware deep-dive (361 lines)

Implements in-memory rate limiting per IP + endpoint + method, API key validation, timestamp freshness, and request signature verification supporting multiple signature formats for backward compatibility.

backend/middleware/securityMiddleware.js — Multi-format signatures
const sigFormats = [
  // Format 1: method + path + timestamp + apiKey
  crypto.createHmac('sha256', apiKey)
    .update(`${method}${path}${timestamp}${apiKey}`).digest('hex'),
  // Format 2: method + fullUrl + timestamp + apiKey
  crypto.createHmac('sha256', apiKey)
    .update(`${method}${fullUrl}${timestamp}${apiKey}`).digest('hex'),
  // Format 3: method + path + timestamp + nonce + apiKey
  crypto.createHmac('sha256', apiKey)
    .update(`${method}${path}${timestamp}${nonce}${apiKey}`).digest('hex'),
];
const isValid = sigFormats.some(s => s === signature);

CSRF protection (28 lines)

backend/middleware/csrf.js
const validateCsrf = (req, res, next) => {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
  if (authHeader.startsWith('Bearer ')) return next(); // API clients skip CSRF

  const cookieToken = req.cookies['csrf-token'];
  const headerToken = req.headers['x-csrf-token'];
  if (!cookieToken || !headerToken || cookieToken !== headerToken)
    return res.status(403).json({ error: 'CSRF token mismatch' });
  next();
};

Auth middleware (211 lines)

Supports three authentication strategies in priority order: (1) Firebase ID token in Authorization header, (2) JWT session cookie, (3) fallback admin JWT. On success, it finds or creates the MongoDB User document and attaches req.user with roles.

Sanitizer: strict vs. content-safe

Recursively walks request body, query, and params. Strict mode strips all HTML. Content-safe mode for rich-text endpoints allows minimal tags: <p>, <br>, <strong>, <em>, <a>, <ul>, <ol>, <li>, <blockquote>. Binary upload paths are skipped entirely.

Rate limiter configurations

LimiterWindowMaxKey
publicApiLimiter1 min100IP
authRouteLimiter15 min20IP
commentLimiter1 min10IP + route
likeLimiter1 min30IP + route
reviewLimiter15 min5IP
ktebnusBookLimiter15 min10IP
boardPostLimiter1 min10IP
followLimiter1 min20IP

Replay protection

In-memory nonce tracking with 5-minute TTL. Each caller (user ID or IP) gets a nonce store. Duplicate nonce + timestamp within the window returns 409 Conflict. Expired entries are garbage-collected on a periodic interval.

API Routes — 38 Route Files & Security Groups

The server groups all 38 route files into four security tiers: public, special, auth-only, and private. Each group gets a deterministic middleware stack applied via forEach loops.

backend/server.js — Route grouping pattern
const publicRoutes = [
  { path: '/api/books', router: bookRoutes },
  { path: '/api/writers', router: writers },
  { path: '/api/stats', router: statsRoutes },
];

publicRoutes.forEach(({ path, router }) => {
  app.use(path, publicApiLimiter, securityMiddleware, methodRestrictor, router);
});

const authOnlyRoutes = [
  { path: '/api/notifications', router: notificationRoutes },
  { path: '/api/bookmarks', router: bookmarkRoutes },
  { path: '/api/users', router: userRoutes },
];

authOnlyRoutes.forEach(({ path, router }) => {
  app.use(path, authenticatedLimiter, securityMiddleware, authMiddleware, router);
});

Route file inventory (top 19 by size)

Route FileSizeDomain
userRoutes.js72KBProfile CRUD, follow/unfollow, search, settings, avatar
adminRoutes.js62KBUser management, content moderation, analytics, badges
ktebnusRoutes.js41KBKtebNus public book/chapter browsing, search, comments
ktebnusPrivateRoutes.js35KBKtebNus authoring: create/edit books, chapters, co-writers
reviewRoutes.js34KBReview CRUD, moderation status, comments, likes
articles.js33KBArticle listing, filtering, search (public)
activityRoutes.js31KBActivity CRUD, embedded comments/replies, likes
feedPostRoutes.js25KBSocial feed, comments, likes, reposts, writers-only
authRoutes.js20KBFirebase auth, Google OAuth, session, logout
imageRoutes.js19KBImage upload (single + multi), profile/banner, B2
articleRoutes.js18KBArticle CRUD (auth-required)
commentRoutes.js16KBArticle threaded comments via parentId
inlineCommentRoutes.js16KBInline text-selection comments for KtebNus chapters
boardRoutes.js16KBProfile board posts with replies and pinning
publicMetadataRoutes.js13KBSEO metadata endpoints for SSR/SSG
repostRoutes.js12KBFeed post repost/unrepost with notifications
reviewCommentRoutes.js12KBReview comment threads
forYouRoutes.js10KBPersonalized "For You" feed algorithm
ktebnusPdfRoutes.js10KBPDF generation and download for KtebNus books

Method restrictor: public route protection

Public routes are restricted to GET only by default. The method restrictor maintains an explicit allowlist for POST endpoints (image uploads, ktebnus comments/likes) and blocks all other non-GET methods with 405.

Validation alignment: route + schema

Social systems have a classic correctness issue: the UI may accept input the server rejects. bnusa.krd enforces max length at two layers: route-level validation (fast fail) and schema-level (database integrity).

🚨
Failure mode: client permits long text, API returns 400 with Content too long (max 10000). Correct from a data-integrity perspective, but breaks UX unless limits are synchronized.
backend/routes/activityRoutes.js — Route-level check
if (content.trim().length > 10000) {
  return res.status(400).json({
    success: false,
    message: 'Content too long (max 10000)'
  });
}

Real-Time: Socket.IO & Notification Engine

The platform uses Socket.IO for real-time notification delivery. The backend tracks connected users by socket, and the frontend maintains a resilient connection with iOS PWA reconnect handling.

Backend socket server (126 lines)

Initializes Socket.IO with CORS, authenticates users from JWT session cookies, tracks user → socket mappings in memory, and exposes sendNotificationToUser() and sendAdminMessage() emitters.

backend/socket.js — User tracking + targeted emit
const userSockets = new Map(); // userId -> Set<socketId>

io.on('connection', (socket) => {
  const userId = socket.handshake.auth?.userId;
  if (userId) {
    socket.join(userId); // join personal room
    if (!userSockets.has(userId)) userSockets.set(userId, new Set());
    userSockets.get(userId).add(socket.id);
  }
});

function sendNotificationToUser(userId, notification) {
  io.to(userId).emit('notification', notification);
}

Notification helper (176 lines)

🔔
createNotification()
Creates Notification doc, populates fromUserId, emits via Socket.IO. Skips self-notifications unless allowSelf is true. Handles ObjectId conversion.
👥
notifyFollowers()
Notifies up to 500 followers about new content (new_book, new_article). Uses Promise.allSettled for resilience.
@
notifyMentions()
Parses @username mentions via regex, looks up users, sends mention notifications with Kurdish default messages.
backend/utils/notificationHelper.js — @mention parsing
const mentions = text.match(/@([a-zA-Z0-9_]+)/g);
const usernames = [...new Set(mentions.map(m => m.slice(1)))];
const users = await User.find({ username: { $in: usernames } });
const fromName = fromUser?.name || 'کەسێک';

Frontend SocketContext (233 lines)

Creates and manages the Socket.IO connection with several resilience features:

Media Pipeline: Image Upload, Sharp & B2 Storage

Every image uploaded to bnusa.krd passes through a multi-stage validation and processing pipeline before reaching Backblaze B2 cloud storage and being served via CDN.

Client upload
Multer (memory)
Magic-byte sniff
Sharp resize
SHA1 filename
B2 PutObject
cdn.bnusa.krd

Image validation

imageUpload.js performs two-layer MIME validation: first checking the file extension, then reading the file's magic bytes (first 12 bytes) to detect actual file type. This prevents attackers from uploading malicious files with renamed extensions.

backend/utils/imageUpload.js — Magic-byte sniffing
function sniffMimeType(buffer) {
  const hex = buffer.slice(0, 12).toString('hex');
  if (hex.startsWith('89504e47')) return 'image/png';
  if (hex.startsWith('ffd8ff')) return 'image/jpeg';
  if (hex.startsWith('47494638')) return 'image/gif';
  if (hex.startsWith('52494646') && hex.includes('57454250'))
    return 'image/webp';
  return null;
}

Sharp processing

After validation, images are processed with Sharp: resized to max dimensions, converted to WebP for optimal size, metadata stripped for privacy, and named with a SHA1 hash of the content for cache-friendly deduplication.

Backblaze B2 storage

Processed images are uploaded to B2 via the @aws-sdk/client-s3 S3-compatible API. The bucket is configured with a CDN-friendly URL at cdn.bnusa.krd for low-latency global delivery.

Compression filter: avoid corrupting binary

backend/server.js — Compression exclusion logic
app.use(compression({
  threshold: 1024,
  filter: (req, res) => {
    if (req.path.startsWith('/socket.io')) return false;
    if (req.path.startsWith('/api/images')) return false;
    if (req.path.includes('/pdf/') || req.path.includes('/download')) return false;
    return compression.filter(req, res);
  },
}));

Badge Achievement Engine & PDF Generation

bnusa.krd includes a gamification system that automatically unlocks badges at publishing milestones, plus admin-grantable luxurious badges. The platform also generates downloadable PDF books via headless Chromium.

Achievement badges

checkAndUnlockBadges(userId, type) runs after each article publish, review acceptance, or book publish. It counts published items, compares against milestone thresholds (1, 5, 10, 25, 50), and unlocks all qualifying badges in a single $addToSet update.

backend/utils/badgeAchievements.js — Milestone check
async function checkAndUnlockBadges(userId, type) {
  const milestones = ACHIEVEMENT_BADGES[type];
  const user = await User.findById(userId).select('unlockedBadges').lean();
  const alreadyUnlocked = new Set(user.unlockedBadges || []);
  const toCheck = milestones.filter(m => !alreadyUnlocked.has(m.id));

  const count = await getPublishedCount(userId, type);
  const newlyUnlocked = toCheck.filter(m => count >= m.count);

  await User.findByIdAndUpdate(userId, {
    $addToSet: { unlockedBadges: { $each: newIds } },
  });

  // Real-time notification for each unlock
  for (const m of newlyUnlocked) {
    await notifyBadgeUnlock(userId, m.id, m.messageKu);
  }
}

Admin badge management

Admins can grant luxurious badges via adminGrantBadge() and revoke them via adminRevokeBadge(). Revocation also clears the user's selectedBadge if they had the revoked badge selected.

Badge categories

🏅
Normal badges
Achievement-based: automatically unlocked at publish milestones for articles, reviews, and books.
💎
Luxurious badges
Admin-only: manually granted/revoked for special contributions, with custom animations and Kurdish names.

PDF generation (648 lines)

pdfGenerationService.js uses Puppeteer to render KtebNus books as styled PDFs. It pre-loads the Rabar Kurdish font as base64 from multiple candidate directories (supporting local dev, Docker, and Coolify deployments), builds a full HTML print template with theme support (light/dark), cover page, chapter pages with RTL layout, and watermark branding.

KtebNus book
buildPrintHTML()
Puppeteer render
PDF buffer
B2 upload
Download URL
backend/services/pdfGenerationService.js — Font loading strategy
const candidateFontDirs = [
  process.env.PDF_FONT_DIR,                                // Coolify override
  path.resolve(__dirname, '../../bnusa/public/fonts/reader'), // Monorepo
  path.resolve(process.cwd(), 'bnusa/public/fonts/reader'),   // CWD = repo root
  path.resolve(__dirname, '../public/fonts/reader'),          // Backend copy
].filter(Boolean);

for (const dir of candidateFontDirs) {
  if (fs.existsSync(path.join(dir, 'Rabar_021.woff'))) {
    rabar400Base64 = fs.readFileSync(...).toString('base64');
    break;
  }
}

Email service

emailService.js sends transactional emails via the Brevo API. The welcome email uses the "Apple approach" for Gmail iOS dark mode compatibility: white backgrounds (#ffffff, #f5f5f7) with dark text (#1d1d1f) that Gmail auto-inverts. The Rabar font is loaded via @font-face from bnusa.krd/fonts/Rabar_021.woff.

Frontend: Next.js 16, Contexts & Component Architecture

The frontend is a Next.js 16 application using the App Router with 90+ React components, 3 context providers, and a comprehensive utility layer for API communication, offline storage, and image handling.

Root layout (157 lines)

layout.tsx wraps the entire app with five nested context providers, loads fonts (Rabar Kurdish, Geist Sans, Geist Mono), sets metadata + viewport, and renders global UI components.

bnusa/src/app/layout.tsx — Provider nesting order
<AuthProvider>
  <SocketProvider>
    <ThemeProvider>
      <ToastProvider>
        <ConfirmDialogProvider>
          <Navbar />
          {children}
          <Footer />
          <AccessibilitySettings />
          <ScrollToTop />
        </ConfirmDialogProvider>
      </ToastProvider>
    </ThemeProvider>
  </SocketProvider>
</AuthProvider>

AuthContext (192 lines)

Manages authentication state with Firebase. On mount, calls /api/users/me with retry logic for rate limiting (up to 3 retries with exponential backoff). Sign-in flow: Firebase signInWithEmailAndPassword → get ID token → POST to /auth/firebase → backend sets JWT session cookie → reload session. Sign-out cleans up Firebase state, localStorage (firebase keys, image caches, pending uploads), sessionStorage, and API caches.

bnusa/src/contexts/AuthContext.tsx — Rate limit retry
while (true) {
  try {
    const me = await api.noCache.get('/api/users/me');
    setCurrentUser(me?.user || null);
    break;
  } catch (e) {
    if (e?.code === 'RATE_LIMIT' || e?.status === 429) {
      attempt++;
      if (attempt <= 3) {
        const retryAfterMs = e.retryAfter * 1000 || 500 * attempt;
        await new Promise(r => setTimeout(r, retryAfterMs));
        continue;
      }
    }
    throw e;
  }
}

Firebase integration

bnusa/src/lib/firebase.ts — Auth initialization
const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
export const auth = getAuth(app);

// Persistence: try localStorage, fallback to sessionStorage
export const authReady = (async () => {
  try { await setPersistence(auth, browserLocalPersistence); }
  catch { await setPersistence(auth, browserSessionPersistence); }
})();

// Google sign-in via popup (more reliable on iOS Safari)
export async function signInWithGooglePopup(): Promise<string> {
  await authReady;
  const result = await signInWithPopup(auth, googleProvider);
  return await result.user.getIdToken();
}

App routes (~30 folders)

RoutePage
/articles/[slug]Article detail with comments, likes, inline actions
/books/[id]Book detail with chapters list, bookmarks
/reviews/[slug]Review detail with comments, ratings
/profile/[username]User profile with board, articles, badges
/ktebnus/[slug]KtebNus book reader with chapters
/feedSocial feed with posts, comments, reposts
/activitiesActivity hub (help, research, debate)
/notificationsNotification center
/dashboardWriter dashboard with content management
/signin, /signupAuthentication pages

Frontend API Client: Caching, Signatures & Offline

The frontend api.ts (319 lines) is a comprehensive API utility that handles request caching, HMAC signature generation for secure communication, image load tracking, and multiple cache management strategies.

Request signature generation

Every API request includes security headers generated client-side: a timestamp, nonce, and HMAC-SHA256 signature computed from the HTTP method, URL path, timestamp, and API key. This matches the backend's securityMiddleware.js validation.

bnusa/src/utils/api.ts — Signature generation
function generateSignature(method: string, url: string) {
  const timestamp = Date.now().toString();
  const nonce = crypto.randomUUID();
  const path = new URL(url).pathname;
  const signature = hmacSHA256(
    `${method}${path}${timestamp}${nonce}${API_KEY}`,
    API_KEY
  );
  return { timestamp, nonce, signature };
}

Caching strategies

📦
In-memory cache
GET responses cached with configurable TTL. Cache keys include URL + query params. Invalidated on mutations.
🖼️
Image cache registry
Tracks loaded images in localStorage with metadata for efficient re-rendering and bandwidth savings.
💾
IndexedDB offline
Articles, reviews, and books stored in IndexedDB for offline reading. 6 object stores with slug/timestamp indexes.

IndexedDB structure

indexedDB.ts (277 lines) manages an IndexedDB database with 6+ object stores for offline capability:

StoreKeyIndexesPurpose
pendingUploadsauto-incrementtimestampQueue uploads for retry when offline
articlesidslug (unique), timestampCached article data
userDataidCached user profile data
cachedImagesurltimestampImage blob cache
offlineArticlesidslug, savedAtSaved-for-offline articles
offlineReviewsidslug, savedAtSaved-for-offline reviews
offlineBooksidslug, savedAtSaved-for-offline KtebNus books

CORS origin strategy

backend/server.js — Subdomain-aware CORS
const corsOptions = {
  origin: function(origin, callback) {
    if (!origin) return callback(null, true);
    const h = new URL(origin).hostname.toLowerCase();
    if (h === 'bnusa.krd' || h.endsWith('.bnusa.krd'))
      return callback(null, true);
    return callback(null, allowedOrigins.includes(origin));
  },
  credentials: true,
};

Conclusion & Future Work

bnusa.krd is engineering for cultural infrastructure: the core value is not only features, but a stable system that keeps publishing workflows trustworthy as the platform grows.

Key technical achievements

Future roadmap

☀️
bnusa.krd is live at bnusa.krd. View the portfolio project page at yadqasim.dev/projects/bnusa.