← Back to Work

VNET

Full-stack event management system with role-based UI views, calendar scheduling, rich-text authoring, and per-event analytics. Built frontend and the entire backend API.

Role
Senior Frontend Engineer (full-stack)
Published
April 2025
Team size
3
Roles
3
API consumers
2+
React 18TypeScriptExpress 4Node.jsSupabaseFullCalendarDraft.jsRecharts

Context

VNET is an event management platform built for Extend — the same company behind Liqaa25. It serves three distinct user roles: admin (full platform control), creator (creates and manages events), and user (registers and attends). The platform combines calendar scheduling, rich-text event descriptions, per-event analytics, and a REST API consumed by Liqaa25 and downstream Extend services.

Problem

The challenge wasn't any single feature — it was building calendar scheduling, rich-text authoring, per-event analytics, and a full REST API in parallel within a 3-person team. Each piece had non-trivial integration surface:

Architecture

The API was designed as a shared service from the start — not an afterthought. Liqaa25's event-embedding feature depends on VNET's /events endpoint, so versioning discipline and explicit OpenAPI documentation were non-negotiable.

Why Express over Next.js Route Handlers here?

VNET's frontend is a React SPA, not a Next.js app — it was built before Liqaa25 and predates the decision to standardise on Next.js for new Extend products. The Express API is intentionally decoupled from the frontend deployment, which means Liqaa25 and future services can consume it without any dependency on VNET's frontend stack.

Implementation

RBAC middleware

Roles are stored in Supabase and verified on every authenticated request. The middleware chain runs: JWT verification → role extraction → route-level permission check.

// Middleware chain: auth → role → permission
const requireRole = (allowed: Role[]) =>
  (req: Request, res: Response, next: NextFunction) => {
    const { role } = req.user // set by JWT middleware upstream
    if (!allowed.includes(role)) {
      return res.status(403).json({ error: 'Insufficient permissions' })
    }
    next()
  }
 
// Usage on a creator-or-admin-only route
router.post('/events', authenticate, requireRole(['creator', 'admin']), createEvent)

FullCalendar — recurring events and timezone

Recurring events are the hardest part of any calendar system. The implementation uses rrule for recurrence rule generation and stores events in UTC with per-user timezone display on the frontend. FullCalendar's timeZone: 'local' prop handles the display conversion — but the storage contract is always UTC to avoid DST ambiguity.

API key management with rate limiting

For machine-to-machine access (Liqaa25 consuming VNET's API), a separate API key system sits alongside JWT auth. Keys are hashed with bcrypt before storage and rate-limited per key using an in-memory sliding window. The rate limiter was intentionally simple — Redis would be the right choice at scale, but for the current traffic profile it's unnecessary overhead.

Swagger as a first-class deliverable

Every route is documented with JSDoc-compatible annotations that swagger-jsdoc compiles into an OpenAPI 3.0 spec, served at /api/docs. When Liqaa25's frontend team needs to integrate a new VNET endpoint, they read the Swagger docs rather than asking a VNET team member. The docs became the contract.

Outcome

What I'd change in 2026: Draft.js is in maintenance mode — the Apollo-mena project already migrated to Lexical for rich text. If VNET were started today, Lexical would be the choice for the editor layer. For the backend framework, Hono running on Cloudflare Workers would be a stronger choice than Express on Node for a shared API service.

← All case studies