<?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: Paweł Mazur</title>
    <description>The latest articles on DEV Community by Paweł Mazur (@efowski).</description>
    <link>https://dev.to/efowski</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%2F4102914%2F34bf9699-b201-47a5-8542-a0ff8df48621.jpg</url>
      <title>DEV Community: Paweł Mazur</title>
      <link>https://dev.to/efowski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/efowski"/>
    <language>en</language>
    <item>
      <title>I'm building a SaaS for independent record labels. But do they actually need it?</title>
      <dc:creator>Paweł Mazur</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:26:41 +0000</pubDate>
      <link>https://dev.to/efowski/im-building-a-saas-for-independent-record-labels-but-do-they-actually-need-it-23h7</link>
      <guid>https://dev.to/efowski/im-building-a-saas-for-independent-record-labels-but-do-they-actually-need-it-23h7</guid>
      <description>&lt;p&gt;I've already started building the backend.&lt;/p&gt;

&lt;p&gt;Django, Django REST Framework, PostgreSQL. Multi-tenancy works. Labels can have members, releases, formats, orders and transactions.&lt;/p&gt;

&lt;p&gt;So naturally, this seems like a good time to ask a slightly inconvenient question:&lt;/p&gt;

&lt;p&gt;Does anyone actually need this?&lt;/p&gt;

&lt;p&gt;The project is called &lt;strong&gt;Label Mechanics&lt;/strong&gt;, and it sits somewhere between my work as a developer and the amount of time I've spent around independent music and small record labels.&lt;/p&gt;

&lt;p&gt;Like many software ideas, it started with a problem that looked much simpler at first than it does now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a small label creates a lot of data
&lt;/h2&gt;

&lt;p&gt;Let's say you're putting out a record.&lt;/p&gt;

&lt;p&gt;You pay for manufacturing. Maybe mastering. Artwork. Promo copies. PR. Some ads.&lt;/p&gt;

&lt;p&gt;You also spend time posting on social media, sending emails, contacting press, updating Bandcamp and doing all the other small things that come with releasing music.&lt;/p&gt;

&lt;p&gt;Then the record comes out.&lt;/p&gt;

&lt;p&gt;Now you have sales on Bandcamp or your store. Streaming numbers somewhere else. Social media metrics. Maybe results from an ad campaign. Costs sitting in a spreadsheet.&lt;/p&gt;

&lt;p&gt;And after all of that, some surprisingly basic questions can be difficult to answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much did this release actually cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the promotion make any difference?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was spending money on ads worth it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was PR worth it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should I do differently with the next release?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Of course, labels do answer these questions somehow.&lt;/p&gt;

&lt;p&gt;Spreadsheets exist for a reason.&lt;/p&gt;

&lt;p&gt;So does experience.&lt;/p&gt;

&lt;p&gt;Someone who has released 50 records probably has a pretty good feeling for what works for their label.&lt;/p&gt;

&lt;p&gt;But "I have a feeling this worked" and "I can actually see why this worked" aren't quite the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first assumption was probably wrong
&lt;/h2&gt;

&lt;p&gt;My initial idea was fairly straightforward.&lt;/p&gt;

&lt;p&gt;Build software that connects the operational side of a label with release economics and marketing.&lt;/p&gt;

&lt;p&gt;Catalog. Releases. Costs. Sales. Promotion. Results.&lt;/p&gt;

&lt;p&gt;Put the data in one place and the problem is solved.&lt;/p&gt;

&lt;p&gt;Except there is an obvious issue with that idea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software like this already exists.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once I started looking properly at the market, I found products covering label management, royalty accounting, distribution, marketing analytics and various combinations of all of them.&lt;/p&gt;

&lt;p&gt;So "labels need a place where all their data lives" isn't a particularly interesting hypothesis.&lt;/p&gt;

&lt;p&gt;It also made me rethink what I was actually trying to solve.&lt;/p&gt;

&lt;p&gt;Maybe a small label doesn't need another dashboard.&lt;/p&gt;

&lt;p&gt;Maybe it needs a better answer to a much simpler question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is actually worth my time and money?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That changes the product quite a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small labels don't necessarily need more marketing
&lt;/h2&gt;

&lt;p&gt;This is another assumption I'm interested in testing.&lt;/p&gt;

&lt;p&gt;A lot of marketing software is naturally designed around doing more marketing.&lt;/p&gt;

&lt;p&gt;More campaigns. More data. More optimization.&lt;/p&gt;

&lt;p&gt;But imagine a label run by one or two people with a limited budget and limited time.&lt;/p&gt;

&lt;p&gt;Maybe the correct recommendation for a particular release isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Spend another €200 on ads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe it's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe a Bandcamp announcement, an email to the existing audience, a few posts and some direct outreach would produce almost the same result.&lt;/p&gt;

&lt;p&gt;If so, that's useful information too.&lt;/p&gt;

&lt;p&gt;Time matters here as well.&lt;/p&gt;

&lt;p&gt;If one release required 15 hours of promotion and another required five hours for a similar result, the difference isn't visible if you're only looking at ad spend.&lt;/p&gt;

&lt;p&gt;That's the direction I'm currently exploring with Label Mechanics.&lt;/p&gt;

&lt;p&gt;Not necessarily how to make labels do more marketing, but how to help them understand &lt;strong&gt;how much marketing is enough&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Label Mechanics is becoming
&lt;/h2&gt;

&lt;p&gt;The current idea can be reduced to something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RELEASE
   ↓
ECONOMICS
   ↓
MARKETING
   ↓
RESPONSE
   ↓
INSIGHTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every release, I eventually want to connect a few basic things.&lt;/p&gt;

&lt;p&gt;What did it cost?&lt;/p&gt;

&lt;p&gt;What did the label do to promote it?&lt;/p&gt;

&lt;p&gt;How much money and time went into that promotion?&lt;/p&gt;

&lt;p&gt;What happened afterwards?&lt;/p&gt;

&lt;p&gt;And, most importantly, what can be learned from it before the next release?&lt;/p&gt;

&lt;p&gt;That last part is where I think the project might become interesting.&lt;/p&gt;

&lt;p&gt;After several releases, the useful comparison isn't necessarily against some generic "music marketing best practices".&lt;/p&gt;

&lt;p&gt;It's against the label's own history.&lt;/p&gt;

&lt;p&gt;Maybe €300 of paid promotion makes sense for Label A.&lt;/p&gt;

&lt;p&gt;Maybe Label B repeatedly gets similar results without it.&lt;/p&gt;

&lt;p&gt;Maybe press coverage matters a lot for one type of release and almost nothing for another.&lt;/p&gt;

&lt;p&gt;I don't know yet.&lt;/p&gt;

&lt;p&gt;And that's rather the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is already a backend
&lt;/h2&gt;

&lt;p&gt;This isn't only an idea sitting in a notes file.&lt;/p&gt;

&lt;p&gt;I've already been building the backend in &lt;strong&gt;Django, Django REST Framework and PostgreSQL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The current domain includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;labels and memberships&lt;/li&gt;
&lt;li&gt;releases&lt;/li&gt;
&lt;li&gt;physical formats&lt;/li&gt;
&lt;li&gt;tracks&lt;/li&gt;
&lt;li&gt;contacts&lt;/li&gt;
&lt;li&gt;orders and order items&lt;/li&gt;
&lt;li&gt;transactions&lt;/li&gt;
&lt;li&gt;release-level economics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's multi-tenant, so users only have access to data belonging to their labels.&lt;/p&gt;

&lt;p&gt;At a very simplified level, part of the domain currently looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Label
 |
 +-- Releases
 |     |
 |     +-- Formats
 |     +-- Orders
 |     +-- Transactions
 |
 +-- Members
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A release can already be connected to its costs, income, formats and sales.&lt;/p&gt;

&lt;p&gt;The next development step is the marketing side: campaigns and the activities around them.&lt;/p&gt;

&lt;p&gt;One thing worth making clear here: &lt;strong&gt;I'm not building all of this alone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm using ChatGPT extensively throughout the project. Not just for generating bits of code, but as a working partner for discussing architecture, reviewing decisions, researching the market, challenging product assumptions and planning customer discovery.&lt;/p&gt;

&lt;p&gt;I still make the decisions, write and understand the code, test it and decide what actually goes into the project. But pretending there isn't an AI sitting next to me for a large part of the process would be a fairly strange way to document how I'm building this in 2026.&lt;/p&gt;

&lt;p&gt;It also makes this experiment more interesting to me. I'm not only trying to find out whether Label Mechanics should exist. I'm also learning what building a real product with AI as part of the workflow actually looks like - where it helps, where it gets things wrong, and where I still need to stop and think for myself.&lt;/p&gt;

&lt;p&gt;But I'm deliberately trying not to build too far ahead.&lt;/p&gt;

&lt;p&gt;For example, I could create a large &lt;code&gt;Performance&lt;/code&gt; model today with Spotify streams, Bandcamp sales, Instagram followers, wishlist numbers and twenty other metrics.&lt;/p&gt;

&lt;p&gt;That would be easy enough.&lt;/p&gt;

&lt;p&gt;It would also mean I'm deciding what labels need before asking them.&lt;/p&gt;

&lt;p&gt;I'd rather not do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  So now comes the less comfortable part
&lt;/h2&gt;

&lt;p&gt;Talking to people.&lt;/p&gt;

&lt;p&gt;I'm starting customer discovery with small independent labels to understand how they actually handle this today.&lt;/p&gt;

&lt;p&gt;Not how I imagine they handle it.&lt;/p&gt;

&lt;p&gt;I'm particularly interested in how labels answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you know whether a release was successful?&lt;/li&gt;
&lt;li&gt;How do you track its real cost?&lt;/li&gt;
&lt;li&gt;How do you decide how much promotion it needs?&lt;/li&gt;
&lt;li&gt;How do you know whether PR or paid promotion worked?&lt;/li&gt;
&lt;li&gt;Do you use data from previous releases when planning the next one?&lt;/li&gt;
&lt;li&gt;How much time do you spend collecting all this information?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are still plenty of assumptions behind Label Mechanics.&lt;/p&gt;

&lt;p&gt;I don't know whether this problem is painful enough for labels to change their workflow.&lt;/p&gt;

&lt;p&gt;I don't know which data actually matters to them.&lt;/p&gt;

&lt;p&gt;I don't know whether the smallest labels want software for this at all.&lt;/p&gt;

&lt;p&gt;And I definitely don't know whether anyone will pay for it.&lt;/p&gt;

&lt;p&gt;Those seem like fairly important things to find out before spending the next six months building features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm writing about it here
&lt;/h2&gt;

&lt;p&gt;I want to treat Label Mechanics as a real product experiment rather than another project where I build the application first and start wondering about users afterwards.&lt;/p&gt;

&lt;p&gt;So I'll write about the process here as it develops.&lt;/p&gt;

&lt;p&gt;There will be Django and backend work, because that's what I'm using to build it.&lt;/p&gt;

&lt;p&gt;But I'm more interested in the decisions around the code.&lt;/p&gt;

&lt;p&gt;Why does a particular model exist?&lt;/p&gt;

&lt;p&gt;Why didn't something make the MVP?&lt;/p&gt;

&lt;p&gt;What changed after talking to actual label owners?&lt;/p&gt;

&lt;p&gt;Which assumptions turned out to be wrong?&lt;/p&gt;

&lt;p&gt;How much does working with AI actually help when building something like this?&lt;/p&gt;

&lt;p&gt;And eventually: does anyone actually want the thing?&lt;/p&gt;

&lt;p&gt;If it works, great.&lt;/p&gt;

&lt;p&gt;If it doesn't, I'd at least like to know &lt;strong&gt;why it didn't&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For now, that's where Label Mechanics is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The backend exists.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hypothesis exists.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now it's time to see whether the problem does too.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>saas</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>RouteFlow — What Happened When I Finally Tested the Whole MVP</title>
      <dc:creator>Paweł Mazur</dc:creator>
      <pubDate>Wed, 09 Sep 2026 21:34:03 +0000</pubDate>
      <link>https://dev.to/efowski/routeflow-what-happened-when-i-finally-tested-the-whole-mvp-1bfj</link>
      <guid>https://dev.to/efowski/routeflow-what-happened-when-i-finally-tested-the-whole-mvp-1bfj</guid>
      <description>&lt;p&gt;In my previous RouteFlow post, I ended with a fairly simple goal: stop adding features and run the application from an empty account as if I were an actual climbing gym.&lt;/p&gt;

&lt;p&gt;So that's what I did.&lt;/p&gt;

&lt;p&gt;It didn't take very long to find the first problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing from an empty account
&lt;/h2&gt;

&lt;p&gt;Until now, most of RouteFlow had been tested while individual features were being built.&lt;/p&gt;

&lt;p&gt;This time I wanted to start from scratch and follow the actual process:&lt;/p&gt;

&lt;p&gt;Register Gym Manager → Create Gym → Add Setters → Plan Setting Session → Assign Tasks → Complete Work → Publish Route&lt;/p&gt;

&lt;p&gt;And I added one rule.&lt;/p&gt;

&lt;p&gt;If I had to open Django Admin or manually fix something in the database to make the normal workflow possible, then v0.1 wasn't ready yet.&lt;/p&gt;

&lt;p&gt;Simple enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first test passed
&lt;/h2&gt;

&lt;p&gt;I registered a completely new Gym Manager account.&lt;/p&gt;

&lt;p&gt;The application logged me in correctly. The gym name appeared in the interface. The user had the correct role. There was no data leaking in from another gym.&lt;/p&gt;

&lt;p&gt;Everything looked fine.&lt;/p&gt;

&lt;p&gt;So initially, I marked the first step as passed.It wasn't.&lt;/p&gt;

&lt;p&gt;When I checked the backend, I realised that registration had created the user, but it hadn't actually created the corresponding Gym.&lt;/p&gt;

&lt;p&gt;The UI knew the name of the gym.&lt;/p&gt;

&lt;p&gt;The database didn't know the gym existed.&lt;/p&gt;

&lt;p&gt;gym_name is not gym&lt;/p&gt;

&lt;p&gt;This came from a distinction I had deliberately made earlier in the project.&lt;/p&gt;

&lt;p&gt;The user model contains both a textual gym name and an actual relationship to the Gym model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gym = models.ForeignKey(
    Gym,
    null=True,
    blank=True,
    related_name='users',
    on_delete=models.SET_NULL
)

gym_name = models.CharField(
    max_length=150,
    default="VertiGym Warszawska"
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction itself wasn't the problem.&lt;/p&gt;

&lt;p&gt;The registration serializer was.&lt;/p&gt;

&lt;p&gt;It could store gym_name, and it could accept an existing gym:&lt;/p&gt;

&lt;p&gt;gym=validated_data.get('gym'),&lt;br&gt;
gym_name=validated_data.get('gym_name', 'VertiGym'),&lt;/p&gt;

&lt;p&gt;But it didn't create a Gym when a new manager registered one.&lt;/p&gt;

&lt;p&gt;So I effectively had this:&lt;/p&gt;

&lt;p&gt;User&lt;br&gt;
├── gym_name = "RouteFlow Test Gym"&lt;br&gt;
└── gym = NULL&lt;/p&gt;

&lt;p&gt;when what I actually needed was:&lt;/p&gt;

&lt;p&gt;Gym&lt;br&gt;
└── name = "RouteFlow Test Gym"&lt;br&gt;
     ↑&lt;br&gt;
     │&lt;br&gt;
User&lt;br&gt;
└── gym = &lt;/p&gt;

&lt;p&gt;This was surprisingly easy to miss because nothing immediately broke.&lt;/p&gt;

&lt;p&gt;Registration worked. Login worked. The frontend displayed the gym name. From the user's perspective, the account looked valid.&lt;/p&gt;

&lt;p&gt;Only when I tested the application as a complete system did the difference between displaying a string and having an actual domain object become important.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix was small
&lt;/h2&gt;

&lt;p&gt;The solution wasn't particularly complicated.&lt;/p&gt;

&lt;p&gt;During registration, if no existing gym is supplied, the serializer now creates one using the submitted gym name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gym_name = validated_data.get(
    'gym_name',
    'VertiGym Warszawska'
)

gym = validated_data.get('gym')

if not gym:
    gym = Gym.objects.create(name=gym_name)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The newly created Gym is then assigned to the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user = User.objects.create_user(
    email=validated_data['email'],
    username=validated_data.get('username')
        or validated_data['email'],
    first_name=validated_data.get('first_name', ''),
    last_name=validated_data.get('last_name', ''),
    role=validated_data.get('role', 'gym_manager'),
    gym=gym,
    gym_name=gym_name,
    password=validated_data['password'],
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I repeated the test.&lt;/p&gt;

&lt;p&gt;New user: created.&lt;/p&gt;

&lt;p&gt;New gym: created.&lt;/p&gt;

&lt;p&gt;User assigned to that gym: yes.&lt;/p&gt;

&lt;p&gt;Problem solved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjx12nub56lyvq65o0qy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjx12nub56lyvq65o0qy.png" alt="RouteFlow Setting Planner showing a planned climbing gym setting session" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the actual workflow worked
&lt;/h2&gt;

&lt;p&gt;After fixing registration, I continued through the complete workflow: creating setters, planning a Setting Session, assigning tasks, logging in as a Route Setter and eventually publishing completed work as a Route.&lt;/p&gt;

&lt;p&gt;That part worked.&lt;/p&gt;

&lt;p&gt;So did the less visible parts I was particularly interested in testing: role-based permissions, persistence after refresh and data isolation between gyms.&lt;/p&gt;

&lt;p&gt;A Route Setter could work with their assigned tasks without getting access to manager-level operations. A Head Setter could manage the setting workflow without getting access to gym-level user administration.&lt;/p&gt;

&lt;p&gt;And a user from Gym A couldn't suddenly access Gym B's routes, setters, sessions or tasks.&lt;/p&gt;

&lt;p&gt;Most importantly, the core relationship I've been building RouteFlow around finally worked as one continuous process:&lt;/p&gt;

&lt;p&gt;Sector → Setting Session → Setter Task → Route&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6idtkiuqr23y2ipcqdjv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6idtkiuqr23y2ipcqdjv.png" alt="RouteFlow Setter Tasks view showing route setting tasks and their workflow statuses" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I hit the second problem.I still had to open Django Admin.Sectors.&lt;/p&gt;

&lt;p&gt;The backend already understands them. Routes and Setting Sessions use them. Gym isolation applies to them.&lt;/p&gt;

&lt;p&gt;But there currently isn't a way for a new Gym Manager to create the initial sectors from the frontend.&lt;/p&gt;

&lt;p&gt;To continue the test, I had to create them manually through Django Admin.&lt;/p&gt;

&lt;p&gt;And that breaks the rule I set for myself before starting this test.&lt;/p&gt;

&lt;p&gt;In the previous post I wrote:&lt;/p&gt;

&lt;p&gt;If it requires developer intervention along the way, v0.1 isn't ready yet.&lt;/p&gt;

&lt;p&gt;Well.It did.So v0.1 isn't ready yet.Not quite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working features aren't necessarily a working product
&lt;/h2&gt;

&lt;p&gt;This was probably the most useful result of the whole test.Most of RouteFlow worked.The task workflow worked. Route publishing worked. Authentication worked. RBAC worked. Gym isolation worked. Setting Sessions worked.&lt;/p&gt;

&lt;p&gt;But a completely new customer still couldn't get from registration to actually using those features without me stepping in.&lt;/p&gt;

&lt;p&gt;That's a very different kind of failure from an API returning 500 or a React component crashing.&lt;/p&gt;

&lt;p&gt;And it's also much easier to miss when development consists of implementing one feature at a time.&lt;/p&gt;

&lt;p&gt;While building a feature, the database already contains the objects you need. You already have test users. Sectors exist because you created them weeks ago. Your account is already attached to a gym. The application slowly accumulates the environment it needs to work. A new user gets none of that.&lt;/p&gt;

&lt;p&gt;Starting again with an empty account removes all of those assumptions.&lt;/p&gt;

&lt;p&gt;In this case, it exposed two of them almost immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One was a bug: registration didn't create the actual Gym.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The other was a missing piece of the product: there was no frontend workflow for creating the first sectors.The first one is now fixed.&lt;br&gt;
The second one is next. So, is the MVP ready? Almost.&lt;/p&gt;

&lt;p&gt;And "almost" means something much more concrete now than it did before running this test.&lt;/p&gt;

&lt;p&gt;The core workflow survived. The three user roles behave as expected. Data isolation between gyms works. A completed setting task can travel through the system and become a real Route without manual database work.&lt;/p&gt;

&lt;p&gt;What didn't survive was the onboarding around that workflow.So I'm not adding another major feature yet.&lt;/p&gt;

&lt;p&gt;The next job is much less exciting: let a Gym Manager create and manage sectors without Django Admin. Then I'll delete the test data, create another completely fresh account and do the whole thing again.&lt;/p&gt;

&lt;p&gt;If I can get from registration to a published route without touching the backend manually, then I'll be much more comfortable calling RouteFlow v0.1 usable.&lt;/p&gt;

&lt;p&gt;After that, I want the next bugs to come from people who actually work in climbing gyms. I'm fairly sure they'll find some. That's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;RouteFlow is still under active development, and the source code is available on &lt;a href="https://github.com/Efowski/routeflow" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the second post about building RouteFlow. In the first one, I wrote about the architecture, gym-level data isolation, the setting workflow and why a completed Setter Task isn't automatically a Route.&lt;/p&gt;

&lt;p&gt;This time I stopped building long enough to actually use the thing.&lt;/p&gt;

&lt;p&gt;Turns out that was useful.&lt;/p&gt;

</description>
      <category>django</category>
      <category>react</category>
      <category>saas</category>
    </item>
    <item>
      <title>RouteFlow — Building a Route Setting SaaS with Django and React</title>
      <dc:creator>Paweł Mazur</dc:creator>
      <pubDate>Tue, 01 Sep 2026 09:59:04 +0000</pubDate>
      <link>https://dev.to/efowski/routeflow-building-a-route-setting-saas-with-django-and-react-40nc</link>
      <guid>https://dev.to/efowski/routeflow-building-a-route-setting-saas-with-django-and-react-40nc</guid>
      <description>&lt;p&gt;Route setting in a climbing gym seems fairly straightforward when you're looking at it from the outside. Old routes come down, new ones go up, somebody grades them and the wall opens again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ajpfrejwcigtckimwzz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ajpfrejwcigtckimwzz.png" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is quite a bit more going on behind the scenes.&lt;/p&gt;

&lt;p&gt;Routes get old. Sectors need regular resets. Setters have to know what they're supposed to build. Someone needs to decide how many new routes are needed and which grades are missing. Then all of that has to be planned around people, dates and the fact that the gym still needs to operate.&lt;/p&gt;

&lt;p&gt;You can manage a lot of this with spreadsheets.&lt;/p&gt;

&lt;p&gt;Up to a point.&lt;/p&gt;

&lt;p&gt;That's basically where RouteFlow started.&lt;/p&gt;

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

&lt;p&gt;RouteFlow is a SaaS application for managing route setting in climbing gyms.&lt;/p&gt;

&lt;p&gt;I'm deliberately focusing on the operational side of the gym rather than building another app for climbers to log their ascents.&lt;/p&gt;

&lt;p&gt;At the centre of RouteFlow is a fairly simple relationship:&lt;/p&gt;

&lt;p&gt;Sector → Setting Session → Setter Tasks → Routes&lt;/p&gt;

&lt;p&gt;A manager or head setter plans work for a sector, decides how many routes should be created and assigns individual tasks to setters.&lt;/p&gt;

&lt;p&gt;The setters work through those tasks, the routes get tested, and finished work can eventually become an actual Route in the gym's database.&lt;/p&gt;

&lt;p&gt;That last part turned out to be more interesting than I initially expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Django on the backend, React on the frontend
&lt;/h2&gt;

&lt;p&gt;The backend is built with Python, Django and Django REST Framework, with PostgreSQL as the database. The application runs in Docker.&lt;/p&gt;

&lt;p&gt;The frontend is React with TypeScript and talks to Django through the REST API.&lt;/p&gt;

&lt;p&gt;There isn't anything particularly exotic about that stack, and that's intentional.&lt;/p&gt;

&lt;p&gt;I'm building RouteFlow as something that should eventually be used by actual climbing gyms. I would rather have boring technology with understandable behaviour than introduce another piece of infrastructure just because I can.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gym has to actually own its data
&lt;/h2&gt;

&lt;p&gt;Once RouteFlow started moving beyond basic CRUD, one of the first things that needed sorting out properly was data ownership.&lt;/p&gt;

&lt;p&gt;The natural boundary is the gym.&lt;/p&gt;

&lt;p&gt;A sector belongs to a gym. Routes inside that sector belong to the same gym. Setting sessions and the tasks created within them need to respect that boundary as well.&lt;/p&gt;

&lt;p&gt;And simply hiding Gym B's data from somebody logged into Gym A wasn't enough.&lt;/p&gt;

&lt;p&gt;The API itself has to enforce it.&lt;/p&gt;

&lt;p&gt;So the Django querysets are scoped to the authenticated user's gym, and I've been testing the API by deliberately trying to retrieve and modify resources belonging to another gym.&lt;/p&gt;

&lt;h2&gt;
  
  
  Those requests return 404.
&lt;/h2&gt;

&lt;p&gt;The same rule applies when creating relationships between objects. For example, a setter belonging to one gym shouldn't be assignable to a setting session in another one.&lt;/p&gt;

&lt;p&gt;This is the kind of work that isn't particularly exciting in a screenshot, but it's much closer to the problems I expect a real SaaS application to have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting workflow
&lt;/h2&gt;

&lt;p&gt;A Setting Session describes a planned reset or setting operation for a sector.&lt;/p&gt;

&lt;p&gt;It contains things such as the date, lead setter and target number of routes. From there, individual Setter Tasks can be created and assigned.&lt;/p&gt;

&lt;p&gt;A task currently moves through:&lt;/p&gt;

&lt;p&gt;Todo → In Progress → Testing → Done&lt;/p&gt;

&lt;p&gt;It can also contain a target grade, hold colour, setter and due date.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmrdg6e42dtyqfgb3j6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmrdg6e42dtyqfgb3j6h.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
Originally, getting a task to Done looked like the natural end of that workflow.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;A finished task means the setter has finished the work. It doesn't necessarily mean RouteFlow has a new Route in its database.&lt;/p&gt;

&lt;p&gt;So publishing became a separate backend operation.&lt;/p&gt;

&lt;p&gt;Once a completed task is published, Django creates the Route and links it back to the original Setter Task. The operation is atomic, so if part of that process fails, it doesn't leave half-created data behind.&lt;/p&gt;

&lt;p&gt;It also means the same task can't accidentally produce two routes.&lt;/p&gt;

&lt;p&gt;That's a relatively small feature from the user's point of view — essentially an action in the interface — but it forced me to think much more carefully about where one part of the workflow actually ends and another begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I built the dashboard
&lt;/h2&gt;

&lt;p&gt;The most recent part of RouteFlow is the operational dashboard.&lt;/p&gt;

&lt;p&gt;By this stage the application already knew about setting sessions, tasks and published routes. The problem was that knowing something and making that information useful are two different things.&lt;/p&gt;

&lt;p&gt;I wanted the dashboard to answer fairly mundane but important questions:&lt;/p&gt;

&lt;p&gt;What's being set right now?&lt;/p&gt;

&lt;p&gt;How far along is it?&lt;/p&gt;

&lt;p&gt;What's coming next?&lt;/p&gt;

&lt;p&gt;Is anything late?&lt;/p&gt;

&lt;p&gt;Are there enough tasks assigned to actually hit the planned number of routes?&lt;/p&gt;

&lt;p&gt;So an active session now shows its sector, lead setter, planned date, target route count and the current state of its tasks.&lt;/p&gt;

&lt;p&gt;Task progress is split between Todo, In Progress, Testing and Done.&lt;/p&gt;

&lt;p&gt;The dashboard also compares the target number of routes with the number actually published.&lt;/p&gt;

&lt;p&gt;That distinction caused one small change in how I thought about completion.&lt;/p&gt;

&lt;p&gt;At first it would have been easy to calculate session progress from tasks marked Done. But a done task isn't necessarily a published route.&lt;/p&gt;

&lt;p&gt;If a session is supposed to produce 10 routes and six tasks are done but only four routes have actually been published, calling the session 60% complete would be misleading.&lt;/p&gt;

&lt;p&gt;So RouteFlow considers it 40% complete.&lt;/p&gt;

&lt;p&gt;It sounds like a minor detail, but those are exactly the details that start appearing once separate features have to work together as one system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that need attention
&lt;/h2&gt;

&lt;p&gt;The dashboard also has an Attention Required section.&lt;/p&gt;

&lt;p&gt;Currently it catches things such as overdue tasks, overdue setting sessions and sessions where the number of assigned tasks is lower than the target number of routes.&lt;/p&gt;

&lt;p&gt;I actually considered adding an alert for sessions without a lead setter while building this.&lt;/p&gt;

&lt;p&gt;Then I realised the application already requires a lead setter when creating a session.&lt;/p&gt;

&lt;p&gt;There was no point adding dashboard logic for a state the system doesn't allow to exist.&lt;/p&gt;

&lt;p&gt;Removing that check was probably more useful than adding another dashboard card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;RouteFlow isn't finished, and I don't want to present it as if it is.&lt;/p&gt;

&lt;p&gt;What works now is the core operational chain:&lt;/p&gt;

&lt;p&gt;Setting Session → Setter Tasks → Task Workflow → Route Publishing → Operational Dashboard&lt;/p&gt;

&lt;p&gt;The next milestone is mostly cleanup.&lt;/p&gt;

&lt;p&gt;There is still some demo and fallback data left from earlier stages of frontend development. I'm removing that and checking the application screen by screen to make sure the API is the actual source of truth everywhere.&lt;/p&gt;

&lt;p&gt;After that I want to run the whole thing the way a new gym would: start with an empty account, create the gym structure, add setters and routes, plan a session, assign the work, publish the resulting routes and see whether anything requires developer intervention along the way.&lt;/p&gt;

&lt;p&gt;If it does, v0.1 isn't ready yet.&lt;/p&gt;

&lt;p&gt;Once that works, the useful part starts: putting RouteFlow in front of people who actually run climbing gyms and seeing which assumptions were right and which ones weren't.&lt;/p&gt;

&lt;p&gt;For me, RouteFlow is also a good example of the kind of development work I want to do more of: Django, REST APIs and applications where the interesting problems aren't just about displaying data, but about defining what should happen to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;RouteFlow is still under active development, but if you'd like to take a look at the project itself, the source code is available on GitHub:&lt;a href="https://github.com/Efowski/routeflow" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'll be sharing more of the technical decisions, mistakes and lessons from building RouteFlow as the project moves toward its first usable release.&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
