<?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: Iqbal Taufiqurrochman</title>
    <description>The latest articles on DEV Community by Iqbal Taufiqurrochman (@iiqbalt).</description>
    <link>https://dev.to/iiqbalt</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%2F960754%2F7620ca66-f17a-4f26-a05f-ef3cf7d6d330.png</url>
      <title>DEV Community: Iqbal Taufiqurrochman</title>
      <link>https://dev.to/iiqbalt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iiqbalt"/>
    <language>en</language>
    <item>
      <title>ChatTodo: A Chat Box That Stops My Friend Fatik From Double-Booking Himself</title>
      <dc:creator>Iqbal Taufiqurrochman</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:29:20 +0000</pubDate>
      <link>https://dev.to/iiqbalt/chattodo-a-chat-box-that-stops-my-friend-fatik-from-double-booking-himself-jjo</link>
      <guid>https://dev.to/iiqbalt/chattodo-a-chat-box-that-stops-my-friend-fatik-from-double-booking-himself-jjo</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ChatTodo&lt;/strong&gt; is a chat-first todo app for my friend &lt;strong&gt;Fatik&lt;/strong&gt;, who meets a lot of people and keeps making appointments — client meetings, workouts, catch-ups. His problem is simple: he forgets. And when he forgets, his appointments collide — a gym session he just promised lands on top of a meeting he already made.&lt;/p&gt;

&lt;p&gt;He's also not going to adopt a heavyweight task manager. He'd rather type a message than fight a due-date picker. So I didn't build him another app with forms. I built him &lt;strong&gt;a chat box&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;He types the way he talks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"besok jam 9 rapat Budi, jam setengah 10 olahraga di gym"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ChatTodo turns that into two dated todos, checks them against everything already on his plate, and warns him on the spot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bentrok jadwal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Rapat sama Budi" (Sen, 5 Okt, 09.00) vs "Olahraga di gym" (Sen, 5 Okt, 09.30)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Under the chat there's a simple dashboard: how many are pending, which are overdue, what's coming in the next seven days — enough for Fatik to glance at in the morning and close the tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem it solves for Fatik:&lt;/strong&gt; he's the person who promises a friend a workout session and then realizes it clashes with a meeting he already agreed to. He told me he needs something to watch his appointments for him, but it has to be effortless — one box, one send button, no app to "keep up with".&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; [DEPLOY URL]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video demo:&lt;/strong&gt; [RECORDING LINK]&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://gitlab.com/iqbal_taufiqurrochman/chat-todo" rel="noopener noreferrer"&gt;gitlab.com/iqbal_taufiqurrochman/chat-todo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Setup is three commands: &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;cp .env.example .env&lt;/code&gt; (plus your Neon connection string), &lt;code&gt;npm run db:push&lt;/code&gt;. The README walks through every environment variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; SvelteKit 3 + Svelte 5 (runes), Tailwind CSS 4, Neon Postgres via Drizzle ORM, deployed on Vercel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The open-source AI at its core:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-weight model via OpenRouter&lt;/strong&gt; — the default is &lt;code&gt;google/gemma-3-27b-it&lt;/code&gt; (Gemma is Google's open-weight family). Every chat message goes through it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI-compatible client layer&lt;/strong&gt; — a thin abstraction (&lt;code&gt;src/lib/ai/client.ts&lt;/code&gt;) where &lt;code&gt;baseURL&lt;/code&gt; and model are just two settings. Point it at OpenRouter's hosted open-weight model, or at a local &lt;strong&gt;Ollama&lt;/strong&gt; server, without touching a single line of application code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured extraction, not vibes&lt;/strong&gt; — each message runs in JSON mode against a &lt;strong&gt;zod schema&lt;/strong&gt; (&lt;code&gt;{ type: "create" | "list" | "complete" | "delete" | "chat", ... }&lt;/code&gt;). The model never writes to the database directly; it proposes a typed action, my server validates it, and only then does it execute. Bad JSON = rejected = safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How a message flows:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The message lands in &lt;code&gt;/api/chat&lt;/code&gt; and is persisted to the &lt;code&gt;messages&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;The last 10 messages + the current todo list are sent to the model (so "selesai yang rapat" finds the right todo)&lt;/li&gt;
&lt;li&gt;The model returns one structured action; zod validates it; the server executes it&lt;/li&gt;
&lt;li&gt;The confirmation streams back to the chat UI over SSE, and the dashboard updates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Conflict detection is mine, not the model's.&lt;/strong&gt; When a &lt;code&gt;create&lt;/code&gt; action lands, the server compares each new due time against every pending todo and flags anything within 60 minutes as &lt;code&gt;Bentrok jadwal&lt;/code&gt;. The model proposes; the server double-checks — so a hallucinating model can't quietly hide Fatik's next double-booking. I tested it by pointing the app at a fake OpenAI-compatible server returning back-to-back appointments: new-vs-new, new-vs-existing, and exact-duplicate times all get caught, while unrelated dates stay quiet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything that isn't the model is boring on purpose:&lt;/strong&gt; a &lt;code&gt;todos&lt;/code&gt; table, a &lt;code&gt;messages&lt;/code&gt; table, and a &lt;code&gt;settings&lt;/code&gt; key-value table. No agent framework, no vector database — the schema fits on an index card, which is exactly how Fatik's mental model of his own appointments works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Three things made this project possible &lt;em&gt;because&lt;/em&gt; the core is open:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;His data never has to land on a server he doesn't control.&lt;/strong&gt; The whole app can run against Ollama on a laptop: set &lt;code&gt;ai.base_url&lt;/code&gt; to &lt;code&gt;http://localhost:11434/v1&lt;/code&gt; on the settings page, and the same app now speaks to a model that never touches the internet. A closed API can't offer that — the request &lt;em&gt;is&lt;/em&gt; the data, and it has to leave the building.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap the model as a configuration change, not a rewrite.&lt;/strong&gt; Because the open-weight model speaks the OpenAI-compatible dialect, changing &lt;code&gt;ai.base_url&lt;/code&gt; + &lt;code&gt;ai.model&lt;/code&gt; on the settings page (no redeploy) moves the app between OpenRouter's hosted Gemma and a local Ollama model. Try that with a closed provider's SDK — you're refactoring imports, not editing a text field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run for him.&lt;/strong&gt; An open-weight model on a free-tier local runtime means his "second brain" has no subscription that can quietly lapse and take his appointments with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The honest trade-off: a closed model would probably parse messier sentences on the first try. What open bought me was control over exactly the things that mattered for &lt;em&gt;this&lt;/em&gt; user — his privacy, his budget, and the freedom to tune the extraction prompt (&lt;code&gt;ai.extraction_prompt&lt;/code&gt;, editable from the settings page) until it understood how &lt;em&gt;he&lt;/em&gt; actually talks, without shipping a new build for every experiment.&lt;/p&gt;

&lt;p&gt;[HANDOVER: what Fatik said after you gave it to him — the challenge gives bonus points for actually handing it over.]&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Overall prize only — this project doesn't use any partner technology.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>sveltekit</category>
    </item>
  </channel>
</rss>
