Course Builder: Commerce Components as a Monorepo Package
This overview highlights a Turborepo monorepo setup focused on software architecture involving Next.js apps using React Server Components. The main applications within this repository include Course Builder Web, which serves as a proof of concept at coursebuilder.dev, and ProAWS, driven by Course Builder as a partner release.
Recent updates introduced commerce functionalities to the ProAWS by integrating live workshop products. This setup involved porting over commerce components from another project into Course Builder Web, leveraging React.
A significant development is the shift from utilizing an external CMS system to deploying an internally built Course Builder CMS, backed by an LLM assistant, thereby enabling direct content management on the site.
The commerce functionalities have been encapsulated into a new package called commerce-next
, reflecting the move towards framework-agnostic components, potentially extensible beyond Next.js. Various configurations, including TypeScript and tsc, were explored to resolve component compilation challenges effectively.
Looking forward, the architecture might evolve to treat products as content resources, simplifying handling and improving scalability across different interfaces and functions.
Transcript
[00:02] This is an overview of the Course Builder Monorepo, which is running TurboRepo, PNPM, and has a setup that's kind of similar to UploadThing in a lot of ways, and then some divergence since it's different in nature. There's the apps folder, currently CourseBuilder web, which is the kind of proof of concept that lives at coursebuilder.dev. And then ProAWS, which is Adam Elmore's and is our first partner release with CourseBuilder driving it. Part of the last set of updates have been around setting ProAWS for commerce specifically. We're decent on content but we didn't have commerce so that meant with Course Builder Web adding products and the ability to buy products.
[01:04] The first product that we're going to sell with ProAWS is actually a live workshop. So I went down the road of setting that up first and bringing over everything from our skill stack in terms of commerce and just kind of dumping it in to the course builder web and made folders for that. And I got that working and had all of our price checking. Let's see if I can actually get this running. Here.
[01:53] Oh, bring this over. And I'll bring the web browser over. So that's loading. So that's loading. There we go.
[02:30] And what we did here is we have products. And I'm logged in. So it's showing that. So this is our logged in view. Obviously it's asking us to buy more seats.
[02:59] I would like to log out. Instead I'll just do this. There we go. So here's an anonymous view. It has teams.
[03:10] You can, it does purchase power parity, all that stuff. Basically just plopped right over. It was pretty nice. One thing that's different, that's interesting is inside of products, previously we were using sanity and... Oh, this is a logged out view.
[03:36] Close this one. And now we're not. So we're actually using our own course builder CMS. And it's fledgling state. And that gives us this ability to manage the content here on site, backed by an LLM assistant.
[04:01] You can, having some sort of connectivity issues or something because it's not loading from the database for some stuff. I don't know what's going on. Anyway, so we have products. We have pricing for the products and I thought I would go over like how this is set up and what it took to get all that stuff out of the apps folder and into a package which I'm calling for now commerce next I called it commerce next because there are some next specific items in here and I think those are mostly related to like links and images not using their APIs but using their components which I feel like we could eventually get rid of entirely so we could remove Next.js from the Commerce next and make it commerce UI or commerce in general let's see I'm looking for yeah so you end up with there's next off be nice to get rid of that. I don't know what this session is used for, if it's used at all.
[05:25] And then you have a navigation. So the router stuff is interesting. My solution has been to pass things in basically. So you have like the purchase transfer form. And this actually takes initiate purchase transfer as a method and then on the consumer side so back here in the course builder web there's actually a folder called purchase transfer that has the actions and these actions get get passed in so server actions get passed into the client-side components which are in the package.
[06:05] So that kind of flows down through this, I think is where the form is. Yeah, so through the form, through this status indicator that takes purchase, initiate, and cancel. And this is a used client. And in some of our packages here, we're using TSUP. And TSUP works great.
[06:25] It's a bundler, it uses ES build in the background. It's generally nice. But I was having issues with useClient specifically, and it says it supports it, but I could not get the bundles to function properly when I was using TSUP. So I tried a few different approaches to bundling, and none of them worked. And what I ended up using was just TSC.
[06:51] So it's simply compiling it. And that allowed me to have all of these components and get them compiled. And then they come into the dist folder compiled and we can import them directly. There's indexes in here also, but I found with the client component specifically, because not All of them are labeled as client, but they needed to be imported directly. They didn't work when they were in any sort of barrel file.
[07:26] So that's kind of a gotcha in terms of just how this is set up. It shouldn't, you should just be able to build in here normally. There is a project called OrbitKit. Look, my head's showing up over here, so. And in OrbitKit, there's a pretty cool TSUP setup, but it's complicated in that it uses some utilities and does this list.
[08:05] But what he's done is quite compelling because it allows you to consume this in a way that either transpiles or doesn't transpile. And that's really nice in that we could use it transpiled in nextjs and that gives you hot media reloading if you're actually working on these which is nice and you don't get that when it's this compiled library like we're doing over here so you don't get hot media reloading and all of your components. So if you're updating these and working on the package inside of the next app you're not getting that feedback loop and I'm pretty sure that this setup, that's the point, this will allow you to both trans-pile this library, this package, or consume it as like an NPM import on the broader, as a published package, which is, it's two different things. You have like the kind of this is in my monorepo and this is external and then or another way to look at that is if another package wanted to use this like the transpiling becomes an issue versus like Next.js that just handles it for you. So all these can be worked around but this is apparently more of a universal solution and OrbitKit has a lot of great work and ideas in it in general that's worth looking at for our own setup.
[09:30] So this is pretty clean and pretty straightforward and this is all related the the structure of commerce next is all related to the kind of the entities that work around or the entity and functionality so post-purchase pricing team a little dumpster for utils all the invoice stuff I actually want to get rid of those one in here it was like a one one failed attempt That was like a one failed attempt. There we go. Get rid of that one. There we go. And that should get rid of those.
[10:44] So there's no barrel file options. And these are all familiar if you have worked with Skill Stack. So these all come, are pulled and they're in skill lesson over there, I think for the most part. And this includes things like the coupon provider. That was one upgrade.
[11:00] So coupons work globally now if you use this little provider. But again it gets passed in get product get coupon for code. So that's that's the thing there is no dependency here and this is I think the important part to make sure we don't get all scrambled up in terms of interdependencies. A lot of that stuff needs to be configured and pushed in and things like get product and get coupon code because it can be from any adapter. So it needs to come from an adapter and this is on the client.
[11:32] So these get passed in, these are what we should understand as like server actions they get pulled from a specific adapter or client side and in this case like get product is over here and I think it's... These haven't, like how these work based on, like in here I'm doing a lot of like DB query kind of stuff and I think this could be abstracted out, like where eventually all of this stuff gets codified into the primary adapter. But this is, you know, like on here in course builder, the proof of concept, this is all kind of fluid and what actually needs to get broken out and what are our common things and how does that get bundled and distributed and if we have, this is really nice because the ability to drop into a DB query is very convenient but then you get this kind of specific you know this is Drizzle and specifically Drizzle with a MySQL in the background right like so that is is very specific and that ultimately can be convenient but then also difficult down the road when we are abstracting it or working across many files where we have the same code copied across a lot of different products.
[13:04] And that's good and bad and it really, it's one of those things where it depends. Functionally speaking, all of this stuff was already abstracted. So all of the commerce next stuff was was abstracted already in skill stack so we know this works across the across projects and and you know we can work on making this very generic and then thinking about what that also looks like from an action standpoint. Because to me, this could basically be configured, or it's like when you get down to it, like add resource to product, like these sorts of things that we know we're going to want to use and that end up being in the like crud ui or whatever or on the the learner side like the the consumption side we want to abstract those and and you know like have a package and then make it to where you're not using the adapter. So things like archive product and other stuff ends up being in, let's see, core.
[14:16] Here in the course builder adapter, right? Like, so you're passing this interface around and whatever is the concrete adapter that you're using. We only have Drizzle right now, but this could be other ones. And behind the scenes, the course builder adapter would be where you would find queries like this right like so archive product would just be something here on the adapter that you could use an access like these other other things get price for product or get video resource these specific API's get abstracted behind the course builder adapter and all of these queries where they make sense move into the adapter. I think the place where I'm kind of on the fence, One product currently is modeled in our schema.
[15:12] So let me get to the... It's modeled in the schema as a product, but when I look at this, and I'm gonna open up another schema and split this like this, and you look at these side by side, they're very, very similar. And like this has a name up at the top, which isn't even that convenient. And the status and then it has quantity, which is actually metadata, probably belongs in fields. So things like name, key, and status and quantity available, all kind of belong in fields.
[15:58] And then when you look at that, the product looks like a content resource and makes me think, maybe this is a content resource. And what's interesting here is there is no, like if you look at this in the product schema, there is no reference to anything external. And what I ended up with was, I had to have content resource product, and then you, they're so similar, the content resource and resource and content resource product are very similar in the join tables that connect the resource and the product. But up here at merchant product, this is where the product id is and instead this could just be a resource id and the resource id would be the specific product. This is what is being sold.
[16:48] This is what connects the merchant product to the content. And I think that's gonna be probably the next thing I work on is making products, just a content resource that is connected to a merchant product, which actually, then behind the scenes connects to Stripe currently or whatever the provider is. And that designates that this is a thing for sale and has a button to push to buy it and all that fun stuff. So I don't know if there's anything... Oh we're in core.
[17:27] This is the adapter. So the kind of the rules of this thing is keep externals to a minimum even stuff like Next.js I would love to get rid of this it can depend on React that's totally and completely fine expected even but then the you know it's like what what gets what gets put in here and what gets added to the package.json has some stuff it needs, like core is always fine. That can be imported into anything that gives you the course builder adapter interface to use but not a concrete implementation so when these components are getting made and when these pages are getting made and when the you know all the crud UI stuff is getting made That's what we have to think about is passing that in and how do we deliver that externally. I've seen these, and we're using server components and we're using the, I don't think utils will have what I'm looking for. You know, use client where needed and I've heard this described as viral and just as a criticism not a where you know like use client and delineating those lines becomes tricky and it's probably true I don't know It definitely causes some rethink about how we deal with this and we can't, we have to avoid barrels.
[18:57] And if we, barrels are indexes, we're in, they call it barrels because you import them and they're surrounded by the brackets and the bracket is the barrel that all your dependencies are in. So avoiding those, these are set up in package.json, this is how these are imported so there's the export section as types import and require. I don't actually think we have the requires in this. No we don't. Interesting.
[19:40] I think all these end up as JSX, so Probably something like this. I don't know if this will work. We'll see Alright, I'm going to go see if that actually functions as expected. We'll come into app, commerce products, slug. There we go.
[20:55] There we go. Yeah, seems to work. Cool. I guess I'd have to build it to actually fully know. Yeah, maybe not.
[21:21] Yeah, maybe not. Oh, yeah, that makes sense. Anyway, I'll fix this up. That's the tour. Haven't added much more.
[21:42] Still thinking about this stuff. Wanna add some tests. Don't have any tests on this stuff. I think it would be a good place for a couple different things. I think storybook would actually be a pretty nice addition to the commerce next folder or just in a very simple way to look at and kind of run the component and isol components in isolation whether it's invoices or any of the pricing stuff I think would be nice to just just have that we could mess with there's no styles here so we we definitely have kind of a generic set of styles that could be imported as a place to start specifically commerce and then the redeemed dialogue and stuff so these are basic styles that it kind of needs to operate so that's another consideration.
[22:36] I think that's about it.