Back to Portfolio

StoryHub

StoryHub is a full-stack publishing platform built to bring writing, discovery, and community interaction together in one cohesive experience. Beyond core blogging functionality, the project focuses on building a scalable and reliable product with thoughtful UX, modern architecture, and production-ready engineering practices.

StoryHub project overview

Why I Built It

StoryHub development and engineering overview

I built StoryHub to go beyond individual features and understand what it takes to create a modern web application end to end.

I was curious about what happens behind familiar interactions—how user-to-post and user-to-user relationships are modeled; how notifications are delivered; how authentication and authorization shape the experience; and how these systems come together to feel seamless to the user.

Rather than exploring these concepts in isolation, I wanted to learn by bringing them together in a single product. StoryHub became an opportunity to move beyond basic CRUD applications and understand how the systems behind modern social and content platforms work beneath the surface.

Core Features

Feature01 / 12

Content Creation & Management

  • Full post CRUD with ownership-aware access control
  • Rich-text editing with Tiptap and clean slug-based post URLs
  • Autosave to prevent data loss while writing
  • Soft deletion for safer content management
  • Dedicated dashboard for All, Drafts, Published, and Scheduled posts

Technical Stack

Exploring

Frontend

01 / 02
Selected technology

Next.js

Full-stack React framework and application routing

System Architecture

StoryHub system architecture diagram

Engineering Challenges & Decisions

I chose a flattened threaded comment model with inline @mentions instead of an infinitely nested hierarchical structure. The goal was to preserve conversational context without allowing the UI and DOM structure to grow increasingly complex with every level of nesting.

Rather than recursively rendering deeply nested comment trees, replies remain within a flatter discussion structure while @mentions provide explicit context about who a reply is directed toward. This keeps discussions easier to scan and reduces frontend rendering complexity as conversations grow, while still preserving the relationship between participants and their replies.

StoryHub relies heavily on user-generated media such as avatars and post cover images. I wanted to keep these assets outside the application while still using Next.js's Image component for optimized delivery. Since remote image sources need to be explicitly configured, centralizing user media in Supabase Storage gave me a predictable storage layer and a controlled image origin.

I chose private Supabase buckets with signed URLs rather than exposing assets through public URLs. Private buckets keep object access protected, while signed URLs provide temporary, controlled access when an image needs to be rendered. This introduced additional complexity around generating and handling expiring URLs, but provided a reusable access-control model across StoryHub's different media buckets.

Scheduled publishing required StoryHub to periodically trigger a secure server-side endpoint that checks for posts whose scheduled time has been reached and transitions them to published.

I initially considered using Vercel Cron Jobs, but the Hobby plan only supports once-daily cron execution, while StoryHub needed much finer-grained scheduling to publish posts close to their intended time.

Instead of introducing a paid infrastructure dependency for this requirement, I used cron-job.org as an external scheduler to invoke the publishing endpoint at a regular interval with a secret authorization header. The endpoint then performs the database checks and publishes eligible posts.

This kept the publishing logic inside StoryHub while using a lightweight external service only for scheduled triggering, giving the system the scheduling frequency it needed without unnecessarily increasing infrastructure cost.

Real-time notifications were an important part of StoryHub, but the communication pattern was primarily one-way: the server needed to push events to connected clients. I considered WebSockets, but they would introduce a bidirectional communication model that StoryHub's notification system didn't actually require.

I chose Server-Sent Events (SSE) because it works over standard HTTP and integrates naturally with Next.js Route Handlers through streaming responses. This allowed the application to maintain a server-to-client event stream without introducing a separate WebSocket server or additional real-time infrastructure.

The browser's native EventSource API also provides built-in reconnection behavior, reducing the amount of connection-management logic I needed to implement myself.

Although WebSockets are now supported on Vercel through its newer Fluid Compute/WebSocket capabilities, SSE remained the better fit for StoryHub because its notification requirements were unidirectional and didn't justify the additional complexity of a bidirectional protocol.

StoryHub's email notification system needed to handle notification events outside the user's request-response cycle. Instead of processing email generation and delivery synchronously, I designed an asynchronous digest workflow that could collect events, batch them, and process them independently.

Upstash Redis serves as the fast, short-lived data layer for this workflow. Notification events are temporarily stored and aggregated there before being grouped into email digests, while Redis also maintains the state required for email rate limiting. Its fast key-value operations, expiration capabilities, and lightweight counters made it a good fit for this high-frequency, transient data rather than storing it in MongoDB, which remains StoryHub's primary persistent data store.

BullMQ provides the queue and worker layer on top of Redis. Once email processing is triggered, jobs can be queued and handled asynchronously by a worker that batches the relevant notifications into digest emails and sends them through Resend. Moving this work into a background workflow prevents email processing from delaying user-facing requests while also providing structured job handling, including retries and failure management.

This created a clear separation of responsibilities: Redis manages transient digest and rate-limiting state, BullMQ manages asynchronous job processing, and Resend handles email delivery.

After the security audit exposed gaps across authentication, authorization, user input, file uploads, sessions, dependencies, and information exposure, StoryHub's security hardening treated these areas as interconnected parts of the application's security surface.

Authorization → IDOR & ownership validation

I implemented authorization checks server-side, deriving the authenticated user's identity from the verified session rather than trusting user-supplied IDs from request parameters.

For resources that belong to a user, the API verifies that the authenticated user has permission to access or modify the requested resource before performing the operation. For post-specific actions, this includes validating the relationship between the current user and the post's author/owner. Unauthorized access is rejected with an appropriate authorization response rather than allowing the requested mutation to proceed.

This prevents horizontal privilege escalation/IDOR, where an authenticated user attempts to access or modify another user's resources. Depending on the endpoint, returning 403 Forbidden or 404 Not Found can also help avoid exposing whether a protected resource exists.

Session Security → JWT & session hijacking

StoryHub uses NextAuth.js for authentication and JWT-based session management. Protected operations are validated server-side against the current authenticated session rather than relying solely on client-controlled identity information.

I configured 8-hour maximum JWT/session lifetimes with a 1-hour session update interval, limiting the lifetime of individual session tokens while allowing active sessions to continue without unnecessary reauthentication.

To handle session invalidation, I also introduced a server-side sessionVersion mechanism. The current version is associated with the user's session, and when a security-sensitive event requires session invalidation, the stored version can be changed, causing previously issued tokens with an outdated version to become invalid.

Together, these controls provide protection at multiple levels: limited token lifetime, server-side session validation, and explicit invalidation of previously issued sessions, reducing the risk associated with token reuse and session hijacking.

Input & File Security → SVG XSS & upload validation

The most extensive hardening was applied to user-uploaded images. I moved away from trusting client-provided file metadata and implemented defense-in-depth validation.

During upload, StoryHub parses the incoming FormData, enforces a 10 MB maximum file size, uses an allowlist for supported image types (JPEG, PNG, and WebP), validates file extensions in addition to MIME information, and validates that the uploaded content can actually be processed as an image rather than trusting the declared MIME type alone.

Accepted images are re-encoded as WebP before being stored in Supabase. The normalized asset uses a controlled server-generated path and filename structure.

This approach helps defend against renamed executables, spoofed MIME types, malformed image payloads, and SVG-based attacks. File extension and Content-Type checks alone are insufficient because both can be manipulated; validating the actual file content and rewriting images provides an additional security boundary.

For signed-URL generation, the server also validates the requested resource and authorization before generating access to the stored asset. The storage path is constrained to StoryHub's expected structure rather than allowing arbitrary user-controlled paths.

Information Exposure → PII & internal implementation details

I reduced unnecessary information exposure from server responses and error handling.

Sensitive personally identifiable information is not exposed to clients unless it is required for the feature, and protected resources are subject to the same server-side authorization checks used elsewhere in the application.

I also tightened server-side input handling and error responses so that production responses do not unnecessarily reveal stack traces, database/schema details, internal implementation information, or other debugging data.

The goal is to expose only the information required by the client while keeping internal application details on the server.

Dependency Security → outdated & vulnerable packages

I also treated third-party dependencies as part of StoryHub's security surface. After identifying outdated or potentially vulnerable packages, I upgraded critical dependencies—most importantly Next.js—and removed or replaced dependencies that were no longer appropriate.

To make dependency maintenance more consistent, I integrated Dependabot to monitor the project's dependencies and automatically raise update pull requests when newer or security-relevant versions are available. This turns dependency maintenance from a one-time cleanup into an ongoing process, while keeping updates reviewable before they are merged.

This also reinforces an important supply-chain security principle: third-party dependencies need continuous monitoring, since newly discovered vulnerabilities can affect an otherwise secure application.

Lessons Learned

01

Feature complexity comes from interactions, not individual features

StoryHub started with seemingly independent features—posts, comments, follows, notifications, bookmarks, scheduled posts, analytics, and more. The difficult part wasn't implementing each feature individually; it was handling what happens between them.

For example, a comment can trigger a notification, affect the activity feed and unread count, while pagination and real-time SSE still need to remain consistent. Scheduled posts similarly introduced interactions between post status, editor state, cron execution, and timezone handling.

Real lesson

Designing a feature means mapping its effects across the existing system, not just building its API and UI.

02

Background work needs its own architecture

The notification and email system taught me a different lesson from normal request-response development. Sending emails directly from an API route would couple user actions to an external service and make bursts, retries, and failures harder to manage.

That led to separating event creation → Redis-backed buffering → BullMQ processing → digest/rate limiting → Resend delivery. Redis wasn't introduced simply because it was fast; it solved a specific problem: temporarily storing and controlling work that didn't need to happen during the user's request.

Real lesson

Once work becomes asynchronous, it needs its own architecture for retries, rate limits, temporary state, and failure handling.

03

Security has to be designed around data flow, not individual endpoints

The security audit changed how I looked at StoryHub. Fixing individual vulnerabilities wasn't enough because the same user-controlled data could pass through multiple layers—editor content, API input, database storage, rendering, file uploads, redirects, authentication tokens, and user-specific resources.

That led to measures such as centralized HTML/plain-text sanitization, SVG upload restrictions, IDOR checks, session-version-based token invalidation, redirect validation, and reducing unnecessary exposure of user data.

Real lesson

Security isn't a final checklist applied to routes; you have to trace where untrusted data enters, how it changes, where it is stored, and where it eventually becomes trusted or rendered.

04

Production constraints change engineering decisions

Several StoryHub decisions only became obvious when the application had to behave outside localhost. Vercel's cron limitations pushed scheduled publishing toward an external scheduler. Differences between local and production timezones exposed assumptions in date handling. Redis command limits made me pay closer attention to background-system resource consumption. Email delivery also required moving from Mailtrap during development to Resend with domain verification in production.

Real lesson

An implementation that works locally isn't necessarily a production solution. Hosting limits, third-party quotas, timezone differences, external-service failures, and operational constraints have to become part of the architecture—not problems discovered after deployment.

Future Roadmap

Where StoryHub could evolve next.

StoryHub

Personalized Topic Recommendations

A recommendation system that helps readers discover stories based on the topics they consistently read and engage with. Instead of relying only on general popularity, StoryHub could use signals such as reading activity, likes, bookmarks, follows, and topic preferences to surface more relevant content.

Real-Time User Messaging

A direct messaging system that allows readers and writers to communicate privately within StoryHub. This would extend the existing social graph beyond public interactions, giving users a dedicated space for conversations, with features such as real-time messages, conversation history, and message notifications.

Explore the project

See the implementation.

Explore the source code or experience StoryHub yourself.