DEV Community

Cover image for Hack, Reflect, Look Forward: Building JH Link with Google Gemini
OLIVIER VANHESTE
OLIVIER VANHESTE

Posted on

Hack, Reflect, Look Forward: Building JH Link with Google Gemini

Built with Google Gemini: Writing Challenge

This is a submission for the Built with Google Gemini: Writing Challenge

What I Built with Google Gemini

For the DEV Weekend Challenge: Community, I built JH Link — a Progressive Web App with the tagline "Your youth centre, in your pocket."

The app serves my local youth center, which runs activities for members aged 10–19 (and welcomes the wider community). Communication was fragmented: events were word-of-mouth, check-ins were manual, and members had no persistent sense of engagement when they weren't physically present.

JH Link solves that with:

  • Activity Feed — filterable, searchable event list with real-time capacity indicators and one-tap registration or waitlist joining
  • Digital Member Card — a QR code (built with qrcode.react) that encodes <appUrl>/admin/checkin?uid=<userId>, scannable by staff directly in the app
  • Gamification — an XP and badge system: attendance earns points, milestones at 50 / 100 / 250 / 500 / 1000 XP, and four automatic badges (First Timer → Regular → Veteran → Legend) plus a bonus for completing your profile
  • Dashboard — personalized home screen with an XP progress bar, badge shelf, and next 3 upcoming events
  • Admin Dashboard — four panels: Members (activate/reject), Points (manual award/deduct), Activities (create/edit/cancel), and Check-In (QR scanner that marks attendance and triggers the XP award)

The stack: Next.js (App Router), Supabase (auth + PostgreSQL with RLS + Storage), Tailwind CSS, Vercel. Built entirely inside a VS Code Dev Container using bun.

Before writing a single line of code, I ran a series of sparring sessions with Gemini on the web to settle the architecture. I described the problem space, the target audience, and the constraints (offline-capable PWA, minimal ops overhead, fast solo build), and we worked through the tradeoffs together — why Supabase over a custom backend, why Next.js App Router over Pages, how to structure RLS for a mixed member/admin user model, and what the gamification layer should look like at a schema level. Those early conversations produced the architecture document and the database schema before I opened VS Code.

Google Gemini was my co-developer throughout the entire weekend build. Every feature started as a conversation. I'd describe what I needed, Gemini would produce a draft, I'd review it, challenge it, and we'd iterate. The result was a feature-complete application shipped in roughly two days.

Demo

Live demo: https://jh-link.vercel.app/login

JH Link Demo Video

What I Learned

Technical skills

Supabase RLS is subtle and unforgiving. I came into the weekend thinking Row Level Security would be straightforward. In practice, a misconfigured policy fails silently — the query just returns zero rows instead of an error. The trickiest part was the gamification layer: awarding points and badges needs to happen whether triggered by a member action or an admin check-in. The solution was a service-role client that bypasses RLS for those writes, but that meant I had to be very deliberate about where that client is used, since a service-role key in the wrong place would be a serious security leak. Gemini helped me think through the permission model, but I still had to manually review every RLS policy and service-role call.

Next.js App Router has sharp async edges. The mix of Server Components, Client Components, and Server Actions creates subtle boundaries. Several bugs came from mis-placed awaits or from a Server Action not having access to cookies the way I expected. One concrete example: the three Supabase client factories (client.ts, server.ts, middleware.ts) each exist for a specific context, and using the wrong one causes silent auth failures. These are the kinds of bugs where the error message points you nowhere near the root cause.

PWA edge cases are platform-specific and fiddly. The service worker cache invalidation strategy that works on Android often behaves differently on iOS Safari — especially around the install prompt and offline fallback pages. The generated sw.js was a solid starting point but needed manual tuning for iOS.

Parallel data fetching matters. The activities page fetches registration counts, the user's own registrations, and their waitlist entries for the rendered activity IDs. Doing those three queries in sequence would be noticeably slow. Gemini suggested wrapping them in Promise.all() immediately, which I kept — and it made a real difference in perceived load time.

Soft skills & unexpected lessons

Prompt quality directly determines output quality. The clearest lesson of the weekend: vague prompts produce vague code. When I started framing requests as "I need a Server Action that registers a user for an activity, enforcing membership, capacity, and deadline constraints, and returning a typed result object", the output was immediately usable. When I said "add registration logic", it was a mess. Treating Gemini like a senior collaborator who needs full context — not a magic code button — changed everything.

Never merge generated security-sensitive code without reading it. The RLS policies and the service-role client usage are the two places where a bug isn't just a broken feature — it's a data exposure or privilege escalation. I learned to treat those sections with the same scrutiny I'd apply to a security audit, regardless of how confident the explanation sounded.

Speed creates scope creep. When you can generate features quickly, the temptation is to keep adding. I had rough implementations of a member messaging system and social reactions on activities before I cut them. Being fast doesn't mean you should keep going — knowing when to stop is a skill the AI can't help you with.

Google Gemini Feedback

What worked well

Context retention within a long session was excellent. I could say "remember that the gamification runs via the service-role client" and Gemini would correctly propagate that constraint into the next feature's implementation without me restating it. This made multi-step features feel like a real dialogue.

Error reasoning saved significant debugging time. When Supabase returned a cryptic error code, Gemini's ability to reason about what the error likely means in this context, rank the possible causes, and suggest targeted fixes was faster than reading documentation cold. This happened several times with RLS policy errors and with the pkce auth flow callback.

Boilerplate for well-known patterns was fast and accurate. The three Supabase client factories, the useTransition-based optimistic UI on the registration button, the next/font/google setup, the PWA metadata in layout.tsx — all of these are patterns that are tedious to type out correctly but well within Gemini's training data. Getting them right on the first pass was a genuine time saving.

Code organisation suggestions. When I dumped a large feature request in one go, Gemini would often propose a sensible file split before writing any code. It suggested separating ActivityCard.tsx from RegistrationButton.tsx so the card could be a server-renderable component while only the button needed to be a Client Component. That's the kind of structural thinking I appreciated.

Where there was friction

Hallucinated API signatures. On a couple of occasions, Gemini generated code using Supabase SDK methods or options that don't exist in the version I was using — most notably around the count query option syntax. The code compiled fine and failed at runtime with an opaque error. This is the most dangerous failure mode: confidently wrong code that looks right. Cross-referencing the actual Supabase docs became a habit.

Over-engineering on the first pass. When I asked for the gamification system, the initial output included an abstraction layer — a GamificationEngine class with a plugin interface for future reward types. I needed a single awardPoints() function that works now. Asking for simplicity became a recurring step; the phrase "write the simplest version that satisfies these requirements" was something I used often.

Debugging loops that don't converge. If a bug had multiple possible causes, Gemini would sometimes propose a fix, I'd report it didn't work, and the next response would be a marginally different version of the same fix — without genuinely reconsidering the root cause. These loops required me to hard-reset: close the conversation, write a fresh prompt that described the symptom from scratch, and reframe the problem. That usually broke the loop, but it cost time.

Occasional confidently wrong explanations. A few times, Gemini explained why something worked in a way that was plausible-sounding but subtly incorrect. I caught these by testing the claim against the actual behavior or the docs. The risk is not catching them — you end up with a mental model of your own codebase that's wrong.

The VS Code extension ignored my project structure. This was a persistent and frustrating friction point with the Gemini extension in VS Code: it consistently created new files under the repository root instead of under src/, regardless of how I phrased the request. Every file it scaffolded — components, actions, route handlers — had to be manually moved or I had to explicitly spell out the full path in the prompt every single time. The web interface never had this problem; it was specific to how the extension interpreted workspace context. It wasn't a showstopper, but it broke flow repeatedly across the entire build.

The honest verdict

Building a feature-complete application in a weekend would not have been possible to the same standard without Gemini. The productivity gain on well-defined, boilerplate-heavy work is real and large. But it's a force multiplier for someone who already understands the domain — not a substitute for understanding it. Every security-sensitive section required me to be fully present and skeptical. Every place I accepted output on trust, I eventually paid for.

The two improvements I'd most want to see: better API version awareness (or at least a clear caveat when generating code against a specific SDK version) and a more deliberate root-cause debugging mode that explicitly backtracks rather than proposing incremental variants of a failing approach.

Top comments (0)