Inspiration

I kept forgetting what I'd bought — what I paid, where it was from, whether I even liked it. And when I wanted a second opinion, the reviews were scattered: Yelp for places, Amazon for its own catalog, Letterboxd for film. Nothing lets you review everything in one place and pool that signal across people. The spark was a single technical insight: if I embed every logged item, then my "ballpoint pen" and a stranger's "ballpoint pen" land near each other in vector space — so they can resolve to the same thing, and our reviews can aggregate, with no shared product ID, barcode, or agreed-upon spelling. That turns a private purchase tracker into a universal, cross-user review graph for anything.

What it does

OpinLog lets you log and rate anything. The moment you add an item, it's embedded and matched against a shared catalog — "is this one of these?" — so your entry links to a canonical item that every other user's matching entries also point to. The payoff is the canonical page: an aggregated rating and review count pooled across strangers. Search is semantic, so "burger" and "pen" both rank by meaning in the same catalog. It's monetizable by design: you earn points for contributing to the catalog and spend them to boost your review's visibility, buy more with Stripe (one-time or subscription), and attach affiliate links (with automatic disclosures) to earn on what you recommend. The catalog is self-governing too — crowd-sourced edits with an approve/revert queue, collaborative lists, and a contributor leaderboard.

How we built it

Next.js 16 (App Router, Server Components/Actions) on Vercel, backed entirely by Amazon Aurora PostgreSQL (Serverless v2). The matcher is pgvector with an HNSW cosine index; embeddings come from Amazon Bedrock Titan Multimodal Embeddings (1024-dim), which map text and images into one space so a photo of a burger matches the word "burger." Drizzle + node-postgres drive the DB, so the exact same code runs against local Docker Postgres and Aurora with a one-line DATABASE_URL swap. Each canonical item's embedding is the running centroid of its members, and rating aggregates are denormalized onto the canonical row (with a generated rating_avg column) to keep "sort by rating" index-fast. The points economy is an append-only ledger with a derived balance; Stripe billing credits through webhooks made idempotent by a partial-unique stripe_ref index. Photos live in S3, magic-link email runs on Amazon SES, and all AWS infra is provisioned with Terraform.

Challenges we ran into

The hardest part was tuning the matcher: collapsing genuine variants ("In-N-Out Double-Double" vs "in n out double double burger") into one canonical entry without over-collapsing distinct items (a pen and a burger must stay apart). That meant calibrating similarity thresholds and a relevance floor against Titan's actual cosine distribution. Drizzle doesn't model vector or geography types, so those queries are hand-written sql templates — including a geography custom-type quoting gotcha in drizzle-kit. Idempotency took real care on two fronts: stopping point-farming via unlink/relink (a unique index on (user_id, kind, canonical_item_id)), and stopping double-credits from replayed Stripe webhooks racing the success-redirect (a partial-unique stripe_ref). And a late catch — the judge demo login was defaulting to the earliest-created user (my own account) instead of a dedicated demo identity.

Accomplishments that we're proud of

The cross-user link works end-to-end: two different users logging the same burger collapse into one canonical entry with a rating averaged across both, while a pen logged by two users stays a separate entry — proven with real additions that run the real embed→match pipeline. I'm proud that one database does vector similarity, full-text fallback, relational joins, and the monetization ledger transactionally — no bolted-on search cluster. The multimodal matcher (photo ↔ word from a single model) makes the core demo feel like magic. And the monetization layer is genuinely shipped — points, Stripe Checkout + subscriptions + billing portal, and compliant affiliate links — not a mock.

What we learned

That designing idempotency at the schema level (unique indexes) is far more robust than guarding it in application code. That derived state — computing a points balance from a ledger instead of storing it — eliminates a whole class of drift bugs. A lot about pgvector/HNSW and how to read an embedding model's similarity distribution to set thresholds that mean something. That multimodal embeddings collapse "text search" and "image search" into one cheap path. And, most of all, that keeping vectors and relations in one Aurora engine is what made a two-sided, monetized network buildable by one person on a deadline.

What's next for OpinLog

Shipping the two features that are built but held back from this submission while they're polished: the business catalog portal (bulk CSV upload that seeds canonical items to solve cold-start) and PostGIS "near me" location search ("best burger near me," ranked by relevance and distance — the schema and queries already exist). Beyond that: receipt OCR to auto-fill items, canonical merge/dedupe tooling for moderators, follows and notifications, business-side analytics, and a sponsored-placement marketplace — plus SES production access and mobile polish.

Built With

Share this project:

Updates