DEV Community

Cover image for Rungset: Building an Open-Source Goal Tracker with Next.js, Cloudflare D1, and Better Auth
Ismail Courr
Ismail Courr

Posted on

Rungset: Building an Open-Source Goal Tracker with Next.js, Cloudflare D1, and Better Auth

Hacktoberfest: Maintainer Spotlight

Most goal-tracking applications eventually become one of two things: a glorified todo list or a large productivity system that requires almost as much work to maintain as the goals themselves.

I wanted Rungset to sit somewhere else.

Rungset is an open-source goal-planning application built around a simple hierarchy:

Goal → Milestone → Task → Completion → Check-in → Review → Adjust

The idea is that long-term goals should remain connected to the work being done today.

Instead of storing a goal in one place, tasks in another, notes somewhere else, and progress entirely in your head, Rungset keeps those pieces connected.

The project is currently in beta, fully open source, and built primarily with Next.js 16, React 19, TypeScript, Cloudflare D1, Drizzle ORM, Better Auth, and Cloudflare Workers.

With Hacktoberfest underway, this is a good time to explain how the project works technically, some of the architectural decisions behind it, and where other developers can contribute.

What Rungset actually does

The main data model is intentionally straightforward.

A user creates a goal.

That goal can contain milestones, which represent meaningful checkpoints.

Milestones can then be broken into actionable tasks.

The user periodically creates check-ins to record what changed, what was completed, what caused friction, and what should happen next.

There are also notes for keeping relevant context close to the work.

A simplified mental model looks like this:

Goal
│
├── Milestone
│   ├── Task
│   ├── Task
│   └── Task
│
├── Milestone
│   ├── Task
│   └── Task
│
├── Notes
│
└── Check-ins
Enter fullscreen mode Exit fullscreen mode

The important part is not the hierarchy itself. The interesting part is maintaining the relationship between planning and execution.

Tasks should not become an isolated pile of checkboxes.

When I complete something, I should still be able to understand:

  • what milestone it moved forward,
  • which larger goal that milestone belongs to,
  • what remains blocked,
  • what changed since my previous review,
  • and what the next concrete action should be.

That principle has influenced most of Rungset's product and technical decisions.

The current stack

Rungset is a TypeScript application using the Next.js App Router.

The current core stack is:

Frontend
├── Next.js 16
├── React 19
├── TypeScript
└── Tailwind CSS 4

Authentication
└── Better Auth

Data
├── Drizzle ORM
└── Cloudflare D1 / SQLite

Infrastructure
├── Cloudflare Workers
├── OpenNext
└── Wrangler

Validation / Security
├── Zod
├── DOMPurify
├── validator
└── xss

Testing
└── Playwright + application/integration tests
Enter fullscreen mode Exit fullscreen mode

The application is deployed to Cloudflare Workers through OpenNext rather than running as a traditional Node.js server.

That architectural choice affects several parts of the application, especially database access, authentication, deployment, and how server-side code needs to behave in a Workers environment.

Why Next.js?

Rungset is primarily an application, not a content website, but Next.js gives the project a useful combination of server and client primitives without requiring separate frontend and API repositories.

The App Router provides the routing and rendering layer while /api/* routes handle authenticated synchronization and server-side operations.

Keeping those concerns in a single TypeScript codebase also lowers the contribution barrier.

A developer cloning Rungset does not need to start:

frontend/
backend/
gateway/
worker/
database/
Enter fullscreen mode Exit fullscreen mode

just to understand how a task gets created.

That matters for an open-source project.

Architecture should serve the complexity of the product rather than trying to demonstrate how many infrastructure components we can deploy.

Data with Drizzle and Cloudflare D1

The persistent database is Cloudflare D1, which is based on SQLite.

Drizzle ORM sits between application code and D1.

Database definitions live in the repository, and schema changes are handled through forward-only migrations.

The rough flow is:

Next.js application
        ↓
Authenticated API route
        ↓
Drizzle ORM
        ↓
Cloudflare D1
Enter fullscreen mode Exit fullscreen mode

For local development, migrations can be applied against a local D1 database:

pnpm db:migrate:local
Enter fullscreen mode Exit fullscreen mode

Production migrations are explicit:

pnpm db:migrate:prod
Enter fullscreen mode Exit fullscreen mode

I prefer keeping migration execution separate from application startup.

Automatically applying production schema changes while starting an application can look convenient, but it couples deployment and irreversible database operations too tightly.

For a relatively small application, explicit migrations remain easy to reason about.

Authentication with Better Auth

Authentication is handled by Better Auth.

Rungset currently supports:

  • email/password authentication,
  • Google authentication,
  • GitHub authentication.

Authentication alone is obviously not sufficient for data security.

Every piece of workspace data needs to remain scoped to the authenticated user.

That means authorization needs to exist at the data-access boundary rather than relying on the UI to hide records belonging to another account.

The browser should always be treated as an untrusted client.

A request essentially needs to follow this model:

request
   ↓
authenticated session?
   ↓
resolve user
   ↓
validate payload
   ↓
execute user-scoped query
   ↓
return response
Enter fullscreen mode Exit fullscreen mode

That sounds obvious, but multi-user applications often become vulnerable when developers correctly implement authentication and then assume authentication automatically implies authorization.

It does not.

Validation belongs at the boundary

Rungset uses Zod and additional sanitization libraries for input validation and content handling.

Client-side validation exists for usability, but it cannot be the security boundary.

Anything arriving at the server should be treated as attacker-controlled input.

The basic pattern is:

const input = schema.parse(payload);
Enter fullscreen mode Exit fullscreen mode

before allowing that input to reach persistence or sensitive application logic.

Markdown and other user-controlled text require additional consideration because stored content can eventually become rendered content.

The general rule is simple:

Validate the shape of data when it enters the system and sanitize content when its rendering context requires it.

The frontend is not an authority.

Building an offline-capable workspace

One part of Rungset I find particularly useful technically is its offline behavior.

The application maintains client-side state so users can continue reviewing and capturing work when connectivity is unreliable, with changes synchronized through authenticated API routes.

This introduces a much more interesting problem than simply saving a todo.

Once local and remote state can exist independently, several questions appear:

Which version is newer?

What happens if synchronization fails?

Should the UI optimistically update?

What happens after authentication expires?

What if the network disappears during a write?

How much data should be cached?

How do we prevent one user's cached data from leaking into another session?
Enter fullscreen mode Exit fullscreen mode

Offline support is one of those features that looks like a boolean on a product roadmap:

Offline support: ✅
Enter fullscreen mode Exit fullscreen mode

but is actually a consistency problem.

Rungset's current implementation is intentionally smaller than a full collaborative offline-first architecture. There is no CRDT-based multiplayer synchronization or similar complexity because the product does not currently require it.

The objective is continuity for a single-user workspace, not Google Docs.

That distinction matters.

Deploying Next.js to Cloudflare Workers

Rungset is deployed using OpenNext for Cloudflare.

The relevant deployment flow is roughly:

Next.js
   ↓
OpenNext build
   ↓
Cloudflare-compatible output
   ↓
Wrangler
   ↓
Cloudflare Workers
Enter fullscreen mode Exit fullscreen mode

For a local Cloudflare preview:

pnpm cf:preview
Enter fullscreen mode Exit fullscreen mode

For deployment:

pnpm cf:deploy
Enter fullscreen mode Exit fullscreen mode

One lesson I try to maintain in this project is that:

A successful build is evidence that the source compiles. It is not evidence that production works.

Those are different states.

A production deployment adds environment variables, authentication configuration, D1 bindings, migrations, network behavior, Cloudflare runtime differences, and external providers.

So Rungset's quality checks do not stop at:

pnpm build
Enter fullscreen mode Exit fullscreen mode

Quality checks

Before considering a change complete, the project exposes separate checks for linting, type safety, application tests, end-to-end behavior, builds, and Cloudflare previews.

pnpm lint
pnpm typecheck
pnpm test
pnpm test:e2e
pnpm build
pnpm cf:preview
Enter fullscreen mode Exit fullscreen mode

End-to-end tests use Playwright.

I prefer keeping these checks explicit instead of hiding everything behind one giant verify command during development.

If a change breaks type generation, I want to know it is a type-generation problem.

If Playwright fails, I want to know the application behavior failed.

If the Cloudflare preview fails but next build succeeds, that difference is useful information.

What Rungset deliberately does not have

One of the easiest ways to destroy a small product is to keep adding features because each individual feature seems reasonable.

Rungset currently does not claim to provide:

  • AI recommendations,
  • AI-generated goals,
  • Google Calendar synchronization,
  • Outlook or Apple Calendar synchronization,
  • team collaboration,
  • a full analytics system,
  • an iOS app,
  • external scheduled notification delivery.

Some of these may eventually make sense.

But adding AI because every application is expected to have an AI button would not automatically make Rungset better.

The current problem is much simpler:

Can the application make the relationship between a long-term goal and today's work clearer?

Until that works extremely well, additional layers are secondary.

The Android application

Rungset is also available on Android.

The Android application currently uses the same hosted Rungset workspace rather than maintaining an independent mobile backend and data model.

That is another deliberate scope decision.

Maintaining:

Web frontend
Web backend
Android frontend
Android local database
Mobile synchronization engine
Separate mobile API behavior
Enter fullscreen mode Exit fullscreen mode

would dramatically increase the maintenance surface of an early-stage open-source product.

A shared hosted workspace lets Rungset validate the mobile use case without prematurely duplicating the architecture.

Why AGPLv3?

Rungset is released under the GNU Affero General Public License v3.0.

For a hosted web application, AGPL is an interesting license because its network-use provisions are stronger than licenses such as MIT.

The basic intention is straightforward:

You can inspect the implementation, run it yourself, modify it, and self-host it while preserving the project's open-source requirements.

For Rungset, open source is not simply a public mirror of otherwise proprietary code.

Self-hosting and source inspection are part of the product model.

Making Rungset contributor-friendly

Hacktoberfest is also a useful reason to improve something open-source maintainers sometimes neglect: the contribution experience.

Getting someone to open a pull request is only half the problem.

A contributor first needs to answer:

Can I run this project?

Where is the relevant code?

What behavior is expected?

What checks must pass?

Is this issue actually available?

How large should my change be?

Will the maintainer review it?
Enter fullscreen mode Exit fullscreen mode

Rungset has a CONTRIBUTING.md, documented development setup, explicit quality checks, and GitHub issues for work that can be discussed before implementation.

The local setup is roughly:

pnpm install

cp .env.example .dev.vars
cp .env.example .env.local

pnpm db:migrate:local
pnpm dev
Enter fullscreen mode Exit fullscreen mode

The development environment currently expects Node.js 24 and pnpm 11.

After startup, the application is available locally through Next.js, while pnpm cf:preview can be used when the actual Cloudflare runtime path matters.

Want to contribute?

This is one reason I'm sharing Rungset during Hacktoberfest.

I'm interested in contributions from developers who want to work on a real application rather than artificially created "Hacktoberfest issues."

Useful contributions can include:

  • bug fixes,
  • accessibility improvements,
  • test coverage,
  • UI and UX improvements,
  • documentation,
  • performance work,
  • offline synchronization improvements,
  • security hardening,
  • developer experience,
  • refactoring where there is a measurable reason for it.

I would rather merge one focused pull request that clearly improves the project than ten PRs changing formatting for the sake of creating contributions.

If you are new to the repository, opening an issue or commenting on an existing one before implementing a large feature is usually the best starting point.

GitHub: https://github.com/Ismailco/Rungset

Website: https://rungset.com

Hosted app: https://app.rungset.com

What comes next

Rungset is still in beta.

There are architectural decisions that will probably change as the application gets used more heavily, and some current abstractions will almost certainly prove unnecessary or insufficient.

That is normal for any evolving software system.

The objective is not to pretend the architecture is finished.

The useful part is being able to explain why the system looks the way it does today, measure where it fails, and change it when the evidence supports a different approach.

For now, the product remains centered on one relatively simple workflow:

Set a goal.
Break it into milestones.
Turn milestones into work.
Do the work.
Check in.
Adjust.
Repeat.
Enter fullscreen mode Exit fullscreen mode

Rungset is open source, so if any part of the architecture interests you, take a look at the repository.

Issues, pull requests, technical criticism, and contributions are welcome.

Top comments (0)