Skip to main content
You're offline. Create, edit, and delete are not supported while offline.

Resource Hub

7 min readLast updated August 10, 2026
Active Project
Type:
Personal Tool
Status:
Active
Project:
Solo Project
Table of contents

Quick Story

Resource links were scattered across different platforms and difficult to find when needed.

I started Resource Hub as a personal link manager. Over time it grew into a fuller platform — public resource discovery, bookmarking, curated collections with custom item statuses, document uploads, and public collection profiles at /u/:username/:slug.

Resource Hub home page — landing page with resource search, public/private links, and community discoveryResource Hub home page — landing page with resource search, public/private links, and community discovery

Overview

Resource Hub is a centralized platform for storing, organizing, and sharing learning resources — links, bookmarks, ordered collections, and documents.

The backend is built with Express 5, TypeScript, PostgreSQL, and MinIO. The React frontend handles resource management, public discovery, and collection profiles. Authentication is delegated to my shared Auth Service.

Technical Decisions & Tradeoffs

1. Auth Service Delegation

Instead of maintaining authentication logic inside Resource Hub, I integrated my shared Auth Service.

The backend expects an HTTP-only cookie containing a JWT signed by Auth Service. Tokens are verified locally using RS256 and JWKS — the same pattern used across my other personal products.

For a deeper explanation of the auth architecture, see the Auth Service case study.

2. Presigned URL Upload Architecture

Used presigned URLs for document uploads to MinIO.

Why?

Instead of:

Client → Server → Object Storage

Used:

Client → Object Storage

Reasoning

The server should not become responsible for transferring large files.

Benefits

  • Reduced server load.
  • Eliminated an unnecessary network hop.
  • Improved scalability for file uploads.

The server still creates a PENDING database row and returns the presigned POST policy — it orchestrates the upload without proxying the file bytes.

3. Webhook-Driven Document State

Document uploads follow an async confirmation flow:

  1. Client calls POST /api/document/upload-url → backend creates a PENDING row and returns a presigned POST policy.
  2. Client uploads directly to MinIO.
  3. MinIO fires a webhook to POST /api/document/webhook → backend marks the document SUCCESS (or handles failures).
  4. Client requests a short-lived presigned GET URL to view the file.

Why?

A client-reported "upload succeeded" is not enough. The backend should only mark a document as available after object storage confirms the file exists.

Benefit

The document lifecycle reflects actual storage state rather than optimistic client assumptions.

4. PostgreSQL + Drizzle ORM

Migrated the data layer to PostgreSQL with Drizzle ORM.

Why?

I wanted a typed schema, SQL migrations, and a relational model that could express collections, collection items, bookmarks, and documents cleanly.

Benefit

Foreign keys, unique constraints (e.g. per-user resource deduplication, collection idempotency keys), and structured queries replaced document-store patterns that were harder to enforce at the application layer.

5. Idempotency Keys on Collection Mutations

Collection create and item mutations accept idempotency keys stored as unique constraints in PostgreSQL.

Why?

Adding items to a collection or creating a collection should survive network retries and double-clicks without duplicating rows.

Benefit

Safe retry behavior on collection writes. For a deeper treatment of idempotency mechanics in another project, see the Vaultex case study.

6. pg-boss Background Jobs

Used pg-boss (PostgreSQL-backed job queue) for background work:

  • Link metadata fetch — on-demand Open Graph / metadata retrieval for resources.
  • Pending document cleanup — nightly cron to remove stale PENDING documents older than a configured threshold.

Why?

Both jobs fit naturally on the same PostgreSQL instance already running the application — no separate queue infrastructure for a personal tool.

Limitation

Job throughput is bounded by a single PostgreSQL deployment. Acceptable at my current scale.

7. guestAuth vs optionalAuth

Two auth middleware variants serve different route needs.

  • optionalAuth — requires a valid token (or dev bypass); used for most authenticated routes.
  • guestAuth — allows anonymous access; attaches user context when a valid token is present and silently ignores invalid/expired tokens.

Why?

Public collection reads (/u/:username/:slug) should work without forcing login, while still personalizing the experience when a user is signed in.

Tradeoff

Simpler public sharing, but guest routes need careful handling so invalid tokens never block anonymous reads.

8. Low-Friction Create, Enrich-on-Edit

Resource creation accepts only name and link. Description, tags, and visibility are set on the edit page.

Why?

Requiring every field at create time adds friction and reduces the chance a user saves a link in the moment.

Benefit

Capture first, organize later. New resources are always created as private; enrichment happens when the user has time.

Challenges & Mistakes

Database Migration Sequencing

Resource Hub originally used MongoDB. I decided to migrate the backend to PostgreSQL using Drizzle ORM.

The sequence I followed:

  1. Migrated the application and database queries from MongoDB to PostgreSQL.
  2. Completed the application cutover toward PostgreSQL.
  3. Realized the target PostgreSQL database did not contain the existing data — I had forgotten to copy MongoDB data before switching over.
  4. Ran the one-off data transfer script (migrate:mongo-to-pg) to move the existing records.

What I learned from this

Migrating application queries is not the same as migrating data.

Schema migration, data migration, application changes, validation, and cutover need to be planned together. Before pointing the application at a new database, I should verify that the target actually contains the expected data.

Debugging

Object Storage and Database Consistency on Delete

When deleting a document, the file is removed from MinIO before the database record is deleted.

Potential edge case

The file is deleted successfully, but the server crashes before the database deletion completes. The database record remains while the file no longer exists — an inconsistent state.

Current status

No orphan-record cleanup mechanism exists yet.

Future improvement

Background reconciliation or cleanup jobs to detect and remove orphaned database records.

Debugging

link vs sourceLink Naming

During frontend and backend evolution, create requests use link while GET responses and PATCH updates use sourceLink.

This is a minor developer-experience friction point — not a runtime bug, but something that caused confusion during API integration and migration.

What I Learned

Migration Sequencing

A successful database migration requires verifying data presence in the target before cutover — not just updating queries and schema.

Debugging

Presigned Uploads + Webhook Confirmation

Combining presigned client uploads with a webhook callback is a practical pattern for document lifecycle management without proxying files through the API server.

Debugging

Idempotency for Collection Writes

Unique idempotency keys on collection mutations make retries safe without needing the deeper concurrency controls required in transaction systems.

Debugging

Shared Authentication Infrastructure

Delegating auth to Auth Service removed duplicated login code and kept JWT verification consistent across Resource Hub, IdeaHub, and Vaultex.

Debugging

Background Reconciliation

Stale PENDING documents and unfetched link metadata are good candidates for scheduled background jobs rather than blocking the request path.

Future Vision

1. Subscription Model

Integrate a subscription model so users can upgrade to a paid tier on the platform when they need more than the free tier offers.

Evolved

Evolved

MongoDB → PostgreSQL + Drizzle

The backend was rewritten around PostgreSQL, Drizzle ORM, and SQL migrations. A one-off migrate:mongo-to-pg script handles legacy data transfer when both databases are reachable.

Collections and Public Profiles

What started as a link manager now supports curated, ordered collections with custom item statuses, public/private visibility, and shareable profile URLs at /u/:username/:slug.

Public collections browse page — community-curated resource lists with item counts and custom status labelsPublic collections browse page — community-curated resource lists with item counts and custom status labels

Collection detail page — ordered items with per-item status, add/edit/delete, and public profile URLCollection detail page — ordered items with per-item status, add/edit/delete, and public profile URL

Document Management

Added full document support — presigned POST uploads, webhook confirmation, presigned view URLs, MIME type allowlists, per-user quotas, and pending-document TTLs.

Auth Extraction

Removed inline authentication from Resource Hub and integrated the shared Auth Service, keeping Resource Hub focused on resource management rather than identity.

Backend Modernization

The backend moved to Express 5, TypeScript (ESM), Zod validation, rate limiting per route, and multi-stage Docker builds with GitHub Actions CI.

Technologies Used

Node.jsExpressTypeScriptPostgreSQLDrizzle ORMMinIOReactpg-bossAuth ServiceDocker

Last updated August 10, 2026

Related Projects

Lakshay Mahajan

Backend Engineer focused on building reliable systems with Node.js, TypeScript, and AWS.

Connect

© 2026 Lakshay Mahajan