<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: fatik07</title>
    <description>The latest articles on DEV Community by fatik07 (@fatik07).</description>
    <link>https://dev.to/fatik07</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F534182%2Fab0cd111-a5ad-496a-82ee-2d4dbf301098.png</url>
      <title>DEV Community: fatik07</title>
      <link>https://dev.to/fatik07</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fatik07"/>
    <language>en</language>
    <item>
      <title>What's in My Fridge? — An Open-Source AI Cooking Assistant I Built for a Friend</title>
      <dc:creator>fatik07</dc:creator>
      <pubDate>Mon, 05 Oct 2026 04:59:31 +0000</pubDate>
      <link>https://dev.to/fatik07/whats-in-my-fridge-an-open-source-ai-cooking-assistant-i-built-for-a-friend-12ge</link>
      <guid>https://dev.to/fatik07/whats-in-my-fridge-an-open-source-ai-cooking-assistant-i-built-for-a-friend-12ge</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A friend of mine has a habit: they open the fridge, stare at a handful of random ingredients — some eggs, half a chicken, a bag of rice — and just... close it again. Not because there's nothing to cook, but because turning "what I have" into "what I could make" takes a kind of mental effort that's easy to skip when you're tired.&lt;/p&gt;

&lt;p&gt;So for this weekend's Hacktoberfest challenge — &lt;strong&gt;Build for a Friend&lt;/strong&gt; — I built them exactly that: &lt;strong&gt;What's in My Fridge?&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tell me what you have. I'll tell you what you can cook.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You add the ingredients sitting in your kitchen, optionally tell it something like "no spicy food" or "only 15 minutes," hit one button, and get back a handful of realistic meal ideas — each one clearly split into what you already have and what you'd still need to buy.&lt;/p&gt;

&lt;p&gt;No chat window. No meal-planning dashboard. Just: ingredients in, meal ideas out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo &amp;amp; code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/fatik07/my-frigde" rel="noopener noreferrer"&gt;github.com/fatik07/my-frigde&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; not deployed yet — this was built and tested locally during the challenge window. The README has full setup instructions if you want to run it yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why open matters here
&lt;/h2&gt;

&lt;p&gt;The AI model is the core feature of this app — not a bolt-on. It runs through &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;, defaulting to &lt;code&gt;openrouter/free&lt;/code&gt;, OpenRouter's router that automatically picks a free, open model for each request.&lt;/p&gt;

&lt;p&gt;Why that choice specifically, and not a single proprietary model locked behind one vendor's API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; A friend using this shouldn't need me to keep a paid API key topped up. &lt;code&gt;openrouter/free&lt;/code&gt; means the running cost is zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It isn't locked to one model.&lt;/strong&gt; The entire AI call is isolated behind one small module (&lt;code&gt;src/server/ai/adapter.ts&lt;/code&gt;). Swapping to a different open model — or a different provider entirely — is a one-line change, not a rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rest of the app doesn't know or care which model answered.&lt;/strong&gt; Everything downstream just sees validated, typed JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be precise about what "open" means in this build: inference doesn't run locally on my friend's laptop — it still happens on OpenRouter's infrastructure. What open buys here is &lt;em&gt;choice and cost&lt;/em&gt;, not offline/local inference. I think that's a legitimate and underrated version of "open matters": it's the difference between a project my friend can actually keep running for free, versus one that quietly stops working the day my API credits run out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Stack: &lt;strong&gt;TanStack Start&lt;/strong&gt;, TypeScript, &lt;strong&gt;TanStack Router&lt;/strong&gt;, &lt;strong&gt;TanStack Query&lt;/strong&gt;, &lt;strong&gt;TanStack AI&lt;/strong&gt; (&lt;code&gt;@tanstack/ai&lt;/code&gt; + &lt;code&gt;@tanstack/ai-openrouter&lt;/code&gt;), &lt;strong&gt;Supabase&lt;/strong&gt; (Postgres), &lt;strong&gt;OpenRouter&lt;/strong&gt;, Tailwind CSS.&lt;/p&gt;

&lt;p&gt;The flow is deliberately boring and linear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ingredients (Supabase) → AI call (server-only) → OpenRouter → structured JSON → reconciled against pantry → rendered meal cards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few pieces worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The AI call never touches the browser.&lt;/strong&gt; It's a TanStack Start server function, and the OpenRouter key only ever lives in &lt;code&gt;process.env&lt;/code&gt; on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMealSuggestionAdapter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;OpenRouterTextAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENROUTER_API_KEY&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENROUTER_API_KEY is not configured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AI_MODEL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openrouter/free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOpenRouterText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. The response is schema-validated, not trusted prose.&lt;/strong&gt; &lt;code&gt;chat()&lt;/code&gt; takes a Zod &lt;code&gt;outputSchema&lt;/code&gt;, so the model either returns a typed object or the call fails loudly — with a plain-JSON fallback for models that don't support strict structured output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AiSuggestionsResponseSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. The model's claims get fact-checked against the real pantry.&lt;/strong&gt; This was the part I cared about most: an AI that says "uses: chicken" when you don't actually have chicken is worse than useless. So every suggestion gets reconciled server-side — anything the model claims as "used" that isn't genuinely in your pantry gets demoted to "missing" before it's ever rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcileWithPantry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MealSuggestion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pantryNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// anything claimed as "used" that isn't actually in the pantry&lt;/span&gt;
  &lt;span class="c1"&gt;// gets moved to "missing" instead — the model can't fake what you have&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard stuff rounds it out: empty-pantry state, a loading state ("Thinking of something delicious…"), and a retry-able error state that never leaks a raw API error to the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Given more time, the next step is exactly what the theme asks for: hand it to my friend and see what breaks. Right now it's been built and exercised locally, but not yet used by the person it's actually for — that's the real test, and I'll be reporting back on what they said.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI disclosure
&lt;/h2&gt;

&lt;p&gt;This project and this post were built with heavy assistance from an AI coding agent (Claude Code) — architecture, implementation, debugging version mismatches in the TanStack Start/Router packages, and writing this post. I reviewed and verified the code and claims above myself.&lt;/p&gt;




&lt;p&gt;Built solo for the &lt;strong&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/strong&gt; (&lt;code&gt;#hf26challenge&lt;/code&gt;).&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
