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


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 storePOST
– Create a new task, store it, and broadcast itPATCH
– Mark a task complete and broadcast the updateDELETE
– 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 maintainedOn 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!
Subscribe to my newsletter
Read articles from Ryan Bell directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
