Iskommerce is a university-only C2C marketplace designed to help students buy, sell, and exchange items within a trusted academic community.
At UP Visayas, student commerce already existed. Students bought and sold through Facebook groups, Messenger, and posts tagged #ForSale, #LF, or #mine.
The problem wasn’t that students had no way to trade, it’s that the marketplace ran on tools never designed for it: finding items meant scrolling posts, negotiations got scattered across conversations, and no one could tell if something was still available.
Why we built it
Facebook groups worked because they were familiar, but sellers had to keep reposting to stay visible, buyers had to search manually, and trust depended entirely on personal judgment.
Instead of competing with general marketplaces, we leaned into the advantage of a smaller network: everyone’s already a student, so trust features like UP email verification and student profiles could be built directly into the system. The goal wasn’t another place to post items, it was making the existing process more structured.
How it turned out
Structured listings, not social posts
Each item is a persistent listing with defined fields (price, condition, category, availability) instead of a post that gets buried, discoverable through feeds, search, filtering, and sorting.
Search that understands what you meant, not just what you typed
Keyword search breaks down fast in a small marketplace. A student searching “laptop bag” should also see listings titled “backpack for laptop” or “sling bag, fits 14-inch,” even with no matching words.
So listings are embedded, not just indexed. Each one gets a 384-dimension vector from a local sentence-transformer model (all-MiniLM-L6-v2, run in-process through Transformers.js) on create or edit.
Vectors live in Postgres via pgvector with an HNSW index for fast nearest-neighbor lookups. A search query gets embedded the same way and compared by cosine distance. “Similar listings” on an item page is just that lookup run against the listing’s own vector.
No hosted embeddings API needed, the model runs on the same machine as the API.
A homepage that learns what you’re into
Every view, click, like, message, and purchase gets logged as an event. Those roll up into a per-user preference vector, weighted toward stronger signals like purchases and decaying over time.
Ranking the homepage is the same nearest-neighbor lookup as search, just against a user’s vector instead of a query, mixed with a recency pool so it doesn’t collapse into one category. New users with no history fall back to recency alone.
This is content-based filtering, which fits a marketplace this size better than collaborative filtering, which needs a lot of users behaving similarly before it’s useful.
Conversations and trust
Each listing has its own conversation thread between buyer and seller instead of moving straight to Messenger, so negotiation history stays attached to the item. Reviews are tied to completed transactions, so trust doesn’t rest entirely on social reputation.
Modeling real transactions
The hardest part for me was representing what happens when real people interact with listings. A listing can’t be sold twice, a buyer shouldn’t reserve something forever, a review should only exist after a completed transaction. That led to transaction states like available, reserved, sold, and archived, and thinking through them surfaced edge cases like what happens when a buyer changes their mind or multiple people want the same item.
How we built it
A modular system that mirrors the product
Iskommerce is a modular monolith with a shared TypeScript codebase, split into feature-based modules: authentication, listings, messaging, transactions, reviews, notifications. The structure was inspired by Django apps, each feature has a clear boundary but still works together as one system.
This was also my first full-stack app with TypeScript on both ends. The stack:
| Layer | Stack |
|---|---|
| Frontend | Next.js, Tailwind CSS, shadcn/ui |
| Backend | NestJS, TypeScript |
| Database | PostgreSQL, Prisma, pgvector |
| Search/ML | Transformers.js, local sentence-transformer |
| Monorepo | Turborepo |
| Deployment | Vercel, Supabase |
Using TypeScript everywhere made sharing types and validation rules much easier, shared packages let the frontend and backend use the same schemas instead of duplicating definitions.
What I learned
It’s easy to see an inefficient process and assume the fix is a better version, completely ignoring why people used the old one.
Students already had a marketplace: Facebook groups. It’s messy, yeah, but it’s familiar. The lack of structure was a problem, but that same convenience is what made it stick.
A product’s biggest competitor is often the status quo: the default behavior people already have.
