What’s in the Bag? Building Fairway Tasks with Deno, Fresh, and SSE

Ryan BellRyan Bell
5 min read

In Part 1 of this series, I introduced the idea of looking at development stacks the way golfers look at their gear. Every tool in the bag has a purpose. Some we reach for without thinking. Others we try out, experiment with, and either adopt or toss aside. This week, I finally teed off on the first build: Fairway Tasks, a collaborative to-do app powered by Deno, Fresh, and Server-Sent Events (SSE).

The goal was to build a real-world demo that shows off the strengths of this stack—without turning it into a full-blown product. I wanted persistence, live updates, and minimal complexity.


What We’re Building

Fairway Tasks is a collaborative to-do list that allows anyone to:

  • Add tasks

  • Mark them as complete

  • See updates in real-time across all open tabs

There’s no login system, no multi-user tracking, and no external database. Just the core interactions and live state syncing.

But there is one key addition to the base demo: I used Deno’s built-in KV store to persist the data. It’s a small change from an in-memory store, but it showcases how batteries-included Deno really is.


The Stack

Here’s what I used:

  • Deno: runtime with built-in TypeScript, security, testing, and KV storage

  • Fresh: a modern web framework with island-based rendering

  • SSE (Server-Sent Events): for real-time updates

  • KV Store: Deno’s built-in persistence layer

Together, this gave me a stack with no extra tooling or dependency setup. Just native capabilities and good architecture choices.


Diving into the Code

Fairway Tasks has a minimal but purposeful codebase. Here’s a closer look at the moving parts and how they interact.

1. API Routes

All task interactions live in routes/api/tasks.ts. It handles:

  • GET – Return all tasks from the Deno KV store

  • POST – Create a new task, store it, and broadcast it

  • PATCH – Mark a task complete and broadcast the update

  • DELETE – Remove a task from storage and notify all clients

All task mutations trigger the broadcast() function from routes/api/stream.ts, sending the action over Server-Sent Events to connected tabs.

2. Server-Sent Events (SSE)

The real-time update system is handled via a simple SSE implementation:

  • Clients connect to /api/stream

  • A Set of writable clients is maintained

  • On any task change, a message is encoded and sent to each connected writer

export function broadcast(data: unknown) {
  const msg = `data: ${JSON.stringify(data)}\n\n`;
  for (const writer of clients) {
    writer.write(new TextEncoder().encode(msg));
  }
}

It’s lightweight, doesn’t require any external library, and perfect for small collaborative experiences.

3. Frontend Islands

The interactive frontend lives in islands/TaskInput.tsx, where:

  • Tasks are displayed from initial props

  • Users can add, complete, or delete tasks with a minimal UI

  • The island listens to the SSE stream and updates local state accordingly

With the recent addition of Jsonous, the SSE message decoding now looks like this:

useEffect(() => {
  const sse = new EventSource("/api/stream");
  sse.onmessage = (msg) => {
    sseEventDecoder.decodeJson(msg.data).cata({
      Ok: (event) => {
        if (event.type === "add") {
          setTasks((prev) => [...prev, event.task]);
        } else if (event.type === "complete") {
          setTasks((prev) =>
            prev.map((t) => t.id === event.id ? { ...t, completed: true } : t)
          );
        } else if (event.type === "delete") {
          setTasks((prev) => prev.filter((t) => t.id !== event.id));
        }
      },
      Err: (err) => {
        console.error(err);
      },
    });
  };
  return () => sse.close();
}, []);

This small change brings clarity, safety, and makes the SSE message format easier to evolve over time.

Bonus: Deno KV Integration

Persistence is handled with Deno’s built-in KV store using a utils/kv.ts helper module. Tasks are saved under a task:<id> key. Fetching is done with a simple kv.list() call.

export async function getTasks(): Promise<Task[]> {
  const entries = [];
  for await (const res of kv.list<Task>({ prefix: ["task"] })) {
    entries.push(res.value);
  }
  return entries;
}

The result? A real-time, collaborative app with no third-party DB or state store.

What Worked Well

  • Fresh’s simplicity: Routing, rendering, and hydration are all predictable and fast

  • SSE was perfect: Minimal setup, great fit for one-way sync like this

  • Deno’s DX: Linting, testing, and dev server all just work

  • KV store: Simple, stable, and lets the app persist between restarts with almost no config


What I’d Explore Next

  • Adding authentication using session cookies or tokens

  • Using external DBs if relational needs increase

  • More complex event handling (editing tasks, reordering, undo)

  • Offline sync and background queuing

  • More Robust SSE (something that scales horizontally and watches the db for changes)


Wrap-up

Fairway Tasks started as a small project—a swing at trying something simple, collaborative, and live. It turned into a rewarding dive into the capabilities of Deno and Fresh. Using only built-in tools like the KV store and SSE, I was able to build a functional app with real-time syncing and data persistence, all without touching external services or bolting on extra complexity.

The island-based approach of Fresh made it easy to separate server-rendered state from interactive components, and adding jsonous brought confidence to decoding live updates. It’s refreshing to build something that feels cohesive, productive, and fun.

There’s still room to grow—authentication, offline support, and more resilient streaming—but as it stands, Fairway Tasks is a solid example of what modern minimal stacks can accomplish.

You can try the app live on Deno Deploy, and view the source code on GitHub.

Next up? I might take a swing with Wasp, Effect, or even try a mobile-first build using Tamagui. Let me know what you'd like to see in the next post—and what’s in your bag these days.

Thanks for reading!

0
Subscribe to my newsletter

Read articles from Ryan Bell directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ryan Bell
Ryan Bell