Inspiration

It started with seven years of a "coffee diary." Since 2019, I had logged every cup I brewed — bean, water temperature, water volume, brew time, and a score — in a spreadsheet. It had grown to roughly 2,300 cups and about 200 beans. But data asleep inside a CSV is hard to revisit and harder to learn from.

I wanted to bring those records back to life as a real app. A hackathon built around v0 and an AWS database was exactly the push I needed: ship the UI fast, but give the data a foundation that lasts. The look from v0, the data backbone from AWS — that pairing matched my wish perfectly.

How I built it

  1. Generated the UI with v0: Next.js 16 (App Router) + React 19 + Tailwind, with screens for the bean collection, brew records, shops, and a dashboard.
  2. Added an AWS backend: DynamoDB (single-table design: PK=USER#me, SK=BEAN#/BREW#/SHOP#) for persistence, and S3 (direct upload/display via presigned URLs) for photos. Writes go through Next.js Server Actions.
  3. Migrated seven years of data: Read two CSVs (Shift_JIS / UTF-8) with a hand-written RFC4180 parser, transformed them into ~2,300 brews and ~200 beans, and loaded them. Scores (0–100) were band-mapped to stars (1–5) based on the actual distribution.
  4. Grew features one at a time: purchase history (repeat purchases), owner-only notes, full editing of every field, shop URL and address-to-map pins, a spending dashboard, and a Japanese/English UI toggle.
  5. Shipped it: Pushing to main auto-deploys on Vercel. The map uses Leaflet + OpenStreetMap, and address-to-coordinates uses Japan's GSI API (both free).

Throughout, I kept the UI and database tightly bound through TypeScript types and a clear update flow. BeanRecord/BrewRecord are shared between server and client; adding a field means updating type, DB, and UI together — so what you see never drifts from what's stored.

What I learned

  • DynamoDB is a great companion for solo development. On-demand billing means zero cost while idle, and its schemaless nature let me add purchaseDates, notes, shop address/lat/lng, and url incrementally with no migrations — the data model evolved in lockstep with the UI.
  • Designing from access patterns. With single-table design, I could clearly see the path to expand: widen PK to USER#<id> for multi-user, and split shop data into a shared partition to become a "community coffee map."
  • The pitfalls of the RSC server/client boundary. Exporting a constant from a "use client" file and using it on the server turned it into a "client reference," which broke cookies().get(). Lesson learned: keep shared constants in a server-safe module.
  • Real data is messy. A broken join column in the CSV made every bean share the same price and purchase date, and the process method was scattered across other columns and bean names. I learned to verify rather than trust, and to track down the correct source columns.

Challenges I ran into

  • Encoding and parsing: The bean DB was Shift_JIS, and comments contained commas, newlines, and emoji. I wrote an RFC4180-compliant parser to eliminate column misalignment.
  • The build wouldn't pass: A packageManager pin (pnpm 11) in package.json conflicted with the lockfile (pnpm 10), failing the Vercel build. I aligned the versions and standardized on pnpm.
  • Permission constraints: The IAM user lacked dynamodb:UpdateItem, so partial updates hit AccessDenied. I switched to a Get+Put read-modify-write so it works within the existing IAM policy.
  • An i18n SSR bug: The locale cookie couldn't be read on the server, so the UI wouldn't switch. The root cause was the RSC constant issue above; moving the constant to a server-safe file fixed it.
  • Japanese addresses didn't map: Nominatim (OSM) is weak on Japanese street-level addresses and returned nothing. I switched to GSI's API first, plus full-width normalization.
  • Unusable on mobile: Edit/delete buttons were hover-only and invisible on phones. I made them always visible on small screens and added a confirmation dialog for deletes.

What's next

From a personal logbook to a coffee map where shop locations and reviews are shared by everyone. Each person's logs stay private while only shop info is contributed to the community — an expansion that DynamoDB's access-pattern design supports on the very same foundation.

Built With

  • dynamodb
  • s3
  • versel
Share this project:

Updates