DEV Community

Iqbal Taufiqurrochman
Iqbal Taufiqurrochman

Posted on

ChatTodo: A Chat Box That Stops My Friend Fatik From Double-Booking Himself

This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend

What I Built

ChatTodo is a chat-first todo app for my friend Fatik, 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.

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 a chat box.

He types the way he talks:

"besok jam 9 rapat Budi, jam setengah 10 olahraga di gym"

ChatTodo turns that into two dated todos, checks them against everything already on his plate, and warns him on the spot:

Bentrok jadwal:

  • "Rapat sama Budi" (Sen, 5 Okt, 09.00) vs "Olahraga di gym" (Sen, 5 Okt, 09.30)

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.

The problem it solves for Fatik: 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".

Demo

  • Live: [DEPLOY URL]
  • Video demo: [RECORDING LINK]

Code

Repo: gitlab.com/iqbal_taufiqurrochman/chat-todo

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

How I Built It

Stack: SvelteKit 3 + Svelte 5 (runes), Tailwind CSS 4, Neon Postgres via Drizzle ORM, deployed on Vercel.

The open-source AI at its core:

  • Open-weight model via OpenRouter — the default is google/gemma-3-27b-it (Gemma is Google's open-weight family). Every chat message goes through it.
  • OpenAI-compatible client layer — a thin abstraction (src/lib/ai/client.ts) where baseURL and model are just two settings. Point it at OpenRouter's hosted open-weight model, or at a local Ollama server, without touching a single line of application code.
  • Structured extraction, not vibes — each message runs in JSON mode against a zod schema ({ type: "create" | "list" | "complete" | "delete" | "chat", ... }). 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.

How a message flows:

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

Conflict detection is mine, not the model's. When a create action lands, the server compares each new due time against every pending todo and flags anything within 60 minutes as Bentrok jadwal. 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.

Everything that isn't the model is boring on purpose: a todos table, a messages table, and a settings 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.

Why Does Open Innovation Matter?

Three things made this project possible because the core is open:

  1. His data never has to land on a server he doesn't control. The whole app can run against Ollama on a laptop: set ai.base_url to http://localhost:11434/v1 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 is the data, and it has to leave the building.
  2. I can swap the model as a configuration change, not a rewrite. Because the open-weight model speaks the OpenAI-compatible dialect, changing ai.base_url + ai.model 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.
  3. It costs nothing to run for him. 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.

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 this user — his privacy, his budget, and the freedom to tune the extraction prompt (ai.extraction_prompt, editable from the settings page) until it understood how he actually talks, without shipping a new build for every experiment.

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

Prize Categories

Overall prize only — this project doesn't use any partner technology.

Top comments (0)