Open-source web app
Rungset
A web-first workspace for long-term goals, milestones, tasks, notes, and weekly check-ins. The same hosted workspace is open source under AGPLv3 and available on Google Play.
- Role
- Full-Stack Developer
- Project type
- Open-source web app
- Next.js
- TypeScript
- Cloudflare D1
- Drizzle ORM
- Better Auth

Context
Rungset is an open-source goal-planning product. The workspace itself is a web application: the hosted beta at app.rungset.com is where someone signs in, plans work, and reviews progress. The Android app on Google Play does not keep a second copy of that data. It opens the same hosted workspace.
The public site at rungset.com is separate from the app. It explains the product, documents what the current beta includes, and links to the hosted app, the source, and Google Play. The application source is published under the GNU Affero General Public License v3.0, so the implementation can be read or self-hosted.
I built Rungset as a focused beta, not as a demo of a stack. The app repository is versioned 0.1.0-beta.1. The boundary of that beta is part of the product: the workspace that ships is the workspace the site and the docs describe.
Problem
Long-term work falls apart in a familiar way. The outcome is written down in one place, the checkpoints live in another, the tasks that would actually move the work forward are somewhere else, and the weekly review never makes it back to the plan. The next action becomes detached from the reason the work exists.
A notes app can hold the thinking. A task list can hold the actions. Neither keeps the hierarchy visible at the same time: the goal that sets direction, the milestones that mark progress, the tasks that make the work concrete, and a check-in that records what changed. Without that loop, planning and doing drift apart, and the review has nothing reliable to return to.
The product also had to be an account, not a browser tab. Goals, milestones, tasks, notes, and check-ins needed to persist for the signed-in person, stay out of other people's workspaces, and remain available from the hosted app whether it was opened in a browser or from Android.
Product model
The working rhythm is goal, milestone, task, completion, check-in, review, and adjustment. Each record has a job, and the relationships between them are part of the product rather than a loose tagging convention.
A goal holds the outcome: a title, category, status, progress, timeframe, and an optional due date. Milestones belong to one goal and mark the checkpoints along the way. Tasks are the actionable work. A task has a title and a priority, and it can carry a due date, a category, recurrence, reminder settings, and a completion history. If a task points at a goal or a milestone, both have to belong to the signed-in user, and the milestone has to belong to that goal.
Notes are Markdown. They can be categorized and pinned so context stays next to the work it supports. A check-in records a date, mood, energy, accomplishments, challenges, the goals it relates to, and optional notes. The dashboard summarizes goal and category progress. Settings cover a JSON export of the workspace and a reset of workspace data.
That is the loop the beta is willing to support. An analytics dashboard, calendar sync with Google, Outlook, or Apple, an iOS app, AI recommendations, note attachments, sharing, collaboration, and external reminder delivery are not shipping features. Reminder settings in the app currently guide due and overdue work inside the product. They are not a scheduled notification service.
Approach
I kept one product surface. The Next.js application is the interface, the API, and the place where authorization happens. The marketing site can change its explanation of the product without being tied to the authenticated workspace. Android does not reimplement goals, tasks, offline caching, or sync.
The Android project is intentionally small. It packages the hosted progressive web app as a Trusted Web Activity. It provides the launcher, splash and icons, deep-link handling, browser fallback, and the Play Store build. Application id com.rungset.app launches https://app.rungset.com/. Until the site's Digital Asset Links statement matches the installed signing certificate, the app falls back to a Custom Tab instead of pretending the verification succeeded.
The beta stays narrow on purpose. A weekly planning loop that a person can actually use was more important than presenting calendar sync, analytics, or an iOS client as if they were already part of the product. The docs list both the current features and the capabilities that are not available yet, and the case study uses that same boundary.
Architecture
The app is a Next.js App Router application in TypeScript, deployed to Cloudflare Workers through OpenNext. Drizzle ORM maps the workspace onto Cloudflare D1. Better Auth owns the session, email and password sign-in, and Google or GitHub sign-in when those providers are configured. Authentication routes live under /api/auth. Workspace routes live under /api.
Those workspace routes are the application's JSON boundary, not a versioned public API. Every workspace read and write requires a session. The server derives the user from that session. A user id supplied by the client is ignored for ownership, so a request cannot ask the API to read or change someone else's goals, milestones, tasks, notes, or check-ins. Related writes are checked the same way: a milestone must belong to the current user and to the goal it is attached to.
Dates that are date-only use YYYY-MM-DD. Write bodies are limited to 2 MB and validated at the route. Errors return a short JSON message, with details when validation fails. The statuses in use are 400 for an invalid request, 401 when no one is signed in, 404 when a record is missing or not accessible, 413 when the payload is too large, and 500 for an unexpected server failure.
The client keeps an offline cache and synchronizes changes through those authenticated routes. D1 remains the authority for the hosted workspace, which is also what the Android app uses. Export is one download of the signed-in user's workspace: profile, goals, milestones, tasks, task occurrences, notes, and check-ins. Schema changes are forward-only migrations, applied to D1 with Wrangler.
The public site is a separate Next.js application. It describes the current product and points to the app, the documentation, the source, and Google Play. It does not share the app's session or database.
Decisions
The interesting choices were about keeping one workspace honest: in the database, at the API, offline, on Android, and in what the beta refuses to claim.
A relational model for the hierarchy
D1 stores users, sessions, accounts, verification records, goals, milestones, tasks, task occurrences, notes, and check-ins. Milestones reference a goal. Task occurrences store completion history for recurring tasks. The interface hierarchy and the database hierarchy are the same structure, so a milestone cannot float free of the goal it is supposed to mark.
The cost is ordinary and worth paying. Every new relationship needs a schema change and a forward-only migration. A single document per user would have been faster to sketch and harder to constrain. Ownership checks and goal-to-milestone checks are simpler when the rows and foreign relationships exist.
Ownership comes from the session
Better Auth establishes the session. Route handlers scope queries to the user on that session. The client is not asked to name the account it wants to act on, and a user id in the body is not trusted. Validation sits at the route boundary, before a write reaches the database.
That adds a check to every workspace request. It also keeps authorization in one place. The interface can be offline, refreshed, or opened inside the Android wrapper without becoming a second source of identity.
Offline cache, server authority
The interface can be reviewed and updated from a local cache, then synchronized through the authenticated API. The hosted database stays authoritative, including for the Android app, which does not keep its own store of goals and tasks.
A purely local app would have avoided sync. It also would not have been the same product in the browser and on Google Play. The tradeoff is reconciliation: local edits have to meet the signed-in account, and the API has to reject relationships that do not belong to that account.
Android as a wrapper, not a second client
The Play app is a Trusted Web Activity around the hosted progressive web app. Product behavior stays in the web application: authentication, routes, workspace data, offline caching, and business rules. The Android project supplies the release identity, launcher, splash, icons, deep links, and a browser fallback.
Fullscreen display without browser chrome depends on a Digital Asset Links file at the hosted origin matching the installed certificate. After Play App Signing, that file also needs the Play signing certificate, not only the upload key. Until the statement matches, the app opens in a Custom Tab. That fallback is deliberate. A broken verification should degrade to a browser, not to a silent failure or a forked dataset.
One Worker, with a narrow gate in front
OpenNext deploys the Next.js app, the API, and D1 access as one Cloudflare Worker. A public Worker receives scanner traffic that should never reach application code. Session checks remain the real authorization. In front of them, a hostname-, path-, and method-scoped WAF rule rejects clearly invalid requests and rate-limits authentication starts.
The allowlist has to move when routes move. A new legitimate path that is not added to the rule will be blocked. That operational cost is preferable to letting unauthenticated noise consume Worker time before the session check runs.
AGPL, export, and self-hosting
The source is AGPLv3. Someone can read how goals, sessions, and exports work, or run the application themselves. JSON export gives a hosted user a copy of their workspace without operating the database. Migrations and a documented local setup make self-hosting a real path rather than a license statement with no way to start.
AGPL is a strong share-alike license for software offered over a network. That was the point. An inspectable product was a better fit than a license that would allow a hosted fork to stay closed. The hosted beta can still be used without reading the source. Export is there for the person who wants their data either way.
Scope
The beta includes goals, milestones, tasks, Markdown notes, check-ins, a dashboard of goal and category progress, email and social sign-in, JSON export, workspace reset, an offline cache, self-hosting, and the Android wrapper around the hosted app.
It does not include an analytics product, external calendar sync, an iOS app, AI recommendations, collaborative editing, note attachments, or a service that delivers reminders outside the app. Those items stay out of the interface, the docs, and this case study until they exist. The marketing site says the same thing, so the public description and the software do not diverge.
Result
Rungset is a web-first product with a hosted beta, a public site, documentation of the current behavior, and a Google Play app that uses the hosted workspace. The source is available under AGPLv3, with forward-only D1 migrations, a JSON export, and a local setup path.
The repository is checked with lint, typechecking, application tests, migration checks, and Playwright end-to-end tests. A local build is evidence about the source. It is not, by itself, proof that a live deployment is healthy. Production still depends on the Worker, the D1 migrations, and the configured secrets.
What I can point to is the product boundary I wanted: one workspace for goals, milestones, tasks, notes, and weekly check-ins, available in the browser and on Android, with the data model, the API, and the public description all referring to the same thing.
Tech Stack
- Next.js
- React
- TypeScript
- Tailwind CSS
- Better Auth
- Drizzle ORM
- Cloudflare D1
- Cloudflare Workers
- OpenNext
- Playwright