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
- 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.
- 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. - 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.
- 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.
- Shipped it: Pushing to
mainauto-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, shopaddress/lat/lng, andurlincrementally 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
PKtoUSER#<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 brokecookies().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
packageManagerpin (pnpm 11) inpackage.jsonconflicted 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
Log in or sign up for Devpost to join the conversation.