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.
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.
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."
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.
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:
- Reuse. The backend already existed in production. The repos didn't inspire the product; they were the product, one tenancy layer away.
- 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.
- 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.
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.
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.
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.
- Product discovery
- Architecture & ADRs
- Issue breakdown
- Tenancy boundary
- Ingestion & catalogue
- Public site & dashboard
- OAuth onboarding
- Pick the email provider
- Confirm the domain
- Run the validation
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.
- Fable · ~Jun 11Discovery & the kill passTen candidate products mapped from four repos, scored, and culled to one survivor. Setfolio scored 91/110.
- Fable · 72-hour windowArchitecture & ADRsSolution architecture, repo conventions, and the issue breakdown, every major call captured as a decision record.
- HandoffFable goes dark → Opus picks it upAccess cut off overnight. The build continued on Opus from the ADRs and the labelled issue board, with no architectural rewrite.
- Opus · Core buildTenancy, auth, ingestion, catalogueTenancy boundary first, then magic-link auth, RSS ingestion, and the catalogue model, built in dependency order.
- Opus · SurfacesPublic site, dashboard & design overhaulThe public archive and search, the DJ dashboard with its live-preview paste editor, and a pass on the visual identity.
- Opus · OperationsMulti-env infra, admin console, backups, SEOGated multi-environment deploy pipeline, an operator console, structured logging, nightly backups with a tested restore, per-tenant sitemaps, and branded error pages.
- Opus · ~Jun 21Security pivot: OAuth onboardingReplaced "paste any RSS URL" with "Connect with SoundCloud" (OAuth 2.1 + PKCE), closing a compliance gap and removing onboarding friction in one move.
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.
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.
- No proof of ownership
- Anyone can archive anyone
- Only recent / RSS-enabled tracks
- On the wrong side of SoundCloud's terms
- 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
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.
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.
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.
- ≥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?"
- "Cool!" … then silence
- Nobody changes their bio
- Tracklist-paste rate is roughly zero; the data moat never forms
- Week-two return is roughly zero
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.
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
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.