Kent C. Dodds and Joel Talk About Egghead on Epic Stack Aka Epic-egghead

Kent C. Dodds and Joel Hooks discuss the best data model for a new software project that involves organizing educational content such as lessons, tutorials, and various types of resources like videos and articles. They are contemplating the trade-offs between using a flexible polymorphic approach and a more structured, type-safe approach with separate models for different resource types. They want the flexibility to iterate and evolve the model but also the benefits of the tooling provided by a more rigid schema, which allows for easier data migrations. The conversation touches on practical aspects of managing and querying the data, the application of runtime and static type safety, and the implications of each choice for the development team and code maintainability.

diagram of egghead information architecture

  • Debating the architecture and scalability of various projects and considering a unified content authoring platform.
  • Valuing hard deletions over soft deletions in databases.
  • Mention of adapting authentication methods and the use of RemixAuth.
  • Interest in incorporating exercise types in lessons and structuring content as modules and collections.
  • Considering the future of and the potential of scaling as a self-service platform.
  • Discussion on authentication, the benefits of rolling custom auth, and excitement about using device flows.
  • Emphasizing the focus on creating a compelling content authoring experience and leveraging emerging AI technology.
  • Exploring Epic Stack and its database layer with plans to build out content-consumption experiences.
  • Considering how to migrate users and content, as well as the implications of a Google algorithm change on traffic.
  • Discussion on the importance of mobile-first design for user experience and SEO.
  • Anticipation of a potential migration of various projects under a cohesive architectural framework.
  • Planning to engage with the code and Epic Stack more actively to work through challenges and evaluate suitability.


[00:03] What, with the epic stack? Yeah, like things are working out okay so far in the last like hour. Yeah, it deployed Just fine. Sweet! You went through the whole deployment setup and everything.

[00:20] I did. Good stuff. You wanna try multi-region already? Or multi-instance? The only thing I don't like is the docs.

[00:30] Because they're, like, like it needs to move to the website, I think. Yeah, it does. Yeah, there's actually a contributor who has built a Docs site already. In fact, I think if you go to, did I put that there yet? No, I was going to.

[00:50] It might be like or something, or epicstack. I can't remember. Anyway, I did put it somewhere, but he never finished and you know how it is. So yeah, we need to put docs on the site at some point. It just uses Prisma's standard migration stuff?

[01:14] Yep. Yeah, that happens as part of startup in the LightFS config. Yeah, one thing that I wasn't clear on, it didn't talk to me much when I was building it, and it put the random string after the app name. So I had one failed deployment because I had to go in and change that. Oh, I see, yeah.

[01:43] Yeah, maybe we could, well, We can definitely do better on the CLI. Eventually that will be a thing. Yeah, I mean otherwise, that's pretty small. That was just like the... And I know why that exists, why it gets the random string so you're not having weird conflicts anyway.

[02:02] Yeah, yeah. But I always change it because I like not having the random string. Right, yeah. So then it was just like go check your config and it was easy after that. Everything else seems to be...

[02:17] And there's a lot of... I've set up on fly before so I know that it's doing... It's pretty nice to have this step by step to get it up and running. Oh, we froze up. Oh, that's right.

[02:49] You're going to tell me you are nervous. Do you want it more sensitive than that? No. Yeah, I like it like that. There he is.

[03:51] There he is. I still can't hear your audio. Okay, now you can hear me. Yeah. Dude, I have no idea what happened.

[04:09] My computer just wigged out. So anyway, yeah. Oh, that was annoying too, having to change my, having the epic notes hard-coded through the whole thing. Oh, yeah, yeah, people have suggested doing like a config or something, but I always think like, what's the end state going to look like? You're not gonna have a config for the name of your app.

[04:36] So I'm gonna let you have it. We absolutely do in all of our apps. Yeah, we do. And because we're stamping it across so many things, right? Oh, it is useful for your use case, yeah, that does make sense.

[04:47] I can actually, let's see. I remember you helped me bootstrap my original site,, with Gatsby, and that did have a config for the title, too. And I thought that was kind of weird, but that makes sense now, why you do that. Yeah, and we even, so here's the config and it's the same. It hasn't changed much, it's probably pretty close to the Gatsby one.

[05:15] I'm not seeing your screen right now. Oh, I'm not sure. I've been sitting here thinking I was sharing my screen the whole time. Yeah. Yeah.

[05:24] Okay. Yeah, what I said was, when we get internationalization going, then we can put the app name in whatever we do for that. I think it's usually like this. I guess there's lots of different ways you could do it. Some sort of key value pairs to do internationalization.

[05:50] I guess people use Toml or other stuff instead. Yeah, there's all sorts. I still need to evaluate modern options. Because The last time I did that sort of thing, yeah, things have changed. Yeah, so it ends up looking like this, and we actually use environment for most of it, so we don't, the config is just so we can get in there easily, you know, like programmatically access it.

[06:15] And this isn't even very good, I would rather this was probably type safe at this point in my life. Yeah, you parse it with Zod or something. Yeah, like a schema. Everything gets a schema now. And I love it.

[06:26] Yeah, it's great. Yeah, so the next thing for me is... Where is it? Did I just give a bunch of secrets away? No, good.

[06:40] What editor is this? Is this VS Code? No, I use WebStorm. Oh, okay, cool. I try to use VS Code, I can't.

[06:53] There are parts of me that wishes I never left WebStorm. I left WebStorm to go to Vim, and it ruined me for years. It was the worst. I mean, I thought I liked it, and then I realized I hated building my own editor, and when I left, I couldn't figure out any other editor. It messed me up bad.

[07:15] Yeah, it's not as bad as changing your keyboard layout maybe. I tried Dvorak once and that was awful. John uses Colmac. Yeah, that's interesting. So for me, the next thing is really I need a data export.

[07:32] So I'm going to get my current egghead database. It's Postgres, so I'm just going to get it local. And then think about it. And like one of the problems I had with that and still kind of suffer with, and that has been too specific in terms of my, my table names, Like I would have courses, tutorials, tips, talks, you know, like as tables versus the other way of looking at it is modeling it as modules or collections or something. And that's how we do it in Sanity.

[08:04] So like just looking at, Sanity is nice because of the ultimate flexibility. So there's like some sort of middle ground, I think where I can get there. And for all of this, building the CMS is, I mean, that's really kind of what the app is. And Sanity makes it really difficult for our specific use case too, because it's like giving out access to Sanity is, it's just not geared to what we're doing, even though it does work. And I like it, and I like Grok, and it's incredibly nice if you have very malleable content, I guess.

[08:43] But I think I can get a lot of that out of just being a little more abstract in the modeling in Prisma and kind of get there. Yeah, that sounds fine to me. Yeah, cool. So sections, lessons, articles are like a subcategory of a lesson or something? They're just the same.

[09:11] Like an article is just a lesson without a video. And like the classic egghead lesson, right? Like which is video plus markdown. And then there's title and a few other properties but there's the same, you know, in our sanity schema which I'll probably just open up for a lot of this and mimic some of what we're doing there. It's just like, here's module and modules have types and tips and talks are a special one because they're there to capture like a top level thing.

[09:42] It's just like a category, they don't have sections, but it's literally all the same thing. And even articles where you'd be like, oh, well that's an article, so it's different, but it's not, it's just a lesson that has a markdown resource and no video resource. That makes sense. And then the way you produce lessons, and we call that an exercise, and Matt has done that as well, where it's the problem exercise solution. So that is a lesson module with the problem exercise and solution, all three as resources, right?

[10:17] Like, so this is a collection, this is a collection, sections are collections. It's collections and resources is the way it breaks down, but naming things at a little higher level than collections and resources pays off. So I think of this like a course is a module type that's paid, a tutorial is a module type that's free, and then tips and talks are kind of standalone. Just to separate, like, to separate from a navigation and information architectures perspective so that people using the site are like, oh, I know what this thing is. Yeah, yeah, totally.

[10:56] So when you, and then podcast too, yeah, that's cool. When you say you're doing a proof of concept of Egghead, are you talking, like, from what I'm looking at, this looks like Epic Web, but what we're saying. That is not coincidental. Okay, so are you. This is how we think about all of it.

[11:15] Like, all of it is this, and this is the current thinking, and Egghead and Total TypeScript and Epic Web, Epic React, they're literally all structurally the same thing, if that makes sense. You know what I mean? They're just all templates of the same idea and evolutions of what we did on AKEhead, right? Like, so it's kind of been the front runner for whatever we're doing as far as that goes. Gotcha.

[11:44] Okay. So We skipped the Gatsby bullet with Agent. Yeah. That was pretty close, man. That almost happened.

[11:53] You were, yeah. I remember seeing your proof of concepts and stuff. Oh my gosh. Oh, what a disaster that would have been. So like Epic stack gives me Rails fields and I think it's interesting, you know, like next and we're pretty well, we, the way we develop our apps is pretty well sorted in terms of next.

[12:15] And I'm like, oh, do I want to throw a wrench at it? But I don't think the best parts are really tied to Next. And at this point, I have to migrate to the app router if I'm going to do that anyway. So it's just a really good time to reconsider. And To be honest, when I was doing this migration project, I was doing the migration project to get all of the, the video out of, our, our archives and into ingested into mux.

[12:56] So that's, that's what the, it's a series of scripts that I built to do that. And I was like, I was sitting here messing with a CSV file, and I was like, oh, man, this is really horrible. So instead of a CSV file, I went, I can't love SQLite. He's always talking about that. So I loaded it up for this project, and I was like, wow, that's really nice.

[13:15] And what does that look like distributed? Like you've been banging the gong about for a while now. And so that to me was the kind of the tipping point because I think the speed problems and like the, you know, our core web vital stuff suffers because of the way our technology is built. The Rails app and then we have Sanity and we're pulling all this data in from different sources. And just laws of physics, it suffers from that.

[13:46] And then there's these inefficiencies I think we could get around if we started from scratch. And, you know, I know I'm preaching to the choir, but. Yeah, yeah, yeah, I'm totally, totally on board. And doesn't get the traffic that gets. But.

[14:07] I don't know, with the Google hit recently, it might. Oh really? That's too bad. Yeah, we had like something in October happened where it decimated our traffic, like in the literal definition of the term decimation. So a tenth?

[14:21] Yeah, like it took it down an order of magnitude. Oh my word. That's nice. And it was like a sharp cliff. So there was some sort of...

[14:28] Something, something happened. We didn't make any technical changes. So it's some sort of algorithmic change where they're now doing mobile first for basically everything, so they're indexing as mobile first, and then the Core Web Vitals stuff. You can use to. That's why you said that you wanted it to be mobile friendly and all that, or mobile first, if that makes sense.

[14:50] Yeah, so it has to be good in that experience. And it's all the stuff that y'all have been talking about, right, like where Ryan will throw his phone, you know, like this is how it looks on 3G. And you know when you're like oh it's a video streaming site so who's looking at it on 3G? It doesn't really matter. It needs to work for them.

[15:11] Or it needs you know it needs to function at that level. Or I'm pretty sure you're going to take hits. And it just kind of makes sense. So if I'm going to address this anyway, I should look at it from the level and performance first, and actually thinking about some of the optimizations, where traditionally I've been able to just kind of ignore them. Yeah, yeah, good call.

[15:34] That makes a lot of sense. So, I'll stop asking questions here in a second so we can actually get to doing the thing. But would you say that if you're successful with Egghead that like Egghead, in my mind, it makes more sense to have this one thing, and then you've got Epic Web and Total TypeScript and even Egghead are all like children of this overarching architecture project, right? So once we're done with this, do you foresee moving other projects to this? Or do you want to leave things with where they're at right now?

[16:15] Mostly I'm concerned with the, like right now, is authoring and the content management portion of it. So that's what Course Builder is, right? Like it's this idea that I want to build, like put this information architecture, I want to say, I say on Rails because that's what I want to do, but then there's the framework, so it's a little confusing. I want to put it in a box, basically. The boundary is flexible, but I want to be able to give people a content authoring experience that's really great.

[16:47] You can get in there and self-serve, but then also in a way that's collaborative, so that where it makes sense and you do the handoff where the team picks it up and you get other collaborators involved is really important. So how do you build that collaborative authoring environment? And that's fundamentally what I'm mostly interested in. And to me, all that is pretty portable, regardless of where I build it, because it's just going to be React at the end of the day. It's just CRUD too.

[17:20] It's not even that complex. So the complexity is handled. That's where PartyKit and Ingest and those things come in, is to handle the kind of businessy complexity. But the authoring should be very clean and focused and straightforward. Leveraging LLMs and the emerging AI stuff as a collaborator, not as something that does the work for you, but it is an always-on junior collaborator that you can interact with and has full context.

[17:51] I think that's interesting too. What does that mean? Yes, I prefer to build the same kinds of apps, but structurally, they're going to be... You know the differences, or maybe you don't because I know you don't build Next apps, but like there's like the router is the biggest, to me in my head is the biggest difference between them. That's kind of thinking about pages and routes and that sort of thing.

[18:19] The data part's gonna be pretty simple. And honestly, I didn't even realize that it was just Prisma. Oh, yeah, Prisma, man, all the way. Because we're using Prisma too. And I've been exploring Drizzle actually for Course Builder simply because I don't like generators.

[18:36] Has been my motivation for digging into that. And it's like SQL, type safe SQL, and it's been pretty nice. But I don't think swapping things in and out is even that hard with Epic Stack if you know what you're doing and have a good reason to. You have to have a pretty good reason though. Yeah, that is one of the guiding principles, is to make it easy to swap stuff out.

[18:58] But I like migrations and Drizzle doesn't got them. So you probably want to keep Prisma at least for those anyway. Yeah, I've seen people dual stack them to where you have Prisma, the schema and migrations. Yeah, that is a big missing thing where, We use PlanetScale, so it actually mitigates that a little bit from a database perspective. Even then, if I'm thinking about Course Builder and thinking about this from a platform perspective, Like you said, is this some sort of overarching thing where all these other ones...

[19:35] I don't know. Having Egghead as the central database was a problem before, but it wasn't built to do that either. So it's like, you know, thinking about that. Are these all clones or is there, you know, kind of a central hive that they're all working from are interesting questions. And if I was going to build a mass market course builder to where it was fully self-service, or you bring your own team or whatever, like a teachable or Podia or whatever.

[20:09] I think about that too and what would that look like and how do you scale that out versus the more boutique-y sites, which makes sense to be standalone. And then to me that means often removing dependencies where planet scale and sanity and just the endless list of service dependencies is really, they're cool and I love them on one hand, but then if you try to set up Course Builder today, where is it? Not dox myself, there we go. You end up with just a, it's a pretty big stack of stuff that you end up having to set up. And you don't even get rid of all of them.

[20:58] Yeah, you'll still have the mux thing, you'll probably still have the ingest thing, and you'll probably have Cloudinary and stuff. I'm gonna literally have all these things. Yeah, so that doesn't even get rid of these, and it's like, you know, because I don't want to, I want to use Cloudinary. And Are you familiar with hexagonal architecture? Have you seen that before?

[21:18] It sounds familiar, but I don't know. I think Java folks love it, other people too. But it's just the idea of you have inputs and outputs in their ports and adapters, where the services are adapters, right? So you have inputs like webhooks or users or whatever, that's the inputs flowing in, and those are the ports that are receiving incoming data, and then adapters to, on the right side, so W-R-I-T-E, so you're building adapters. So your image service would be an adapter and I'd want an adapter so I could swap those in or I'm using upload thing and maybe there's a VS3 adapter for media uploading, transcript adapter, socket adapter, and then GitHub and etc.

[22:11] Adapters in terms of being providers anyway where you can like OOffice kind of build like that anyway. I don't know if I want to... I don't know if I want to build a teachable competitor in my life. But that's on the table. So, you know, it's like I think about that too.

[22:31] Like, what if I wanted to take this... Make it self-service. Yeah, just that side of it, which is it's like a different business, but also interesting to have on the table. And something that, you know, like I think we could do it in a way that would be interesting. So I don't know if this iteration is that yet, but that's kind of the...

[22:50] That's in the back of your mind. And just like having so many core dependencies where it's like planet scale and these other things. Even like Vercel is kind of an issue for me. If you wanted to do something like that, it would probably get pretty expensive if you were even moderately successful. Yeah, yeah, totally.

[23:10] Well, and so you'll definitely want to hand roll auth as well. You don't want to handle auth for everybody's. Yeah, we use NextAuth, which I think is similar to, there's RemixAuth is what's getting used, right? Yeah, well, I use RemixAuth for your GitHub authentication that's built in, but everything else is just custom. It's easy.

[23:34] Yeah, and you know, it's like, I'm not scared of rolling authentication anyway. That's good. We did so. We did it recently with the device flows. Yeah, that was actually, I still think that is super cool.

[23:52] It's awesome, like it opens up all sorts of interesting possibilities if you take it to its conclusion, just like having that as a solved problem is now, Now there's a reference so you can just do that whenever you need to. That kind of stuff is interesting to me as well. For this project, when I talk about proof of concept, doing just the content side, like the read side, is what I'm going to do. Go read and get the experience from just a consumption to where existing, I would like it if existing Egghead users could log in. Egghead does OAuth and GitHub as a provider itself.

[24:30] So they can even log in with egghead would work in terms of doing that. But we've done the migrations already anyway out and it's super simple to like, or super similar to what testing JavaScript did in terms of, you know, like because they're all the same thing, right? Like, so migrating testing JavaScript speaks to, like, how you would migrate users and purchases and all that. Though subscriptions is a pain. We haven't done that yet, because none of these other sites are, AKED's the only subscription site.

[25:10] So you end up managing those loops. NGEST makes that kind of stuff actually kind of simple in my head anyway. One of the things, TRPC is one of the things I'll miss, actually, and I don't know how Remix does that kind of stuff. Yeah, so it is possible, of course. I don't know anybody who's using TRPC in Remix.

[25:38] I don't know if... I don't think you need it at all. Yeah, I'm probably not going to need it, is one of those. Is the first blush reaction. I like it, but I like it for reasons that are probably more specific to the context in which I like it than really needing TRPC for any application I sit down to, is the way I'm thinking about it.

[26:03] Yeah, that's 100%. Well, all right. I'll just sit back and watch and learn. Oh, actually doing stuff? Yeah, let's do stuff.

[26:16] I want to get some data migrated or code written. And I want to watch where you bump into stuff and you're like, oh, this is super annoying or whatever. Or like if you have questions about stuff, I want to be here for that. I was like, oh man, I'm going to have to do some sort of complicated process to get all my data out, but actually that's not true. So while you're doing all that, the local environment is designed to be 100% offline capable.

[26:57] So every API that you hit locally is going to, well everything that is there right now is going to be mocked by MSW. So even if you try to, like you say, log in with GitHub, it will log in with a fake GitHub user that is called Cody. And yeah, so as soon as you start adding more APIs, it's going to give you a warning in the console that like, hey, this API that you called was not mocked, just so you know. If you want to add a mock to it and keep things working offline, then that's awesome because it makes it easier for people to contribute to and easier for you to work on and make changes and whatnot. But if you'd rather not, because it is extra work to do that, if you'd rather just hit the APIs directly, then I can show you how to really easily let those pass through without warning and stuff like that.

[27:57] Or you can just disable the warning altogether. Yeah. Yeah. Let me write, I'm gonna actually run the site. Where do you have it deployed by the way?

[28:20] Whatever the default is. So it's epic egghead? Yep, there it is. Sweet. I love that Simon updated the homepage.

[28:31] It looks so much better. I didn't have any comparison but I was like, wow, that looks nice. And I recognized it from the word. And that's all CSS, man. Yeah, it is.

[28:46] It's awesome. Yeah, so like the first steps for me It's like deleting a lot of stuff. Yeah, it's like going through and deleting the stuff. I managed to get the name changed. But it's like I don't, obviously don't need the notes and all that fun stuff.

[29:04] Yeah, so probably the best place to start for that would be the data model. Go and delete that from there and then regenerate the Prisma client and you'll get a bunch of TypeScript errors and you just go and delete stuff a lot. Oh yeah. That makes sense. What to do...

[29:37] Now I need most of the other stuff. Yeah, all that, the rest of that stuff looks like it could be useful. Yeah, I just use NPX, so NPX Prisma Migrate Dev. Unless, if you wanna, well no, you already deployed it, so you wanna have a migration for this, so. It'll be easier that way.

[30:17] What is it? Npx prisma migrate dev Like that? Yep. Yep. There you go.

[30:37] Sweet. Now if TypeScript runs across your code base, it's going to yell at you for all the places you do notes related stuff. So yeah, the next thing to get rid of, or preemptively get rid of a bunch of those, if you go to the routes, or app and then routes, then let's see, go to users plus, and then a username, and then everything that has, yeah, in fact, everything in there, yeah, delete everything in there Yeah, the whole directory to Yeah, just that one yeah And then there's probably some in resources. Yeah, I mean, you're going to find stuff all over the place for this. TypeScript.

[31:47] Yeah. Doing its job. From here on out, it's pretty much just like, delete the TypeScript errors, and then you'll have a lot of the stuff cleaned up. I'm going to rerun it I guess. Oh, nice.

[32:37] Yeah, that's just called, yeah, so you could rename the download to be my egghead data or whatever. That's a nice feature. Yeah, it's kind of cool. Easy to implement. Yeah, you're going to have a bunch of tests busted too, which is fine.

[33:10] Yeah, just delete that one, that note or line. Cool, Now if you go, you'll have some of that in the end-to-end tests as well. Yeah, yeah, Yeah, yeah, no image, you'll want to get rid of that. Actually, I think this file you can delete. This is...

[33:44] So... Yeah, the way that Epic Stack works is we don't have an image solution. When you upload an image for a note or your profile, it saves it as binary data in SQLite. And then it's served from SQLite on that route. You may want to change that.

[34:08] It works and it works pretty well, but I don't know how well that scales. Because it's a lot of data. Like Blob storage and SQLite. Yeah, basically. I'm a big fan of Cloudinary for all my images at this point.

[34:28] Even like user avatars and stuff? Or like user uploaded images? You want to have them have Cloudinary store those too. It makes it easy, I don't really, that's a good question. The other thing is just doing, I guess Cloudflare would do it, or there's lots of different solutions actually.

[34:51] Yeah, I mean you could just use Gravatar for end users and then Cloudinary for the instructors. So yeah, here you're looking at, what are we looking at? I need to find where the path is. So yeah, this is the user search page, and we order the users by how recently their last updated note was. So you could delete that order by, because you don't have notes anymore.

[35:30] So if you like SQL, there you go, you can write raw SQL. Oh yeah, you can. All day. Where is that? Oh, it must be gone.

[35:45] There you go, there's our back stuff. You're missing my castle. Your castle? Yeah, C-A-S-L. The World Based Access Control Library.

[36:02] Oh, oh. It uses The Mongo query syntax for you. Oh interesting mission. It's really nice actually. I love it.

[36:12] I'll look that up Real notes Real notes. That's interesting. Yeah, oh no, somebody sent this to me. Probably me. Yeah, maybe it was.

[36:35] Yeah, I haven't ever seen anybody except for me talk about it. It makes managing permissions super simple. So I should just be able to, I can build this I guess, right? Yeah, yeah, if you run the build and it passes, then probably good. All right, sweet.

[36:59] You may have like, because Remix doesn't have type safe routes yet, like links and stuff, so you could have links that point to a notes page somewhere, but you're probably pretty good. I was pretty aggressively removing just the word note. Yeah, you probably are good now. I don't know, the data modeling is probably what I'm about to come up on. Maybe it would be useful to run the app and take a look at the user profile page, or like when you're logged in, your app settings page.

[37:45] Settings page. So you get an idea of what's provided out of the box. The old cell phone button. Yeah, cell phone button? Yeah, where you're owning yourself.

[38:21] Oh, yeah. Yeah, that might be one to delete. People always want to do it. Yeah. We get, particularly Europeans, like deleting all their data from websites.

[38:34] Yeah. And I do, on this stuff, we do hard delete too. So no isDeleted flag. Oh, now I've just Oh, now I've just exposed my most used emojis. Oh, the process.

[39:02] I see the process right there. Yeah, the spiral gets used. I got that tattooed on my arm, actually. Oh, I didn't know that. He was like, what do you want?

[39:15] And I was like, I don't know, I'm into the blue spiral emoji right now. Like, all right. I can do. So all of this connected GitHub stuff. Is that fake, Cody?

[39:29] Yeah, it's all fake. Yeah, it's just local. That's cool. So you don't even have to have the OAuth set up for my local environment. Yeah, yep.

[39:39] I assume I can if I want it. You can, yeah. In fact, it's actually really easy. The environment variable during local development has a mock underscore prefix. And if the mock that we have with MSW, it'll check the environment variable.

[39:59] If it has that mock underscore prefix, then it'll mock. If it doesn't, then it will pass through to GitHub, and then you'll do the full GitHub Auth flow. So if you... So the mock client, these look like I can, I can share them and it's fine? Yeah.

[40:19] Yeah, so the GitHub, all three of those GitHub tokens, it's mock underscore, and if you change those to something real, then it'll take you to the whole flow. Nice. Easy. Easy. What else?

[40:42] What's the other top thing you're going to end up configuring when you start a new Epic step? So in production right now, oh yeah, you actually, when you went through the deployment step, I'm pretty sure it set most of these environment variables, but some of them it did not set because you need to sign up for the service. So we use Resend for email. If you're going to use Resend, you'll need to sign up and get a token. Otherwise, it will...

[41:14] Actually, I could probably do it right now. If you go to, try to log in, and if you do log in with GitHub, of course that's not going to work because you don't have that set up. If you try to create an account with an email address, then what it's going to do is it will, the app will act like it did send an email, but all it does is it logs to the console that an email was sent and it logs what the email was. So you could go to your monitoring fly dashboard and see what the code is and log in that way. So you can proceed and then once you're ready to really make this a thing that people can log into, you can update the resend API key to something real and set that on that environment.

[42:03] If you wanna see, the logic's really easy. Just the file I think is called email.server. It's under Utils, under app, app utils. And yeah, email. So yeah, just at the top of that send email function, inside the send email function.

[42:29] A little bit further, on line 42, I've got that comment. So that's the logic right there. If resend API key is not set, then we'll just console.log and pretend that it worked. Yeah, we do that in dev too because It's annoying to be sending emails all day when you're working on an app. Yeah, so if you're not going to use Resend, you just change some of the logic here.

[42:55] If you are, then you can just wait. And yeah, during development as well, that API is mocked out. So even if you do have that API key, during development, it's going to just log to the console that email. So if you try to log in now as a different user, you'll see the email contents logged to the console so you can easily open or log in as anybody. And we also can author our emails in React which is pretty nice too.

[43:31] Yeah, I have the... Epic Web actually uses React email also. Yeah, sweet. Yeah, I guess the next thing probably, honestly, the next thing is... I mean, you could get the marketing page, the homepage, so if you go to routes marketing, just delete a bunch of the contents of that, delete the logos thing, directory, and yeah, just delete a bunch of this stuff if you want to.

[44:02] Is that the index? Yeah, this is the index. But the most interesting thing next would be to update the data model probably. It's funny because it still kind of fits because we teach a lot of those stuff on AK. Yeah, that's true.

[44:26] Part of the reason that all of those marketing or those logos. Well, yeah, nevermind. In the next couple weeks, once Vite or Vite is supported by RemixLite gets stable, then I'll swap EpicStack out with Vite. It should be pretty simple. And your HMR is gonna go like sub 50 millisecond.

[45:13] It's just nuts. HMR right now is pretty good, but like Vita is outrageous. Yeah, that'll be cool. I think it I Like the ecosystem alignment also, I think it's pretty cool. Mm-hmm.

[45:26] Yeah, I'm pretty stoked on that. Please click they do Cool alright, so the next thing, yeah, probably the data model. Yeah, so I'm gonna go have lunch, I think. And I gotta doodle on that one a little bit. If you have any ideas though, I guess while we're here, what's the, like what, did any of my, so I actually, I'll pull this up, it's funny.

[45:59] You mean ideas on like what the data model should look like. Oh, cool. Here's my chat GPT. I just made it analyze my image. Brilliant idea.

[46:09] Yeah, that's awesome. And it'll read the image. So it doesn't do a great job. But it was like, it was doing, and this is almost like polymorphic, I guess, is the, but where, where a module belongs to, and I don't really, no, I don't really like it. And this is the, so it's pretty good actually, And the way I've been thinking about it is mainly the way we do.

[46:40] And even this, I get too specific, I think. This is sanity, and I know it's a lot, but you end up, they all have the same shape, where there's a title and whoever the creator is, Slug, obviously, the current state of publication, and then we just give it resources. You see body, markdown, markdown, images, And they all kind of end up with that same shape, even like an exercise. We vary a little bit for some of these, but they end up with resources and markdown and a description. So it's just like taking this content structure that I've got here and then, like a video is a thing and a video encompasses the original media, it's Muck's asset and then the transcript is actually part of a video because it's a one-to-one relationship.

[47:38] And it's a one-to-one immutable relationship between the video and its transcript. And it's original. And That's worked out really well. That's like a thing. Yeah, like...

[47:54] I don't have to worry about products and stuff. I definitely would recommend staying clear of polymorphism in favor of duplicating fields on different models. That's what we... Well, I mean, so the module is basically a module has types, right? And This is my debate right now.

[48:16] It's like a module has types and they look like this. Because it doesn't matter if it doesn't have enough deviation to matter and where the deviation between the types makes a difference is like, I keep stacking all these fields onto this that are specific for a certain type, right? Like, which is an indicator. But these in particular, you don't really need to add any to. Where it kind of gets weirder is the lessons and having like a lesson type.

[48:48] You know, because like One that is shaped like the exercise I mentioned, like your style where it's the three part is different than an egghead lesson, which is a single part. It's either a lesson with a type or the different types like this. As an individual model. An explainer is a lesson type, as a for instance, an exercise is a lesson type. So Which way is better in that sense, where the type field is sufficient or does it need its own table?

[49:25] Yeah, I think if there are other fields that are affected by the type, then that to me is like, this should be separate models. If the type is just like a classification, and all the other fields are the same, so like a module is just a collection of things, and it's just a way to classify different types of modules. The only difference between different types of modules is their type, right? Whereas I've just hit issues where I'll say it's this type, and because it's this type, it will also have this other set of properties. The way we handle that, currently, right?

[50:18] So say this is a lesson with a specific type. The type is a hint at what resources are contained. So the type is like, so you can filter basically. So I can filter on type and I know they will all have the same structure. The resources, you'd have a lesson type and then lesson resources where the type gives you a hint at the schema of the resources not the properties of the lesson container itself.

[50:51] For me, if you can define where resources is a thing that, resources are polymorphic, they're not really, I know that word has a specific meaning. Meaning I just have a bucket and if I do .resources then I can grab all those. In practice what we end up doing is using Grok with Sanity and using the query to reshape what we actually want on the client side. So we will query and then we will filter and rename the resources property on the top level. I don't know if that makes any sense.

[51:32] Yeah, that does make sense. That's interesting. It's pretty... Let's see, where is the... I bet the query is kind of gnarly.

[51:41] Nothing. Yeah, it is. It's like we want to reach in and instead of having to go through the video resource or call that reference, we go ahead and just build that up and then push those properties up because it contains it. And the queries end up being very specific and kind of rigid-y and structured because they're complex, but the backing data model ends up being really flexible. And this is like the, for me this is the complete value proposition for Sanity actually, is being able to use Crop to query and just take that content data and kind of shape it into whatever kind of clay you want it to be.

[52:22] But it's pretty much the same. Like we keep doing the same thing over and over again. And if I already know that, and I know all my queries in advance, in terms of how we want to load that data and the shape of it here, this contract. I don't think I need that flexibility and the data model could be more representative of what we actually need. Yeah, I'm kind of a split mind there.

[52:48] See, this is it, like resources, type video resource, and it just grabs the first one, so it's pretty naive, but because we know the shape of it, because this is an exercise explainer, interviewer lesson or whatever, We know the shape of it, so we have those guarantees and then we back that up. It's not being backed up here, but often we back that up with Zod just to validate the data here. Yeah, and you could do something similar even like once you... So the big problem with the polymorphic stuff is... This could just be SQL though at the end of the day, right?

[53:26] Yeah, most of it. Yeah, yeah. But, Yeah, part of the problem with polymorphic stuff that I've found is that you just have a bunch of fields that are optional. Because on this type, there are these fields, and then on this type, there are these other fields. And so now everything is optional, and there's no way to correlate and enforce at the database level that these fields are all together required if the type is this.

[53:53] And so that's where I say let's make separate models. But if that's not a big deal, what you could do is you do your query and then you have a Zod thing that parses that and even does a transform, which is basically what Grok is doing. You would just have a Zod transform that says, here's my data, And if it's this type, then I know it's going to have these properties. If it doesn't have those properties, something's up, and so we can just handle that edge case. So I'm of split mind on this, because I typically like to avoid situations where it's like, well, if it's this type of resource, it's a video resource, then it's going to have a transcript and that's going to have text.

[54:37] But if it's like an article, then it's not, or whatever the resource sort of thing is. So I, because if you do decide to split it up into multiple models, Then your lesson can have a collection of each of those different types of resources. So a lesson can have... What do lessons have? What types of resources can lessons have?

[55:19] Well, there's the fun bit, because it can be any. Oh, okay. So let's say... But then it could, and that's not a problem. They belong to a module, a module is a container, and then a lesson itself is a container for a set of resources.

[55:36] You've seen some of this discussion where the term lesson is overloaded in our particular workflows because of baggage from Egghead and the rest of it, where then these lessons have these types, including lesson. So that's weird. And that kind of thing, it's like these are literally all just lessons. And In the context of education, the leaf to me, like the end node, is always a lesson. That's what we're dealing with, is collections of...

[56:11] It's because we're learning, right? The goal is to learn something, and what might not be a lesson is something that might be strictly for entertainment. But all of these different types are literally lessons. And to me, it's like, it is an overloaded term and it really is just a, it's a module type. But because modules are collections, they can be collections of other modules, which is where sections and lessons comes in.

[56:37] So you end up with the module. And I get weird about it. And the team just kind of nods at me, and like the Helmer backing up into the bushes. Yes. Yes.

[56:49] Yes. Yes. Yes. OK, Joel. See you.

[56:52] Yeah, I mean, I've been thinking, I think about it a lot and probably, it's one of those things that I'm always, it's like the words matter and how do you explain this stuff. I get explainers so silly to me as a thing to say, but lecture is even worse. But to me, it really is. A lesson is a collection of resources and then all those resources have the types and in the perfect world my UI would be like, oh, well, here's all the resources I have. So I present to you those resources in the kind of like REST style, right?

[57:31] Yeah, just a giant list of resources. Where the data defines the structure of the page, not you necessarily putting in a hard type. But humans have, you know, like people are still working on this, so having named things helps people understand what it is they're supposed to be making. Yeah, so you don't want to just call it collection and lesson. Collection and resource, even.

[57:54] Yeah. And then people will be like, what's a resource? Well, a collection is a resource and a collection contains resources. They can also be collections themselves And then a resource can literally be anything. So then it's nothing.

[58:06] And that all comes from, I don't know if you've, so we hired LMM to do research into just the, this was years ago, I don't know if no one probably tells me in here somewhere. We've done a lot of IA research and She went through and created this and it's great. It's just a breakdown of what we have and the punchline of it being that it's this resources and collections. That's not a good one because of the... I don't even know how to get out of that.

[58:49] There we go. There's my diagram. That's sad. Yeah, but that was the end conclusion of the whole thing, looking at Egghead and all the stuff we had. It's like, well, you have collections and you have resources.

[59:09] But then being too vague doesn't help people in terms of getting work done. I have a lot of queries, though, and they all look a lot like this. So it's like if I have all these queries and I know how my data is supposed to be, I feel like going through and collecting all those. I could probably feed that into ChantGPT and say throw out the... Well I mean, why can't the model just be collections and resources?

[59:36] And then you'd have a resource type that is... So you've got a module, and that module needs to have a description of what this module is. And that description is a resource type. It's the type of resource it is. You're taking it to the next level.

[59:57] I think you have to normalize to some extent. And it's like everything has body text. We ended up with description and body and it's like that's silly but the body is the structure, the text structure and literally everything from every one up to module, the collections themselves have to have the text that goes with them, and the title, and the slug. And then that's where the type comes in. And that's actually been pretty nice from the module perspective, Just because one module, and we have a tips type and a talks type, and those are containers and don't have sections and that sort of thing.

[01:00:42] Being able to differentiate, and I have not done lessons that way yet. Well, no, because see that's the... Yeah, the exercise explainer lesson. The underscore type is the internal sanity where this would be lesson type to go with module type. So instead of that I've hard-coded those, but the way we use them ends up being, you know, I'm grabbing the same things.

[01:01:06] The title, the description, the slug, the ID, right? Like I want these things every time so I can link to them or whatever. And they end up being even similar in shape based on the resources. So you get the lesson level to me is where the collection and the shape of the resources comes in. I think there is a place for a lesson to be a type.

[01:01:30] I don't know. So is it just a module type at that point? Where you have sections a module type, lessons a module type, tutorials a module type, and then kind of all fit in together and you can have Yeah, so like then a tip could be a modulable. And it's a collection of one. Yeah, well, and that's the...

[01:02:05] Yeah, I mean it is, right? Because a tip is a lesson at the end of the day, as far as that goes. And they have, again, the body and then we do summaries and descriptions, which are just kind of poorly named because they're used for, you know, it's like the less than 240 characters kind of stuff. And then we elevate the transcripts because these are always a video. So this will always have a video resource as one of the resources in its collection.

[01:02:37] Yeah, so you just make it easier to consume. And as far as resources go, a video resource is so specific that making it, having... These are the easiest, right? Like these, I call these like atomic resources, where like a video resource or like a markdown file, right? Like that's a, that's an atomic resource where it's not going to split any further.

[01:03:04] It's technically not even true because it has the transcript. In terms of normalizing it, it wouldn't make sense to split those up. Even though it ends up having it still has a title and it has all those same features. These always to me have made sense. As a resource, a leaf node.

[01:03:30] Yeah, I agree with that. I'm just, I keep coming back to collections and resources, and I guess I'm not seeing why you need more than that from the data model perspective. Yeah, well, to me it would be modules and resources. So I don't mind the term module, though it's actually a reserved word. That's fine.

[01:04:03] Like, Oh, I'm going to run a class. Oh, are you? Yeah. So you got to name it something totally different. So that's like, that's the, the, and I can play with it.

[01:04:14] And that's, what's cool about this is I want to take the export and take it because this is what it looks like on Egghead. There's a lot going on. But I still break it down. And even though there is a lot of data here, and it's all kind of still actually very, very similar. Yeah, like some of that image stuff, for example, would that make sense for to be properties on the module?

[01:04:47] And I think yes. Does it make sense to be properties on certain types of resources? Also yes. So I can't see anything in there that a simple model of module and resource doesn't satisfy. Yeah, and particularly where the, you know, if you think about module, well, it is a resource, so they're gonna share that same, so they get title, body, created at, or whatever, like the same basic things, but they're separate.

[01:05:20] So I don't know, like the module probably makes them, even then, they're still the same. And you get down to the resource that does not have any additional, I think that's the deal, it doesn't have children. And then also keeping that shallow is relatively important too. What's your nesting limit? But the nesting limit can be naturally enforced by the UI.

[01:05:51] I don't think that you need to have a differentiator between a resource that can have child resources versus a resource that can't. But I would still say though that it's not, sorry, and I don't think I'm being clear, but I think having a thing that can have children resources or that individual resources should be individual modules. Because resources will necessarily have different properties. A video resource is going to have a transcript, but an article resource is not going to have a transcript. I do think you'll have a module model that is a thing that is a collection of resources.

[01:06:38] And then you'll have quite a few data models for the different types of resources. And so a module can have video resources and it can have the article resources and it can have whatever other types of resources. But I do think that- Well, what's funny is like, So if you take the properties here and we talk about body, so you end up like this, if when you talk about articles in particular, the article is just the body. But an article module could then also, in terms of its resources, have something like this. And it could have other stuff too, but like a video.

[01:07:30] And interestingly, one that's pretty common for us is the URL resource, which is super atomic also, because whether it's supplementary kind of stuff. But what else do you got? Body, time, slug maybe. I'm not sure I understand how you're representing this. So module is the model and then the things below it are the properties?

[01:08:01] Yeah. I'll change their color. The properties. Yeah. And so video and URL are types of resources.

[01:08:23] Got it. And those can. Even things like a tag is technically a resource. Yeah, and I see those as being, if we're talking about Prisma and databases, those are tables, right? Yeah, for sure.

[01:08:47] Yeah, okay. I Think they make sense that way. Yeah, and and like a URL can Well, it just it has an alt and and they end up, you know, like being resources So it would probably have a title and stuff to you or could Mm-hmm So basically it's like these these properties, right? Like these are common. I don't know, like these are special, so they don't necessarily need to conform to this stuff.

[01:09:17] They live with whatever they have and then everything else is basically. And you've got like audio as well for like audio podcasts. But the point is that you can just make any number of these resources and they can all go on that collection. So you'll have collection.videos, collection.urls, collection.tags, collection.audios. Collection.urls, collection.tags, collection.audios.

[01:09:56] Yeah, that's where it's like, I mean most of the time it's, you can also do this too I guess. You want a collection.resources and that just has all of the, regardless of their type, all of the resources? Yeah, that's what we typically do. And it's sorted. Yeah, and you can, you know, and filterable.

[01:10:20] So you're grabbing them. I think that's the, maybe from Prisma, I don't, I haven't Yeah, you can't represent that in Prisma. Because I don't like, like I don't want to have .videos, .urls, .tags, like that stack of, it kind of defeats the purpose. I don't know if that's where the polymorphic relationship. That is where the polymorphic stuff comes in.

[01:10:47] Like This is precisely, this is the gateway into polymorphism as you look at that stack of stuff and you're like, ah, I'm going to go this way instead. And then I've just not been happy with that. And so what I would say is it's okay to have the stack. That's not a problem. But to handle the order of stuff, you'd have another property.

[01:11:13] That's just the order. Part of my point here maybe is that if a course would have lessons and I don't know What drives me batty is the rigid structure and lack of imagination that having a database schema that is very set in stone will give to your end products. Sometimes it's great, right? Like we know exactly what this should be and how this should be structured, but like I've suffered mightily for years with Egghead like breaking, because it's hard-coded, right? Like what the shape of this thing is hard-coded, so if it has dot videos or a video or whatever, like you end up with a situation where you just kind of stamp that out because that's what it is and evolving it is challenging versus the polymorphic approach where it's like the bucket of things.

[01:12:17] Yeah, it ends up being like the sanity stuff and you get to query it. Yeah, the problem with reducing structure is that you have to do what you're doing here, which is not bad necessarily, but. It's doing it in query instead of table level and there's advantages to that. But then you have to create a schema that you parse because you have no guarantees about the properties that are there. And so the nice thing about having, like you already have a schema, it's the Prisma schema.

[01:12:51] But if you go with something that's really loosey goosey, then you have to do application code schema too. Which is not necessarily bad, it's just something to like. That's where Zod comes in, right? Yeah, exactly. Because we have to do that here if we want type safety.

[01:13:07] And you don't necessarily need type safety if you're not into that sort of thing, but we are in this house. Yeah, we are. So. Yeah. Yeah, so.

[01:13:15] And you just end up, it ends up like this. And at the end of the day, if it's polymorphic, it's literally what we're doing today. I would just be querying with SQL instead of Grok, which is fine. It would take me however long to cry through my way to get here, but I already know, I know where I want it, so figuring that part out, like How do I query this from a SQL database is a very tractable problem. Yeah, sure.

[01:13:55] The problem is that you get runtime type safety, but not static type safety. And so if you're like, oh snap, this particular resource, like the video resource type is video, we need to rename this property or we need to add this thing, or we need to make sure this is required, or whatever, you then have to go through and find all of your SQL manually, which, I mean, we did this. This is where Drizzle actually shines, to be honest. Yeah, I guess that's true, yeah, that's fair. Although, I don't know that you can represent this very well in Drizzle either.

[01:14:38] Just because you have to say, well, You've got a table and it's got columns, and different properties are going to be defined for different types of resources, and other columns are gonna just be empty for those types of resources. And so you need to do null checks all over the place. You do it in the one place where you're, no, you can't. It ends up infecting the entire code base, you're doing these null checks, and you have to make sure that those properties get filled in in your application code. Because like for a video, the transcript, let's just say that the transcript is required on a video.

[01:15:26] And so you wanna upload a video, if you go with the polymorphic approach, you have to enforce that requirement within the application code. And yeah, with this runtime schema. But if you're using... It's like there's null everywhere. So it's like, once you get a little bit, you're...

[01:15:46] You're knee deep in it. Yeah, exactly. Which is like, I mean, clearly it works. But it bothers me enough that I'm very hesitant to go with a polymorphic approach. Whereas what you're talking about with, if we split them up into separate models, that can be really annoying, because now you've got this giant stack of all these different properties on this object.

[01:16:13] I can appreciate that being annoying, but I don't think that it's as constraining as you suggest, just because you can always add another type of resource. Like you say, oh, this is not exactly that. It's not, it needs a couple of different properties. You just make another model for it and then away you go. And migrations are way, way easier as well.

[01:16:39] Prisma will help you do all those migrations. So that's my perspective on this stuff. Here's my experience with what you described actually. So many migrations. 2013.

[01:17:00] Oh my gosh. Anyway, I'm familiar. This is Morpheus and like welcome to the real world. It slowed down a lot. It was pretty aggressive.

[01:17:15] And then there's a lot of ideas. But what's interesting also is looking at that and then thinking about, hey, what do I need today in terms of building that out structurally so you're not in the exploration space so much as here's what I want to build and I know how to do it. So I think that structure and that kind of rigidity of the schema approach versus where it's hard-coded in a schema versus polymorphic can be actually good because it's putting constraints around what you're doing and what it means to, but you know, it's like adding tables and doing migrations really isn't the end of the world. Yeah, like making changes to existing models is really easy when you've got a tool that'll help you do that. Whereas if you've got...

[01:18:08] This conversation almost sounds like deciding between NoSQL versus SQL, where you've got a document store. It's just like, you can put anything in here. Well, I mean, that sounds nice at the start, but you get into it and it's just too free. Well, and Yeah, you lose the very handy help of your tooling in a lot of cases. Yeah, and that's why you have to do nullable and why you have to do Zod in the first place.

[01:18:44] So I think that going with the non-polymorphic route and individual models for each one, you'll probably still use Zod. Zod comes installed already for parsing forms and stuff. And it can be useful for the raw SQL queries like you saw on the search page, for sure. But if you can just lean on the Prisma type safety or if you switch to drizzle that type safety rather than taking the loosey-goosey that's in the database and slurping it into some runtime type safety, then I think that that's better. The primary loss with Prisma would definitely be the schema, right, and the migration.

[01:19:28] Yeah, that's the biggest thing. I've not used Drizzle. I'm not scared of SQL, especially with chat GPT and copilot and stuff. Like it's easy now, but yeah, those migrations are great. And honestly, I like, so performance, like some people will say, well, Prisma doesn't do joins.

[01:19:53] We're in SQLite. We don't care about like this, that particular thing. You can execute. You could, right? Like you can just run, you could run a SQL join all day if you wanted to, right?

[01:20:06] Yeah, if you like, I have to do a join here, it's for whatever, you just do raw query, it's fine. But then you have to do Zod to make it type safe. That's what Drizzle will say. Well, you have to use something else to make it type safe. Sure, okay.

[01:20:21] But. I like the lack of generation and just the super sequel. It's why Drizzle is appealing at all. I don't, Prisma's great though. In practice, like for day-to-day working with Prisma, outside of like mono-repo issues.

[01:20:37] Yeah, well, and also like keep in mind, you're not the only one working in the code base, and I don't know what the future holds for employees at Egghead, but it does seem like people have an easier time using Prisma, at least to me. Yeah, it's like not throwing RxJS at them or something and being like, yeah, you're learning this now, this is what we're doing. Yeah. Good luck scaling up. Like Drizzle is pretty one-to-one with SQL.

[01:21:05] So it's definitely a transferable skill that's good for people to know. But I don't know, I think Prisma is friendlier. Yeah, I mean, that's the benefit of the generated client. Generation bothers me because of my expertise. You know what I mean?

[01:21:27] I would just assume not have something generate. This is nice and what they generate is actually super handy but there's just a lot of times where it's like, do I need that? And then there's always headaches to me whenever I'm doing mass dynamic code generation and just what they need to do to generate the client and then the way it stores an NPM, even though you can't eject it, it gets weird. It's like, that stuff is what bothers me. Yeah, yeah.

[01:21:54] Not really, just using Prisma is fine too. I had to do some funny things to make it work well in my workshops too. Not terribly bad, but yeah, some funny stuff. Yeah, just kinda getting around it because of just the nature of how they have to build the tool, so it makes sense. Cool, well, maybe I'll sit down and get some data in here and do an initial thing.

[01:22:24] We can circle back and chat again about it. Yeah, that sounds cool. I'm super stoked that you're going to give it a shot and love to hear your feedback. Yeah, I'll give it. Go Mac.

[01:22:36] Cheers.