Work/MarathoMN

Case 01 / 04 · 2026

MarathoMN public calendar — the 2026 season, terrain-coded.Fig 01Public calendar — bilingual, terrain-coded · live
MarathoMN signed-in app — the calendar inside the member shell.Fig 02Member shell — the accounts layer over the calendar
MarathoMN council review queue — pending race proposals awaiting approval.Fig 03Review queue — the approval gate before the calendar

01Public calendar · the whole 2026 season at a glance.

02Signed-in app · calendar, communities, notifications.

03Council review · proposals awaiting publish.

Role

Strategy, Product, Engineering

Stack

Next.js · TypeScript · Notion · Resend · Vercel Blob · Vercel

Mongolia's marathon season on one bilingual calendar — running clubs propose their own races into a council-reviewed pipeline that publishes to a shared calendar and emails every runner.

Challenge

A runner in Ulaanbaatar planning a 2026 season has no single place to look. The races exist — thirty-odd of them, road and trail and steppe ultra, spread from June to November — but the schedule lives in a dozen club Facebook pages, a few poster JPGs, and word of mouth. The closest thing to a portal, marathon.mn, reads like a government notice board. Nobody owns the calendar, so nobody can trust it.

The deeper problem is who keeps it current. A schedule maintained by one editor goes stale the week they get busy. A schedule anyone can edit becomes spam. MarathoMN had to be curated and open at the same time — authoritative enough that a runner plans around it, yet fed by the clubs who actually run the races.

Approach

We started where the trust is: the calendar. A single bilingual season view — Mongolian first, English a toggle away — that shows the whole year at a glance. Desktop gets a wall-calendar grid; a phone gets an agenda list. The real data shapes the edges: June 13 collides three races into one day, the Mongol Gobi ultra spans a week as a single bar, October is empty and says so. Each race is colour-coded by terrain and links straight out to the club's own registration.

Then we built the part that keeps it alive. Runners sign in by email — a six-digit code, no password — and the calendar grows a second layer: communities. Any runner can start a club page or join one; inside, members post, and the page's creator can see their roster and promote moderators. Crucially, a club admin can't touch the public calendar directly. They propose a race, and it enters a queue.

The spine

The whole product turns on one path. A member posts in their club. A club admin turns an idea into a proposal — name, distances, terrain, a cover image. That proposal lands in the council's review queue, never on the public calendar. The council approves it, and three things happen at once: the race goes live on the shared calendar, every opted-in runner gets an email, and an in-app notification fires. One approval, one source of truth, everyone informed. That sequence — propose, review, publish, notify — is the case for the whole thing existing.

Outcome

  • The 2026 season — road, trail, mountain, steppe, track, and a multi-day Gobi ultra — readable in one glance, in two languages.
  • A three-role model — council, club admin, member — with a real approval gate between a proposal and the public calendar.
  • Passwordless sign-in, club communities with post feeds and rosters, and a council review queue, all live.
  • Email on publish to opted-in runners, sent from our own subscriber list rather than a rented newsletter — so the email looks exactly like the site.
  • Live at marathomn.run, on its own domain, as the studio's proof that a community platform can ship on a lean backend.

Stack reasoning

Notion is the database — on purpose. The council edits races in a tool they already understand, and every other table (users, communities, memberships, posts, proposals, notifications) sits behind a thin lib/store seam so a table can move to Postgres later without a single screen changing. Auth is passwordless and stateless: a six-digit code over Resend, then an HMAC-signed httpOnly cookie is the session — no store to keep, no code table to expire. Images go to Vercel Blob, because Notion's own file URLs are roughly one-hour signed links and useless as a persistent src. The whole thing is Next.js App Router on Vercel, bilingual from the first line.

What we would do differently

Notion is a fast, honest start and a deliberate ceiling — it rate-limits writes and isn't built for a busy social feed. The seam is already there; the move is to migrate the social tables (posts, memberships, notifications) to Postgres while leaving the council's race-editing in Notion, where the human workflow actually belongs. The second thing is reach: the email list is ours, which is the right call, but the next milestone is a digest — a weekly "what's coming up" rather than one mail per race.

A MarathoMN community page with a post feed and member list
01 · CommunitiesEvery running club gets a page — a post feed, a member roster, and admins who can promote their own moderators.
The propose-an-event form a community admin fills in
02 · The pipelineA club admin proposes a race; it lands in the council's review queue, not on the public calendar.
The new-race email and notification preferences screen
03 · NotificationsApprove once and every opted-in runner gets the new race by email — the same design as the site, sent from our own list.

○ Start something

Could your operation use one of these?