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 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:
- Client calls
POST /api/document/upload-url→ backend creates aPENDINGrow and returns a presigned POST policy. - Client uploads directly to MinIO.
- MinIO fires a webhook to
POST /api/document/webhook→ backend marks the documentSUCCESS(or handles failures). - 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
PENDINGdocuments 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:
- Migrated the application and database queries from MongoDB to PostgreSQL.
- Completed the application cutover toward PostgreSQL.
- Realized the target PostgreSQL database did not contain the existing data — I had forgotten to copy MongoDB data before switching over.
- 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.
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.
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.
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.
Idempotency for Collection Writes
Unique idempotency keys on collection mutations make retries safe without needing the deeper concurrency controls required in transaction systems.
Shared Authentication Infrastructure
Delegating auth to Auth Service removed duplicated login code and kept JWT verification consistent across Resource Hub, IdeaHub, and Vaultex.
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
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 labels
Collection 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
Last updated August 10, 2026