<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonas</title>
    <description>The latest articles on DEV Community by Jonas (@jofflin).</description>
    <link>https://dev.to/jofflin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4007371%2Fc9c86b18-c55a-4191-b02d-5036cff53395.png</url>
      <title>DEV Community: Jonas</title>
      <link>https://dev.to/jofflin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jofflin"/>
    <language>en</language>
    <item>
      <title>Shipping 4 apps solo from one Turborepo</title>
      <dc:creator>Jonas</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:07:48 +0000</pubDate>
      <link>https://dev.to/jofflin/shipping-4-apps-solo-from-one-turborepo-340e</link>
      <guid>https://dev.to/jofflin/shipping-4-apps-solo-from-one-turborepo-340e</guid>
      <description>&lt;p&gt;&lt;a href="https://sportsflow.de" rel="noopener noreferrer"&gt;SportsFlow&lt;/a&gt; is four apps: a web app, a native (Expo) app, a marketing site, and a docs site. I'm one person. The only way that's sustainable is to make the four apps share as much as possible &lt;em&gt;without&lt;/em&gt; coupling them into a tangle.&lt;/p&gt;

&lt;p&gt;This is the final post in my build-in-public arc. It's the meta one: the conventions that let one brain hold the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
  web/        the product            (TanStack Start)
  marketing/  the public site        (TanStack Start, no auth/db/trpc)
  fumadocs/   the docs
  native/     mobile                 (Expo)
packages/
  ui/         design system + app-agnostic court geometry
  api/        tRPC router
  auth/       better-auth + org + stripe plugins
  db/         Drizzle schema + relations
  analytics/  pure-compute stats (runs everywhere)
  env/        t3-env validated config
  config/     shared tsconfig
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turborepo + pnpm. The pnpm &lt;strong&gt;catalog&lt;/strong&gt; pins shared dependency versions in one place, so all four apps move in lockstep instead of slowly drifting into four different React versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conventions are the real architecture
&lt;/h2&gt;

&lt;p&gt;When you're solo, you can't rely on review to enforce consistency — there's no second person. So the rules have to be &lt;em&gt;structural&lt;/em&gt;, the kind a type-checker or a linter catches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zod-first types.&lt;/strong&gt; Every type is a zod schema + &lt;code&gt;z.infer&lt;/code&gt;. No standalone interfaces anywhere. This isn't a style preference; it means runtime validation and compile-time types are the same artifact and can't drift. Value vocabularies live as code constants + zod, not Postgres enums — extending a list is a deploy, not a migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tRPC procedure ladder.&lt;/strong&gt; Authorisation is a staircase, and you physically cannot skip a step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;publicProcedure&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;protectedProcedure   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;orgProcedure       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="nx"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;injects&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;staffProcedure   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;staff&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="nx"&gt;only&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every domain query filters on &lt;code&gt;orgId&lt;/code&gt;, and &lt;code&gt;orgProcedure&lt;/code&gt; is the thing that &lt;em&gt;provides&lt;/em&gt; &lt;code&gt;orgId&lt;/code&gt; to the context. You can't write a query that forgets tenancy scoping, because the scoping comes from the procedure you built on. Multi-tenant safety becomes a property of where you start, not a checklist you remember.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One design system, two consumers.&lt;/strong&gt; &lt;code&gt;packages/ui&lt;/code&gt; holds brand-locked, app-agnostic atoms — &lt;code&gt;Button&lt;/code&gt;, &lt;code&gt;ScorePill&lt;/code&gt;, &lt;code&gt;PlayerChip&lt;/code&gt; — plus the &lt;em&gt;court geometry&lt;/em&gt;: the SVG math for a handball court, goal zones, and play playback. Both the web tracking UI and the marketing site's live demos render the same court from the same geometry. The marketing animations aren't a reimplementation; they're the real component with demo data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Billing stays out of the domain.&lt;/strong&gt; Subscriptions are per team (&lt;code&gt;referenceId = orgId&lt;/code&gt;) via better-auth's Stripe plugin. But domain tables know &lt;em&gt;nothing&lt;/em&gt; about billing — tier limits live in one file (&lt;code&gt;limits.ts&lt;/code&gt;, e.g. &lt;code&gt;assertCanCreateMatch&lt;/code&gt;) called from the API. The schema never grows a &lt;code&gt;plan&lt;/code&gt; column. If pricing changes, exactly one file changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the monorepo buys me concretely
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change a type once.&lt;/strong&gt; A field added to a zod schema in &lt;code&gt;db&lt;/code&gt; ripples through the tRPC router, the web app, and the native app as type errors — a to-do list the compiler writes for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same analytics run everywhere&lt;/strong&gt; (see post #3) precisely &lt;em&gt;because&lt;/em&gt; it's a shared package, not copy-paste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing has no auth/db/trpc at all.&lt;/strong&gt; It's deliberately a different dependency graph — a fast static site that imports only the design system. The monorepo lets me share the &lt;em&gt;look&lt;/em&gt; without sharing the &lt;em&gt;weight&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Honesty section. A monorepo isn't free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native is early.&lt;/strong&gt; It's scaffolded and has a first cut of season analytics, but it lags the web app. Sharing packages helps, but React Native still needs its own UI primitives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The better-auth schema is generated&lt;/strong&gt;, which means a re-merge checklist every time (strip the old relations blocks, since relations live centrally for the new query builder). Generated code in a monorepo needs guardrails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One person is still one person.&lt;/strong&gt; The tooling multiplies what I can build; it doesn't multiply the hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The throughline
&lt;/h2&gt;

&lt;p&gt;Across this whole series — offline-first capture, cross-platform analytics, this monorepo — the same idea keeps showing up: &lt;strong&gt;make the correct thing structural.&lt;/strong&gt; Idempotency in the schema instead of the network. Tenancy in the procedure instead of the query. Types from one zod definition instead of two. Stats from one pure package instead of three implementations.&lt;/p&gt;

&lt;p&gt;When you're solo, discipline you have to &lt;em&gt;remember&lt;/em&gt; will eventually fail. Discipline the system &lt;em&gt;enforces&lt;/em&gt; is the only kind that scales to one person building four apps.&lt;/p&gt;

&lt;p&gt;Thanks for following the series. If you're building something in this space — live sports, offline-first, multi-tenant SaaS, solo — come say hi.&lt;/p&gt;

</description>
      <category>monorepo</category>
      <category>typescript</category>
      <category>turborepo</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Hi, I'm Jonas — building a sports SaaS solo, in the open</title>
      <dc:creator>Jonas</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:25:52 +0000</pubDate>
      <link>https://dev.to/jofflin/hi-im-jonas-building-a-sports-saas-solo-in-the-open-odd</link>
      <guid>https://dev.to/jofflin/hi-im-jonas-building-a-sports-saas-solo-in-the-open-odd</guid>
      <description>&lt;p&gt;Hi 👋 I'm Jonas.&lt;/p&gt;

&lt;p&gt;CS bachelor, Entrepreneurship master. By day I'm at &lt;strong&gt;nono&lt;/strong&gt;. On the side I'm building &lt;strong&gt;SportsFlow&lt;/strong&gt; solo — and I'm going to write about every hard part of it out in the open. This is the intro.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building
&lt;/h2&gt;

&lt;p&gt;SportsFlow replaces the thing every amateur handball coach still uses: a clipboard and a pen.&lt;/p&gt;

&lt;p&gt;The idea is simple. Live-track every shot, assist and save during the game on a phone or tablet, and get real season analytics out the other end — shooting percentages, heatmaps, goalkeeper saves, momentum, lineup impact. Handball first, then volleyball, basketball, ice hockey.&lt;/p&gt;

&lt;p&gt;I sat on enough benches to know the problem is real: the data is right there &lt;em&gt;in the game&lt;/em&gt;, and it evaporates the second the whistle blows. Nobody should need a spreadsheet and a good memory to coach with numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I write about both code and product
&lt;/h2&gt;

&lt;p&gt;I build at the seam between engineering and product — that's the CS + Entrepreneurship combo. So I won't only post architecture. I'll also post the decisions about &lt;em&gt;what's worth building at all&lt;/em&gt;: where I drew scope lines, what I deliberately didn't build, how billing shapes the data model.&lt;/p&gt;

&lt;p&gt;The recurring theme in everything here is one idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Make the correct thing structural.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Idempotency in the schema, not the network. Tenancy in the procedure, not the query. Types from one zod definition, not two. Discipline the system enforces, not discipline you have to remember — because when you're solo, the stuff you have to remember eventually fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll get if you follow along
&lt;/h2&gt;

&lt;p&gt;Biweekly build-in-public deep-dives, including the honest costs and not just the wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline-first capture&lt;/strong&gt; — sports halls have zero WiFi, so the whole tracking pipeline works offline and reconciles later without double-counting goals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One analytics codebase, three runtimes&lt;/strong&gt; — the same shooting-percentage code runs in the web app, the native app, and offline during live tracking, so the numbers can never disagree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping four apps solo from one monorepo&lt;/strong&gt; — web, native, marketing, and docs, and the conventions that make that sane for one person.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;If that's your filter: TypeScript (zod-first), TanStack Start, tRPC, Drizzle, better-auth, Turborepo, Expo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come say hi
&lt;/h2&gt;

&lt;p&gt;If you're building in live sports, offline-first systems, or multi-tenant SaaS — or you're a solo dev shipping more than one person reasonably should — follow along, and please come argue with me in the comments. The build-in-public part only works if it's a conversation.&lt;/p&gt;

&lt;p&gt;First deep-dive drops soon.&lt;/p&gt;

</description>
      <category>introduction</category>
      <category>buildinpublic</category>
      <category>typescript</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
