Case Studies
Engineering Case Study

Setfolio: A Multi-Tenant .NET SaaS, Shipped With an AI-Assisted Process

A multi-tenant .NET 10 SaaS taken from first requirements to live on Azure in about a week, using an AI-assisted process I documented as I went. Midway through, access to the model doing the work was cut off by a US government decision, and a second model finished the build from the decisions the first had written down. The process held. I know it's built. I don't yet know if anyone wants it. And that, not the code, is what decides whether this is a business or a case study.

Type Engineering Case Study
Domain SaaS · Architecture · AI-Assisted Development
Stack .NET 10 · ASP.NET Core · SQLite · Azure
Status Live · Beta
~10 days
build window, June 2026
9 projects
6-layer clean architecture
8 ADRs
decisions on the record
0 rewrites
across the Fable → Opus handoff
The Setfolio public archive: featured mix hero at the top, mix grid below
The thing I actually shipped: a DJ connects SoundCloud once and gets a public, searchable home for every mix they've ever made.

The product is simple. The way it got built isn't, and that's the only reason it's worth writing up.

I had roughly 72 hours with Fable, one of the two frontier language models this build ran on (Opus is the other), before a US government decision pulled my access overnight. I didn't know the clock was running. What I did have, out of habit rather than foresight, was a complete set of architecture documents and decision records that Fable had written before touching a line of product code. When I handed the build to Opus, the project didn't reset. It just continued, issue by issue, from the notes already on the record.

At a glance

What it is A multi-tenant SaaS that turns a DJ's SoundCloud back-catalogue into a public, explorable archive. Every mix on its own page, every tracklist searchable, with per-artist pages ("every set where I played Calibre").
Stack .NET 10 · C# · ASP.NET Core Razor Pages · EF Core / SQLite · Azure App Service (B1) · Blob Storage · Resend
Build window ~10 days, June 2026
Shape of it 9 projects in a 6-layer clean architecture · ~315 C# files · 8 ADRs · 9 operational runbooks · a deterministic test suite around an 11-fixture tracklist-parser corpus
Where it is now Live on setfolio.app, deployed beside my existing apps on one B1 plan, OAuth onboarding shipped, v1.0.0-beta.15
The unusual part Discovery and architecture were done by Fable 5. When my access was cut off mid-project I handed the build to Opus, issue by issue.
The honest part It's built. Whether anyone wants it is still unproven, and that, not the code, is the make-or-break.

The half-answers I'd been carrying around

For a working DJ, SoundCloud is usually where the mixes live, and it's genuinely good at what it's built for: hosting audio, a feed, plays, reposts, discovery. But it's SoundCloud's house, not yours. Your catalogue sits in the same template as everyone else's, arranged the way the platform wants it: newest first, social signals up front, and no way to search what's actually inside a mix. Ask "which of my sets has Goldie in it" and SoundCloud has no answer, because the tracklist is just dead text in a description, if it's there at all. Each upload stands alone, with no thread between sets, genres or moods. So DJs improvise around it: a link in one place, an Instagram bio in another, a tracklist pasted into a caption. SoundCloud is a great place to upload. It was never meant to be a personal, explorable home for a body of work.

I didn't set out to build a SaaS. Over a few months I built four DJ tools for myself, each one better than anything I could find, each one a dead end when I tried to imagine it as a product. TuneFinder scraped music stores but I could never guarantee 100% uptime as sites blocked or died on me (Juno a prime example). MixLab proved set-planning could be pure algorithm, but every run cost API money. Fine for one person, not great as a business. The Rekordbox enricher hit MusicBrainz's one-request-a-second ceiling and wanted access to people's private libraries, which felt wrong from the start. And changsta.com, my own DJ archive site, a .NET backend ingesting my SoundCloud feed, parsing tracklists, serving a shared-producer graph, a radio mode, a genre compass: by my own entirely objective assessment, the best DJ archive site anywhere. Audience of one.

TuneFinder
Never build a product on data you have to take without permission.
MixLab
Per-run LLM cost is fine for one user, fatal as a business.
Rekordbox Enricher
Confidence-scoring, and only ever export what changed.
changsta.com
The best DJ archive I could find, with an audience of one.
A few months of side-projects, each one a lesson, and each one a reason not to build the next version of the same mistake.

What I couldn't answer was which of them, if any, someone other than me would actually care about.

The 72-hour window

When Fable 5 launched, I didn't burn it on code generation. I spent it on the question I'd never properly answered: whether any of my DJ tools was actually a business. I had no idea that roughly 72 hours later a US government decision would pull my access offline.

At the time I was on the pro plan, which is to say I was token-poor, and I'd spent the previous few months learning to work that way: spend the model on thinking, not on producing code I might throw away. I didn't burn Fable on "help me build X." I spent it on what to build at all, and on getting the architecture properly defined. That constraint, not any plan I'd made, turned out to be a blessing in disguise.

I gave Fable everything: all four repos, market research, my own gut feelings. And I asked it to do discovery, not implementation. Not "help me build X," but "here is everything I've made. Tell me, brutally, whether any of it is a business."

The brief I held it to: no AI in the product path, no scraping, no private-library handling, must run on the B1 Azure plan I already pay for, must validate for roughly £0. Constraints first, ideas second.

What came back mapped ten candidate products, scored each one across eleven dimensions, and killed eight of them. It was more honest about my own side-projects than I'd been.

Discovery: ten ideas, one survivor

Fable mapped ten candidate products out of the repos, scored them, and then ran a brutal kill pass, killing most of them while I watched.

✅ Wedge
Setfolio - a DJ's archive as an explorable public site
~70% already built, lowest legal surface in the domain, fastest to validate. Scored 91/110.
↗ Ring two
Selecta Radio - scheduled, radio-style stations for friend-groups
Great hook (radio's shared feel, zero broadcasting), but recruiting whole groups at once is slow.
↗ Deferred
Resident Archive - a B2B tool that archives stations' broadcasts
Real revenue, but B2B selling is too heavy a first move solo.
❌ Killed
Crate Report Card - Rekordbox library health
Lexicon already is this, at £8/mo, with audio fingerprinting.
❌ Killed
Setlist Studio - MixLab as SaaS
DJ.Studio owns it, and every run costs LLM money.
❌ Killed
Tracklist Pages (standalone)
1001Tracklists' SEO fortress above it, moderation tax below.
❌ Killed
Label & Scene Digest
"TuneFinder with a moustache"; same dying sources.
➰ Folded in
DJ Wrapped · Publishing Copilot · Mix Memory
Features of the winner, not products.
The kill board. Survivors in accent; killed ideas struck through. It was great to see a model arguing against my own enthusiasm.

The scoring covered eleven dimensions. Most were the expected ones: commercial viability, legal safety, solo-founder feasibility. The one that stuck was whether I'd personally care about it, which is simultaneously the most important test and the least useful evidence. Setfolio scored 91 out of 110: a perfect 10 on reuse, near-perfect on legal safety and validation speed, and dragged down only on the honest axes. The pain it solves is chronic-mild rather than acute, and its defensibility rests on taste and accumulated data rather than a moat. The radio mode scored 80, on the strength of the best single insight in the whole space ("radio without broadcasting" decouples the programming layer from the licensing cost that defines the entire market), but it loses on reach: crews are slower to recruit and coordinate than individuals. Setfolio won on three boring, decisive things:

  1. Reuse. The backend already existed in production. The repos didn't inspire the product; they were the product, one tenancy layer away.
  2. Legal safety. A DJ's own public data, fetched at their request, played back through official SoundCloud embeds. No audio ever stored. No scraping. The cleanest posture in the entire domain.
  3. Validation speed. I could generate a real DJ's real archive from their public feed and show it to them before they signed up. Nothing converts (or kills) faster than someone's own data staring back at them.

And the caveat, stated up front: this is a niche prosumer product. Best case is a sustainable few-hundred-quid-a-month side business, not a rocket. The defining unknown is whether any DJ other than me cares enough to put the link in their bio.

Fable argued against my own enthusiasm. I'm the ideal user of this thing, which is exactly why my excitement is worthless as evidence. The discovery document said so, in writing, before I'd typed a line of code.

The decision that breaks most side-projects

This is where I made a choice to go against what the discovery doc explicitly told me not to.

The right move, by the book, was to validate first: DM ten DJs, show them mock-ups, watch what they did, and only build if they bit. Instead I decided to build the MVP first, then validate.

I didn't pretend this was the safe call. I wrote the rationale down, and the conditions with it:

  • The small Fable window (which ended up far shorter than I expected) was better spent kicking off a real build than on cold DMs.
  • A production multi-tenant SaaS in .NET on Azure is a portfolio asset for me as a contractor regardless of the commercial outcome. That return is guaranteed; the commercial return is not.
  • The scope was genuinely small because of the reuse.

That's the tidy version, and all three bullets are true. The less tidy version is that I just wanted to build it. The portfolio logic came after the wanting, not before. What stopped it being pure indulgence was the clock: Fable was only ever mine until the 22nd of June, and on the pro plan I was never going to get much of it. So the real question was never "build or validate first." It was this is almost certainly the only time I'll ever get to build with this model, so what do I point it at? Validation could happen on any model, any month. Fable couldn't.

And if I'm being properly honest, I've already made my peace with how validation might go. If it comes back and tells me no one but me wants this, I'm okay with that, and it won't have been a waste. I'll have built a production SaaS I'm proud of, learned a great deal in the building, and have the case study either way. I've pre-committed to running the test properly. I just went into it knowing that a clean "no" is an outcome I can live with.

"I built it" never becomes "people want it." Validation still happens, after the build, with the real product. The kill thresholds still gate everything past the MVP. And if validation fails, this becomes a case study and it stops.

That last line is why this document exists. I pre-committed to writing it before I knew whether it would be a victory lap or a post-mortem. Right now it's neither. It's the pause in between.

Architecture: constraints as a feature

I spent the back half of the Fable window having it act as solution architect: write the architecture docs, the repo conventions, and the issue breakdown the build would follow. It never got to write the product code, and that turned out not to matter. Eight Architecture Decision Records, each one a constraint rather than a suggestion with the reasoning attached, were written before any product code existed.

System Map
DJ browser · dashboard
Listener browser · public archive
ASP.NET Core Razor Pages · setfolio.app
Layered: Core · Contracts · BusinessProcesses · Infrastructure.* (tenancy enforced in the data layer)
SQLite · source of truth
Azure Blob · artwork
→ embeds only
SoundCloud player · audio never stored
One web app, one database file, one storage account. Anything fancier needs a reason in an ADR. The audio never touches the system: it's played back through official SoundCloud embeds.

The one decision worth explaining to anyone, not just an engineer, is the one I think of as inverting the data flow. My old changsta pipeline demanded a proprietary block of metadata inside every SoundCloud description and fell over without it. Fine for one obsessive operator (me), useless for everyone else. Setfolio flips it: the database is the source of truth, and the SoundCloud description becomes generated output, never required input. The feed supplies only the reliable shell (title, date, artwork), and the DJ enters tracklists into a forgiving editor. That single inversion made the promise honest ("instant shell, then about a minute per mix to unlock the rest") and the system simpler at the same time.

ADR-001 captures the exact reasoning: "RSS supplies shell fields only: title, date, duration, artwork, permalink. These are reliable for every SoundCloud user with zero setup. Tracklists and all intelligence are first-party data entered in Setfolio. The SoundCloud description becomes a generated export the DJ can copy back. Never required for the product to function." That's the whole product promise in one paragraph. It's also why the onboarding claim is honest.

The rest of the big calls got the same treatment, each one constrained on purpose rather than left open. SQLite, for instance: chosen not because it's impressive but because the entire beta is a few hundred thousand rows of text on one instance, with five explicit migration triggers named in the ADR (write contention, a second App Service instance, a file over 2 GB) so the decision is bounded, not lazy. Embeds-only and data-layer tenancy, both visible in the map above, were settled the same way.

The handoff: Fable goes dark, Opus picks it up

Then, with no warning, it was gone.

I found out scrolling Twitter first thing in the morning: a US government decision, nothing to do with my account or my project. A mate messaged a few minutes later to check I'd seen it. I had. The first feeling wasn't panic, just a flat disappointment, and then a more practical unease right behind it: where exactly had I got to? How much of this was actually built, and how much still lived only in a conversation I could no longer reopen?

One day my solution architect was there; the next it wasn't. If you've ever had a key contractor walk off a job mid-sprint, it felt exactly like that.

The difference is that this contractor had written everything down first.

Even with the architecture fully documented, there was plenty I'd wanted to do with Fable that I never got to. A tool like this needs a wow factor for the first time a DJ sees their full archive, it has to land. I never got to explore that with Fable, never got to push on the visual identity or brainstorm the features neither of us had thought of yet. Opus is a capable model and it did the job, but it's not Fable. The creative conversation I'd planned to have never happened.

The first Opus session opened with the architecture document, the eight ADRs, and the labelled issue board. I didn't need to re-explain the tenancy model, the data flow inversion, or why SQLite rather than Postgres. Those decisions were already on the record with their reasoning, not notes to a human collaborator but formal constraints any capable implementor could pick up cold. Opus did. The handoff from Fable to Opus was a label change on a GitHub issue board, not a rewrite.

I leaned into the seam rather than hiding it. Every issue got an agent:* label saying which model should implement it: the heavy, ambiguous epics tagged for the most capable model, the mechanical tickets sized for cheaper, faster ones, and the human decisions (pick the email provider, confirm the domain, run the validation) tagged agent:human.

agent:fable
  • Product discovery
  • Architecture & ADRs
  • Issue breakdown
agent:opus
  • Tenancy boundary
  • Ingestion & catalogue
  • Public site & dashboard
  • OAuth onboarding
agent:human
  • Pick the email provider
  • Confirm the domain
  • Run the validation
Every ticket said who should do it: a model, or me. The handoff from Fable to Opus was a label change, not a rewrite, because the constraints lived in the ADRs.

The build, and the things that broke

The MVP came together as twelve founding issues, each a real subsystem, built in dependency order: the tenancy boundary first, because it's the one thing you can't bolt on afterwards, then magic-link auth, RSS ingestion, the public archive, the dashboard, and backups with a tested restore.

  1. Fable · ~Jun 11
    Discovery & the kill pass
    Ten candidate products mapped from four repos, scored, and culled to one survivor. Setfolio scored 91/110.
  2. Fable · 72-hour window
    Architecture & ADRs
    Solution architecture, repo conventions, and the issue breakdown, every major call captured as a decision record.
  3. Handoff
    Fable goes dark → Opus picks it up
    Access cut off overnight. The build continued on Opus from the ADRs and the labelled issue board, with no architectural rewrite.
  4. Opus · Core build
    Tenancy, auth, ingestion, catalogue
    Tenancy boundary first, then magic-link auth, RSS ingestion, and the catalogue model, built in dependency order.
  5. Opus · Surfaces
    Public site, dashboard & design overhaul
    The public archive and search, the DJ dashboard with its live-preview paste editor, and a pass on the visual identity.
  6. Opus · Operations
    Multi-env infra, admin console, backups, SEO
    Gated multi-environment deploy pipeline, an operator console, structured logging, nightly backups with a tested restore, per-tenant sitemaps, and branded error pages.
  7. Opus · ~Jun 21
    Security pivot: OAuth onboarding
    Replaced "paste any RSS URL" with "Connect with SoundCloud" (OAuth 2.1 + PKCE), closing a compliance gap and removing onboarding friction in one move.
Roughly ten days, June 11–21. The Fable → Opus handoff sits in the middle; the constraints written down before it are why the line didn't break there.

The one piece worth singling out is the tracklist parser, because it's where the product lives or dies. DJs write tracklists a dozen different ways: numbered or not, with labels, with timestamps, with w/ and ID - ID. The parser has to make sense of all of them without ever silently dropping a line. The bar is "good, not perfect": around 80% of lines parsed automatically, everything human-correctable, the raw text always kept. It ships with a corpus of 11 fixtures spanning the common dialects, including a couple of deliberately messy fail-gracefully cases, and every new dialect has to add one.

The dashboard tracklist paste editor: raw text on the left, live parsed rows on the right
The paste editor with live preview. The parser does ~80% of the work; the human owns the last 20%, and nothing is ever lost.

Past the MVP came the unglamorous half that separates a demo from a product. The clearest example is the deploy pipeline: dev auto-deploys, prod is manual, reviewer-gated and SHA-pinned. The rest is the invisible scaffolding, an operator console, structured logging, per-tenant sitemaps, branded error pages, the stuff that keeps a live site running smoothly. The tests follow the same instinct as the rest of it: deterministic (no live SoundCloud, no network, no real blob storage) and aimed at the parts that can actually hurt you, the parser and the tenancy boundary. I don't chase a coverage percentage; in my experience 100% usually just means you've started testing things that never needed it. And nine operational runbooks, because "how do I restore the database at 2am" is a question you want answered before 2am.

Not all of it went smoothly, and the failures are the part I'd actually keep:

  • The B1 ran out of memory, and the fix was to remove a problem, not buy a bigger box. That little plan was already hosting my consultancy site (Soltech), an ASP.NET MVC app, and a .NET Core WebApi that powered changsta.com. Dropping Setfolio beside it tipped the whole thing into out-of-memory territory. Rather than pay to scale up, I rebuilt Soltech as an Astro site and moved it to Cloudflare, new GitHub CI/CD pipeline and all, in a single day. That freed enough headroom to run Setfolio safely. Sometimes the cheapest scaling move is taking the right thing off the box.
  • My test suite was testing the wrong parser. For a stretch, the eleven fixtures were exercising a different parser than the one the paste editor actually called. Every test passed. The parser the editor actually used had no coverage at all. A green suite is comfortable enough that you stop checking what it's pointed at. That's exactly when you should check.

The realisation that became a pivot

Near the end, a nagging security thought turned into a compliance problem I had to fix before anyone else touched the product.

The original onboarding asked a DJ to paste their SoundCloud RSS feed URL. Simple, low-friction, and anyone could paste anyone's feed. Nothing stopped a stranger from standing up an "official" archive for a DJ they'd never met. Impersonation, baked right into the front door.

Before · impersonation risk
Paste any RSS URL
  • No proof of ownership
  • Anyone can archive anyone
  • Only recent / RSS-enabled tracks
  • On the wrong side of SoundCloud's terms
After · proven owner
Connect with SoundCloud (OAuth 2.1 + PKCE)
  • Call to /me proves the account is yours
  • Derive the feed URL and public slug
  • Full public catalogue in one pass
  • Token used once, then discarded
One move took a compliance problem and my single worst piece of onboarding UX and replaced them both with a one-click sign-in.

That realisation sent me back to SoundCloud's terms, properly this time, and I didn't like what I found. Standing up archives from pasted feeds, with no proof the person setting one up actually owned it, put me on the wrong side of those terms. This wasn't a "harden it later" nicety any more. It was something I was already getting wrong, and it had to be fixed before a single stranger onboarded.

Then a piece of good fortune came my way: SoundCloud had quietly re-enabled their API in May, after a long stretch of it being effectively closed. That changed the whole shape of the fix. Instead of patching the paste flow, I could replace it. Onboarding became "Connect with SoundCloud": OAuth 2.1 with PKCE, then a call to /me that proves the account is yours. From the verified identity I derive the feed URL and the public slug, and the DJ's full public catalogue comes through in one pass rather than the partial slice the RSS feed exposes. The verification token is used once and then discarded; I never hold long-lived credentials.

And here's the part that turned a chore into a gift: the same change quietly removed all the onboarding friction that had been bothering me for weeks. The old RSS feed only carried recent or RSS-enabled tracks, so a new DJ had to fiddle mix-by-mix just to get a complete archive. The API simply enumerates the whole public catalogue.

Wiring up OAuth isn't the point; anyone can wire OAuth. The point is the timing: I'd realised I was in breach with no users yet to notice and no external pressure to force my hand, and I fixed it before a single stranger onboarded. The liability turned into a better product on the way through.

Where it actually stands

As I write this, Setfolio is real and live, running at setfolio.app beside my other apps on that same B1 plan. Everything in the box at the top of this page is shipped: one-click SoundCloud onboarding, the public archive and search, the DJ dashboard, an operator console, and nightly backups with a tested restore. The only archive on it so far is mine: 121 mixes, every tracklist tracked, nothing outstanding.

The Setfolio operator dashboard showing 121 mixes synced and the full mix list
The operator dashboard, running my own archive.

By the measure I set at the start (build a production SaaS worth showing to a client), it's done. Properly layered, tested where it counts, shipped through a gated pipeline.

And by the measure that actually decides whether it's a business, I haven't started.

The real test: validation, and the thresholds I can't move

The uncomfortable truth, the one the discovery doc made me write down before I'd typed a line of code, is that everything above is the easy half. I'm a software contractor; building software is the thing I'm confident I can do. What I can't do by being good at my job is make DJs care.

So the next phase isn't a feature. It's a verdict. I'm going to recruit 10 to 15 real DJs: a few friends (whose words I'll weight low, because friends are kind), a few community-radio and podcast hosts, and a majority of strangers with visibly maintained archives. Each one gets their archive already built and waiting: "I ran your public feed through this. Log in and publish it if you want it."

And then I watch what they do, not what they say.

Proves it's a product
  • ≥40% of invited DJs publish their page
  • ≥3 put the link in their bio, unprompted
  • They return in week two without nudging
  • They paste tracklists for old mixes (effort is the strongest currency)
  • A community post pulls a waitlist past 50
  • Anyone, unprompted, asks "can I pay for this?"
Kills it
  • "Cool!" … then silence
  • Nobody changes their bio
  • Tracklist-paste rate is roughly zero; the data moat never forms
  • Week-two return is roughly zero
A pre-registered bet. Strangers' behaviour outweighs friends' words three to one. Compliments are noise; I count only actions.

The rule I wrote for myself: if three or more of the kill signals hold by day 30, I write the post-mortem, publish this case study as the ending, and stop. Strangers' behaviour outweighs friends' words three to one. Compliments are noise. I count only actions: bio links, pasted tracklists, return visits, pledge clicks.

The pricing test is deliberately cheap: a "Founding supporter: £4/mo at launch, locks your subdomain forever" button that goes to a thank-you page. No payment plumbing. The click is the signal.

I genuinely don't know which way this goes. That's the honest state of the project, and I think it's a more interesting place to write from than a tidy success story would be.

What I'm taking away (whichever way it lands)

A few things I believe more firmly now than I did ten days ago:

  • Constraints written down as decisions are what make a project survive losing its author. When Fable disappeared overnight, the ADRs and discovery documentation are why Opus could pick up the exact same project instead of a vague approximation of it. I'd like to call having them ready foresight. It was mostly habit. But it's the habit I'd never give up now.
  • Use the expensive thinking on the expensive question. The best thing I did with a powerful model wasn't generating code. It was making it argue, brutally, against the idea I was already in love with. The kill pass saved me from building three products that were already someone else's.
A Setfolio mix detail page: artwork, player, and tracklist, exactly what a fan lands on
Setfolio is, among other things, a 72-hour proof that front-loading judgement beats brute-forcing tokens.

If validation passes, the next chapter is about turning a built thing into a beloved one: parser hardening from real pastes, per-tenant theming, Mixcloud, and eventually the radio mode for crews that I've kept caged on purpose.

If it fails, then I'll have learned that this domain gives me excellent personal tooling and no business. And I'll have a production-grade .NET SaaS, built in ten days across a model handoff I didn't choose, to show for it. Either way, the next thirty days are the part that actually counts, judged by DJs rather than me, against kill conditions I've written down where I can't quietly move them. Then I'll know.

Tech Stack

Setfolio - Multi-tenant SaaS
.NET 10 C# ASP.NET Core Razor Pages EF Core SQLite Azure App Service (B1) Azure Blob Storage Resend OAuth 2.1 + PKCE SoundCloud API
Architecture & Delivery
6-layer clean architecture 8 Architecture Decision Records 9 operational runbooks Deterministic test suite Gated multi-env CI/CD Nightly backups + tested restore
AI-Assisted Workflow
Fable 5 (discovery + architecture) Opus (implementation) GitHub issues · agent:* labels
Case Studies

A production SaaS, built across a model handoff

Setfolio is live at setfolio.app. Not a mock-up, the actual product. If you're a DJ with a SoundCloud worth keeping, connect it and tell me what's wrong with the result. Honest reactions are exactly the evidence this next phase runs on.

If you're sizing me up for engineering work, this is the work sample: the ADRs, the test suite, the gated pipeline, and a model handoff that didn't break the build. Feel free to get in touch.