[{"data":1,"prerenderedAt":439},["ShallowReactive",2],{"footer-primary":3,"footer-secondary":93,"footer-description":119,"100-apps-100-hours-saas-app":121,"100-apps-100-hours-saas-app-next":171,"sales-reps":187},{"items":4},[5,29,49,69],{"id":6,"title":7,"url":8,"page":8,"children":9},"522e608a-77b0-4333-820d-d4f44be2ade1","Solutions",null,[10,15,20,25],{"id":11,"title":12,"url":8,"page":13},"fcafe85a-a798-4710-9e7a-776fe413aae5","Headless CMS",{"permalink":14},"/solutions/headless-cms",{"id":16,"title":17,"url":8,"page":18},"79972923-93cf-4777-9e32-5c9b0315fc10","Backend-as-a-Service",{"permalink":19},"/solutions/backend-as-a-service",{"id":21,"title":22,"url":8,"page":23},"0fa8d0c1-7b64-4f6f-939d-d7fdb99fc407","Product Information",{"permalink":24},"/solutions/product-information-management",{"id":26,"title":27,"url":28,"page":8},"63946d54-6052-4780-8ff4-91f5a9931dcc","100+ Things to Build","https://directus.io/blog/100-tools-apps-and-platforms-you-can-build-with-directus",{"id":30,"title":31,"url":8,"page":8,"children":32},"8ab4f9b1-f3e2-44d6-919b-011d91fe072f","Resources",[33,37,41,45],{"id":34,"title":35,"url":36,"page":8},"f951fb84-8777-4b84-9e91-996fe9d25483","Documentation","https://docs.directus.io",{"id":38,"title":39,"url":40,"page":8},"366febc7-a538-4c08-a326-e6204957f1e3","Guides","https://docs.directus.io/guides/",{"id":42,"title":43,"url":44,"page":8},"aeb9128e-1c5f-417f-863c-2449416433cd","Community","https://directus.chat",{"id":46,"title":47,"url":48,"page":8},"da1c2ed8-0a77-49b0-a903-49c56cb07de5","Release Notes","https://github.com/directus/directus/releases",{"id":50,"title":51,"url":8,"page":8,"children":52},"d61fae8c-7502-494a-822f-19ecff3d0256","Support",[53,57,61,65],{"id":54,"title":55,"url":56,"page":8},"8c43c781-7ebd-475f-a931-747e293c0a88","Issue Tracker","https://github.com/directus/directus/issues",{"id":58,"title":59,"url":60,"page":8},"d77bb78e-cf7b-4e01-932a-514414ba49d3","Feature Requests","https://github.com/directus/directus/discussions?discussions_q=is:open+sort:top",{"id":62,"title":63,"url":64,"page":8},"4346be2b-2c53-476e-b53b-becacec626a6","Community Chat","https://discord.com/channels/725371605378924594/741317677397704757",{"id":66,"title":67,"url":68,"page":8},"26c115d2-49f7-4edc-935e-d37d427fb89d","Cloud Dashboard","https://directus.cloud",{"id":70,"title":71,"url":8,"page":8,"children":72},"49141403-4f20-44ac-8453-25ace1265812","Organization",[73,78,84,88],{"id":74,"title":75,"url":76,"page":77},"1f36ea92-8a5e-47c8-914c-9822a8b9538a","About","/about",{"permalink":76},{"id":79,"title":80,"url":81,"page":82},"b84bf525-5471-4b14-a93c-225f6c386005","Careers","#",{"permalink":83},"/careers",{"id":85,"title":86,"url":87,"page":8},"86aabc3a-433d-434b-9efa-ad1d34be0a34","Brand Assets","https://drive.google.com/drive/folders/1lBOTba4RaA5ikqOn8Ewo4RYzD0XcymG9?usp=sharing",{"id":89,"title":90,"url":8,"page":91},"8d2fa1e3-198e-4405-81e1-2ceb858bc237","Contact",{"permalink":92},"/contact",{"items":94},[95,101,107,113],{"id":96,"title":97,"url":8,"page":98,"children":100},"8a1b7bfa-429d-4ffc-a650-2a5fdcf356da","Cloud Policies",{"permalink":99},"/cloud-policies",[],{"id":102,"title":103,"url":81,"page":104,"children":106},"bea848ef-828f-4306-8017-6b00ec5d4a0c","License",{"permalink":105},"/bsl",[],{"id":108,"title":109,"url":81,"page":110,"children":112},"4e914f47-4bee-42b7-b445-3119ee4196ef","Terms",{"permalink":111},"/terms",[],{"id":114,"title":115,"url":81,"page":116,"children":118},"ea69eda6-d317-4981-8421-fcabb1826bfd","Privacy",{"permalink":117},"/privacy",[],{"description":120},"\u003Cp>A composable backend to build your Headless CMS, BaaS, and more.&nbsp;\u003C/p>",{"id":122,"slug":123,"vimeo_id":124,"description":125,"tile":126,"length":127,"resources":8,"people":128,"episode_number":132,"published":133,"title":134,"video_transcript_html":135,"video_transcript_text":136,"content":8,"status":137,"episode_people":138,"recommendations":152,"season":153,"seo":8},"1109be0d-8ab5-479b-a052-8ad30d9ffb1c","saas-app","894439990","Building multi-user is a common convention when building Software as a Service. Bryant has an hour to build a todo app which manages users, teams, and ensures data can only be accessed within a team.","2c1fb560-c144-43ac-9e87-00e712be818e",64,[129],{"name":130,"url":131},"Bryant Gillespie","https://directus.io/team/bryant-gillespie",4,"2024-01-01","Mission: SaaS App","\u003Cp>Speaker 0: Welcome back to the next episode of 100 apps, 100 hours where we rebuild or recreate some of your favorite apps in 60 minutes or less or publicly fail trying. Sounds really scary when we say it that way. I am your host, Brian Gillespie, developer advocate at Directus, and today, we're gonna be building a multi tenant SaaS application. I realize that is not everybody's most favorite app, but it is the architecture that powers a lot of your favorite apps, and it's a question that comes up a lot within our community. So by multi tenant, what do we actually mean when we say that?\u003C/p>\n\u003Cp>It means that we have multiple different accounts or different tenants in this case, so we serve multiple different groups of users in one single application. And all of their data is separated. It's, you know, team a doesn't see the data or can't interact with the data for team b. It is totally doable on our back end as we're gonna see, and it's a popular convention for a lot of the SaaS apps that you will use so that we could share resources like a database, you know, an actual front end application without having to, you know, commission a brand new database for every single account that we have. Alright.\u003C/p>\n\u003Cp>Sounds great, sounds interesting, something that we want to build. How do we build it? What are the rules here? If you're new to the series, there are only 2 rules. We have 60 minutes to plan and build an application, no more, no less.\u003C/p>\n\u003Cp>And I do like to include a little planning ahead of time so we know, or before we actually build, not ahead of time. We're starting fresh here. You've got as much of a clean slate as I do. And then the second rule is use whatever you have at your disposal, whether that's Tailwind, online resources, AI, anything is fair game. We've got to get as far as we can in 60 minutes or less or look like an idiot.\u003C/p>\n\u003Cp>So sounds great. Everybody knows the rules. We're gonna be building the multi tenant SaaS app. Let's open this up and fire off the countdown. Here we go.\u003C/p>\n\u003Cp>So before we actually touch any code or any data models or, you know, anything in our back end, let's go through and and plan this out a little bit. And usually I like to start with our functionality. So let's shrink this down a little bit. And what functionality are we looking for out of this application? We want to create and manage tenants.\u003C/p>\n\u003Cp>In this case, let's call it, Teams is a pretty good convention. You know, accounts would be one that I see often organizations and in some applications. We wanna create and manage users within those teams. We wanna make sure that all data, make sure all data is scoped to each team. Right?\u003C/p>\n\u003Cp>So team a can access team b's data and vice versa to the team. And then, you know, a SaaS app needs to do something, so let's do just like the standard to dos. So we wanna create and manage to dos. Perfect. Alright.\u003C/p>\n\u003Cp>So there's our functionality. Nothing particularly fancy but it is tricky to set up some of this, especially around permissions. You know, that is a a big piece of the puzzle. Security is always very important. So, you know, how how do we set this up and how do we do it within an hour?\u003C/p>\n\u003Cp>Let's go through and now let's let's work on our data model a little bit. So we got our data model. And what are we gonna need as far as our tables or our different collections that we're gonna be using? So we will have, what, teams. Let's shrink this up a little bit.\u003C/p>\n\u003Cp>We will have users, we'll have to do's, and you know, we may not get to this but if I was gonna actually have this be built as a SaaS that I could charge people for, we'd probably have something like subscriptions. And we didn't include that in the functionality because I'm not sure exactly how far we can get in an hour, but we'll get to as much as possible. Right? So in this scenario, a user could be part of one team or, technically I guess we could build it so that a user can be in multiple teams. Multiple teams.\u003C/p>\n\u003Cp>Let's break that in half so we can actually see that. To do's belong to a team. To do's belong to a team assigned to a user. And then there's probably a a junction table here that is what? Users and teams.\u003C/p>\n\u003Cp>Oh, let's use something that's not connected. Right? So this is probably a table called something like teams users because it is a many to many relationship and just to get fancy and finish out our little arrows could look something like this. I'm definitely not an artist but you get the picture. Alright.\u003C/p>\n\u003Cp>So we've got teams, we've got users, we have to do's, we have team underscore users or or some of junction collection. We potentially have, subscriptions. So what do we have set up to actually build this? I'm using Directus for the back end of the application. I've got a super simple Nuxt application for the front end that we may or may not get to that and I've just got a little boiler plate that, uses the Directus SDK for that information.\u003C/p>\n\u003Cp>Let's jump right in. So if I pull up my Docker Compose and that's how I've got Directus set up locally, just through a simple Docker Compose file that you pull down from the documentation. And I'm gonna get logged in to our first user, and I'll just copy and paste this password here because I suck at typing a lot of times. Alright. So you can see that we've got a blank application.\u003C/p>\n\u003Cp>There is no data model. We've got a single admin user. We're starting from a clean slate. So if I just pull this up and we rearrange all these windows, maybe let's pull this over to the side so we can actually see what we're building, And I'll zoom in a little bit so you've got a better view. Maybe we don't put the clock in our face, so we're so we're not beating up against the clock.\u003C/p>\n\u003Cp>But how do we start? You know, let's start by creating this tenant collection. I'm gonna call this teams. You could easily call it tenants. You probably want to whatever makes the most sense in the context of your application is probably what I would use.\u003C/p>\n\u003Cp>Right? So the teams here, we're gonna generate a primary key called ID. We're just gonna use a UUID for that. As far as the status of that, yeah, we probably want a status for the team. We don't need a sort field.\u003C/p>\n\u003Cp>And then these others are just system fields that you can create, to give you a little shortcut. You know, when was it created? What was the user that created it? This specific team, blah blah blah. Alright.\u003C/p>\n\u003Cp>So as far as the team, what are we gonna need for the team information? We'll probably need a name for that team. That's great. Maybe we want to have a slug for the team. Could be a a good one to have.\u003C/p>\n\u003Cp>That way, we're not using a weird UUID, but instead a slug for the team. And let's go in and, adjust this a little bit. We're going to look for the interface section and Directus has the slugify option to make sure that slug input is URL safe. And one of the other things I'm gonna do here is just open up my little mouse pose tool. If you guys are ever wondering how I do this little highlight on the screen, that is a tool called mouse pose, very handy piece of kit.\u003C/p>\n\u003Cp>Alright so we've got our teams, now let's add our users, right? I could create a separate user collection but when I fire up Directus, it gives me a system collection for users that I can use to actually I could just reuse that for all of the authentication, all of the roles and permissions because what happens behind the scenes with Directus as my back end is it will mirror all these tables or these collections that we're creating inside the app to MySQL database, which is super handy. I I keep MySQL pure and, MySQL databases remains pure. And I've got built in REST and GraphQL APIs that are also automatically generated based on this data model. So tremendously powerful, enables you to build really quickly.\u003C/p>\n\u003Cp>So we're just gonna use that that system collection. I'm gonna I am gonna go in and create a new one. Let's just call it team users, and we'll generate a UID for that. We'll leave that one blank for now. Let's go in and add our to do's.\u003C/p>\n\u003Cp>So each to do we'll need, who's the date created, who was the user created it. We wanna give those a status as well, maybe a sort, so we can rank those in order of priority. So we've got the bare bones of our data model here, and we've got the again, we're gonna use the directus users system collection. How do we go and actually start setting up the tenancy? Right?\u003C/p>\n\u003Cp>Because right now, if anyone were to log in to this application, they would see all the data that we have available. So first, we are going to go in and create our relationships between these different collections, or which are just equivalent to tables within your Postgres database, let's first go through and do the teams and users. So a user can be in multiple teams. Let's go in and model that relationship. So we'll hit create field.\u003C/p>\n\u003Cp>We're gonna look for the many to many relationship inside Directus here, and let's just call this our users. Alright so the related collection is going to be users as well, actually it's not. It's gonna be directus_users, and that'll turn purple letting me know that that is an actual collection. And then we probably wanna show a link to the item. But instead of hitting save here, I'm going to hit continue in advanced field mode or advanced mode because I want to control what my Junction collection is named.\u003C/p>\n\u003Cp>So I'll just go in, we've already got this, it's called team underscore users, And I could go in and edit the, the foreign key fields here as well. It's just gonna be teams underscore ID, directus underscore users underscore ID. That's fine. Not a big deal. And if I wanted to, I can also add the reverse or the inverse relationship.\u003C/p>\n\u003Cp>Somebody will correct me on the comments. But, I can add that back to the Directus users collection so that I can reference that in any of our permissions that we're going to use later. We probably don't necessarily need a sort field, not a big deal. And the rest of this is pretty standard. So let's go ahead and save that.\u003C/p>\n\u003Cp>And now we have our users that are linked to, that direct us users table. So if I were to go in and create a team, I can see that I can add existing users or create new users for that team. Great. Alright. So next, let's take a look at teams and to dos.\u003C/p>\n\u003Cp>Right? So the to dos belong to a single team and I can assign those to dos to a user. So if we flesh out this data model a little bit more, we probably got a title or a name for this to do, let's use that. We probably have a description, so for that we'll just use our WYSIWYG editor inside the back end here. And I can even control what options we have as far as HTML content, but we'll just keep this the standard one for now.\u003C/p>\n\u003Cp>Alright. And then we've probably got a user that this has been assigned to. So we're gonna scroll down to our relational section. And depending on how we wanted to assign these, if we wanted to assign this task to or this to do to many users, you know, I would use either like a many to 1 or a one to many. In this case I just want to assign it to a single user.\u003C/p>\n\u003Cp>You know, whoever we assign this to is ultimately responsible for getting this thing done, and we're just gonna call this assign to. So we're going to use the many to one relationship within Directus, call it the assign to, and our related collection is going to be Directus users. So we'll hit save, that's who we assign this to. And now, let's go in and make sure this is applied to a team. So we need that relationship to a team so we can actually filter that out.\u003C/p>\n\u003Cp>Right? If team a is logged in or a user on team a is logged in, we don't want them to be able to see or interact with any of the to dos for team b, unless they are part of that team, which makes it a little more complicated to model as well. But we'll just call this, team or, you know, it could be team ID. Whatever. I'm gonna I'm gonna stick with team, and then the related collection is teams.\u003C/p>\n\u003Cp>Straightforward enough. Right? So if I were to go in now and we create a to do actually, let's just first create our team. Right? I've been talking about team a, there's the slug for team a, and team b.\u003C/p>\n\u003Cp>Cool. So now we have 2 teams, And I can go in and if I were creating a to do item, let's just say test item for team a. Here's a short description. I could assign this to a user which we'll come back and fix, but then I pick a team as well. Right?\u003C/p>\n\u003Cp>Now if I were a member of team a or team b, I wouldn't want to pick this, so we'll come back to that in a moment. But, basically, I can have a team that owns each one of these to dos. Great. So that's all set up. Right?\u003C/p>\n\u003Cp>How do we go in and manage those relationships? Because, you know, if we were to go in and create a user, let's just call it test user a for we'll we'll call it team a. Right? Team a user atexample.com. We're gonna do a real secure password here of password.\u003C/p>\n\u003Cp>And let's give them oh, the only role that we have now is administrator, so we'll sort that in a moment. But if we were to open this up and we go to team a user, so I'm in a incognito window. We'll go to password. Great. Okay.\u003C/p>\n\u003Cp>So and now you can see I could see all the teams, I can edit all the to dos. Right? So how do we solve for this problem? That is going to be using our user roles or the access control within Directus. So if we go to our access control, let's create a new role and call it user in this case.\u003C/p>\n\u003Cp>And they are going to have app access, you know, whether we enable app access or not. If I was building our front end and I I'm not sure if we're gonna have enough time to actually build the front end for this, but, you know, I could disable app access as I was using the APIs to to access this information. But we'll just call this user. And then we can go in and restrict our roles. Right?\u003C/p>\n\u003Cp>So now if I were to go in and, again, this is an incognito window on my left and the regular window on my right, so these have 2 different user sessions. But if I were to go in now to my team a user and change that from user or from administrator to user, you're gonna see that this person or this user, team a, does not see any content whatsoever and that is because we have not enabled any permissions for the user role. So we can go in and now let's start enabling some of these permissions. Right? So as far as Teams, we have create, read, update, delete, and the optional share, operation.\u003C/p>\n\u003Cp>But how do we how do we give them access to just the teams they're a part of? Right? Well, first, I need a relationship between our users and the teams, and and that's a piece that we forgot. Or did we? No.\u003C/p>\n\u003Cp>We've already fleshed that out. We've got our users there. Cool. So now I've got that relationship created, we can account for it inside our teams. Alright.\u003C/p>\n\u003Cp>So inside of permissions, let's go and we'll use if we use read access and we enable all access, what will we see? Right? Oh, that's the public permission. I guess I need to go into the user permission. Always love building against the clock.\u003C/p>\n\u003Cp>Right? You overlook some things. So here we can see all the teams now. That's not great because this user is a member of team a or at least should be. Let's make sure that we've got that set up.\u003C/p>\n\u003Cp>Right now they're a part of no team, so they can see all the teams, which is not helpful either. But let's make them a part of team a, and yet even though they're a part of team a, they can still see team a. So inside the that permissions, what we'll do, if if I actually go to the right one, we'll use the custom permission and we'll configure a rule that restricts that access. So the team, we can look and make sure that users, and I could drill into this, users dot directus user ID is one of, and we'll use a little shorthand here, current_user.id. So this is a dynamic variable within Directus that allows us to pick up the current user, so who is actually logged in, and we'll use the ID field.\u003C/p>\n\u003Cp>So this will allow us or should allow us to refresh and then we'll see that now this user can only see items or teams that are they're a part of, which is great. Right? They can't edit any of this information right now, but we could go in and restrict that as well. But just by doing this where we say our users and then we reference the user ID, which is gonna be an array in this case because we could have multiple users for one team, is one of dollar sign current underscore user dot ID. That allows us to build that permissions in.\u003C/p>\n\u003Cp>And I could even go through and restrict the specific fields that they could read about this as well if I didn't want them to have access to the slug or the other users, whatever. So now if we go into something like to dos, you know, we can enable all access for to dos, right, and we'll get the same effect where this one is assigned to a different team, they can still see that. So I think right now, this one is assigned to team a, I believe. And I could fix that as well where I go in, I go to my to dos and and instead of showing the actual ID, we wanna show the name of the team that's assigned to you. So we'll just on our display templates, we'll adjust that and go back, and we could see that, okay, this is assigned to team a.\u003C/p>\n\u003Cp>Let's create a new to do to do for team b. And again, I'm logged in as the administrator account over here. This is the individual user on the left. We'll save that. And if I were to go back to to dos now, they could still see to do for team b.\u003C/p>\n\u003Cp>Right? So how do we fix that? We go back into our access control and instead of having all access, again we use custom access. So we'll set up a rule where the team, and I could drill down and get the team dot ID, is again, we'll use is one of current user dot Teams? I can't remember what we named this, if it's team or team.\u003C/p>\n\u003Cp>Let's try team dot ID and see. Okay. So that breaks it to where they are not able to access any of it. Let's go back to our data model. We're gonna look at system collections, and we've got teams.\u003C/p>\n\u003Cp>Teams is the name of that. So if we go back, teams teams dot ID. Let's see if that sorts it out. It does not. Let's just check the syntax again.\u003C/p>\n\u003Cp>Is one of teams. Blah blah blah. Why is that not working? Team. Team team is okay.\u003C/p>\n\u003Cp>So we're hitting a snag here somewhere. Team dot ID. Let's try that. Team dotid is equal to current user dot teams is one of equals unexpected server error. So it has to be this one, Current user dot teams, or maybe it's just team.\u003C/p>\n\u003Cp>Alright. Let's take a look at our user directory. We've got our team. Teams hasn't been properly configured. Maybe something in the data model?\u003C/p>\n\u003Cp>Let's take a look. Teams is a many to many relationship. We've got team underscore users, team's ID. Oh, okay. Maybe that could be it.\u003C/p>\n\u003Cp>Team we've got to access through the junction collection, I believe. So we need to go back and, duh, we'll do current user dot team_users.directus. Nope. Team is teams_id. Team dotid.\u003C/p>\n\u003Cp>Let's try this. Team dot id is one of current user dot teams users or actually, maybe it is that teams dot ID. Still struggling. One of the the fine points of being against the clock. Right?\u003C/p>\n\u003Cp>We got 37 minutes, I'm going to cheat and not cheat, remember there are no rules, and we're just gonna reference one of the other, applications that I've built where I've referenced this before for a client role. So let's go in and look at this user dot contacts dot organizations dot ID, dot organizations dot underscore ID. So that is the collection there. That would be the organization's ID. Let's just take a look at the data model for this really quickly.\u003C/p>\n\u003Cp>Users. Okay. Trying to figure this out on a hurry. Users dot team users. No.\u003C/p>\n\u003Cp>Still really struggling here. Alright. So if we back this up, team, let's take a look at this one again one time. I just love building against the clock. Right?\u003C/p>\n\u003Cp>Let's let's just go back and evaluate what I've done. My brain is not thinking well today, I guess, so I haven't had enough coffee. So we'll go into our teams. We've got the users, so that's gonna be our junction collection. And then within the users collection or our junction table, it's gonna be we're identifying by the team's underscore ID.\u003C/p>\n\u003Cp>So thinking of it through that lens, our permission here instead of this whatever I've got here, it should just be something like this where it is the team underscore ID. So the team that's assigned to this to do, we get the ID from that team, and then we use the current user, and then we use the teams, which will be an array of objects, and then we get the teams underscore ID from that junction collection. So if I hit refresh, boom. Okay. Now we're cooking with gas.\u003C/p>\n\u003Cp>Let's just close this sidebar and continue building, right? So now I'm seeing only the to dos that have been assigned or or available to that specific team. Right? And if I want to see other users within that team, so within that junction collection, we're probably gonna need to do something similar here. So we can see users that are, where the teams dot id equals current user dot, oh, current underscore user dot teams dot teams underscore ID.\u003C/p>\n\u003Cp>So again, we're just referencing that relationship that we built and we'll probably do the same with our direct us users. Right? We don't want them to be able to see all the users. Where are you? Now I can't see any user.\u003C/p>\n\u003Cp>What's going on there? Test user for team a. Did we totally mess something up? Okay. So I could see these.\u003C/p>\n\u003Cp>We've got Let's go in and adjust the permissions for this. So we shouldn't be able to see, actually, that's gonna be on the the users collection anyway. Right? So users, we want the field permissions, the teams dot teams ID is one of current_user.teams._teams or dot teams underscore ID. Right.\u003C/p>\n\u003Cp>Great. So now we can only see users that are a part of the team that they're assigned to. Cool. Alright. So how do we actually go about creating to dos, right?\u003C/p>\n\u003Cp>This is not allowed inside this account. If we give all access, they should be able to create to dos now. But say we only we want to, restrict this team, right, or we wanna default this particular, team name, right, or team ID. What we could do is something like this where inside our to dos, we have field presets. So I could go in and say team is what, current, So we'll do this, like current underscore user dot teams dot ID.\u003C/p>\n\u003Cp>No. Dotteams_id. And maybe we get the first one, and that's how we assign. Let's see what this does just for testing. Alright.\u003C/p>\n\u003Cp>So we create a new to do. I should have access to this. Why is it saying we don't have permission access? Don't have permission to access this. Let's just try creating a test.\u003C/p>\n\u003Cp>Test to do. Hit save. Unexpected error occurred. Probably something to do with the preset that we've got saved here. Oh, I forgot the brackets.\u003C/p>\n\u003Cp>Could be it. Yeah. Still showing we do not have permission to access that. Great. So now we can go in and create this test to do.\u003C/p>\n\u003Cp>Alright. So we'll pick the team a, we'll assign that to team user a. Great. So now I can see those to dos. If we load up all the to dos in our master account or our administrator account, we could see that, I could see those to dos that are assigned for team b, but I cannot see those for team a because this user is not part of team a, right, and they cannot update that themselves.\u003C/p>\n\u003Cp>Great. So we would likewise just go through all of our different collections for our access control for the user role, and then we would just set those items. Right? So we could, whether we want this person to be able to edit Teams, yes or no. So again, I could copy this actual rule and I can get the raw value and I could use that.\u003C/p>\n\u003Cp>So we just copy paste this in. And now if I go to the team settings and I refresh, it doesn't look like this user can actually edit that. Users, direct as users is one of direct as users ID is 1 of okay. Oh, it's because there are no permissions enabled for the fields. Right?\u003C/p>\n\u003Cp>So now, with those field permissions updated, I can go in and this user can edit all of that information for that specific team. Likewise, for our to dos, we could go in and edit to dos. You know, you could set this up one of 2 ways where anybody within that team can edit this to do, or I could, you know, just let them edit to dos that we created or assigned. In this case, I'm just gonna use that same rule, copy and paste it in, and then I can go in and add all these fields that are available. Great.\u003C/p>\n\u003Cp>So we can edit those. You know, do we want to let them delete a team? Maybe we don't give them access to do that, but as far as the deleting to dos, go in and then as long as they're a member of that team, we can let them delete to dos in the account. So if I were to go here, now the user Team dot ID, that should be working. Teams dot ID.\u003C/p>\n\u003Cp>For some reason, this is not allowed for this specific user is one of current user dot Not sure why that is not available. It should be. Okay. If we give it all access, it is, But we're still logged in as that same user. So let's try to debug this a little bit, where team is one of current user dot teams, teams underscore ID.\u003C/p>\n\u003Cp>Now I can go in and delete that. Okay. So we've got that properly set up. Everything looks to be working correctly now. Where are we at, time wise?\u003C/p>\n\u003Cp>We got 26 minutes to, build out the rest of this SaaS application. Right? There's a a few other things, a few directions we could go, but as far as our functionality, we can create and manage tenants. We can create and manage users within those teams. And now we've made sure that all of our data is scoped to the individual team.\u003C/p>\n\u003Cp>And then, you know, technically they can go in and manage to do this. Right? But if I wanted to, flesh this out on the front end, like if I wanted to build an actual to do application on the front end, how would we make that work? Let let's just see how far we can get with that. So again, I've got a let me just close all the windows here.\u003C/p>\n\u003Cp>I've got this Nuxt application configured with a little bit of boilerplate already stood up just so we can take a look at how this works. But if I take a look, I've got a Directus module within this Nuxt application that is using the SDK. It does a few auto imports from the SDK and then just provides a composable called use Directus, that allows me to call the Directus SDK. You could of course call the API directly, but I like using the SDK just to standardize that across server or client runtimes. Alright.\u003C/p>\n\u003Cp>So if we take a look at our actual application, we've got nothing really fancy here. I do have an auth page. So if we go to auth/login, I've got a a simple login form that we can use. But, let's go open up our index page and start customizing this a bit, where if I wanted to read all of the tasks or the to dos from Directus. Alright.\u003C/p>\n\u003Cp>So I could do something like this where to dos equals await, what's my composable, use directus, and then we'll say read items. So I wanna read all the items from the to do's collection. And maybe I've just got, some options that will pass to that. And then here, let's just log out those to do's. Right?\u003C/p>\n\u003Cp>Let's see what this looks like. So if I open this up, you could see that we're getting an error, that says forbidden. So what does that actually mean? That is Directus telling us that the user for this application is not logged in. So what we could do is basically let's go to our login page, and let's actually just do this as well.\u003C/p>\n\u003Cp>So we'll get the user equals use state. This is a a helper with inside Nuxt, and I need to take a look at just what I've got set up here. It's just user. Alright. So we'll go back.\u003C/p>\n\u003Cp>We'll take a look at the user equals use state. And if I were to log the current user on this page, let's see what that shows. Maybe we just wait on that. We don't see anything at all. Right?\u003C/p>\n\u003Cp>So if I open up dev tools, we take a look at Vue, the user is undefined in this case. So let's just go in and let's log in using these credentials that we set up. Alright. So we look at our test user. We've got team a user at example with a very secure password, teammateuser@example.com with a password of password.\u003C/p>\n\u003Cp>I'm gonna log in. It's trying to redirect us to the portal, but that is not a page that we have right now. But now I can actually see the user because I am logged in. And if I were to just briefly swap these out, where we're just logging to do's, now I can actually see those to do's as well. So now that we're logged in, we're getting that information from Directus.\u003C/p>\n\u003Cp>Right? So let's do 2 things here and just check the clock very quickly. We're at 22 minutes. How do we set this up? Alright.\u003C/p>\n\u003Cp>So I'm just gonna sketch these out really quickly. We're going to show a list of to dos, and then we're going to have the form show a form to add to dos. This is your standard to do list functionality. Alright. So we got the user.\u003C/p>\n\u003Cp>If use if there's no user, let's just redirect to the login page. That's a good idea. Thank you, GitHub Copilot. And that will be await navigate to /auth/login. And, of course, if I was actually building this for real, I would take great care to modularize this and and extract some of this logic so it could be reused.\u003C/p>\n\u003Cp>You know, I'd probably stick this in a Nuxt middleware or something like that. So now if I were to go in and just delete the cookie that we're storing for the user, which is in here where it says direct us off, If I just nuke all the cookies, what's going on? Forbidden. If no user, maybe we need to wrap this up. Else, we're gonna fetch the oh, there is no argument for else.\u003C/p>\n\u003Cp>So this is still showing forbidden. Maybe we return dot navigate 2. If there is no user, just navigate dot 2. I don't understand what's going on here. If we nuke all this, should not be getting forbidden.\u003C/p>\n\u003Cp>Right? If user user dot ID, maybe. Okay. There we go. User was actually defined.\u003C/p>\n\u003Cp>There just weren't any properties within it. So we don't have a user ID. We're gonna redirect. Great. So again, if we do team a user at example.com, password, and log in.\u003C/p>\n\u003Cp>That's gonna redirect us to the portal. But if we go back to the home page, We logged in, do we not? Okay. Let's just scrap this for now. We'll go back to the home page, see what's going on.\u003C/p>\n\u003Cp>Okay. Great. So we want to show a list of to do's in this case, and we will do ul, to do list for to dos and to dos. Let's pick up the name. Then we have, let's actually wrap this in a p tag.\u003C/p>\n\u003Cp>We'll make this, what, text excel. Make it large. We'll make it bold. Great. Then we'll do a div and we have the description of that.\u003C/p>\n\u003Cp>So vhtml.todo.description. Prefer the self closing tags personally. And we'll use like the the Tailwind Pros class because I've got that set up. Here's the short description of this. Maybe we add a input type checkbox for to do.\u003C/p>\n\u003Cp>Just call it status. Alright. And let's go ahead and flex those 2. Right? So we'll wrap these again.\u003C/p>\n\u003Cp>Okay. Give it a little bit of gap and some padding and maybe a border. Let's start the items at the top. Cool. And maybe we wrap that as well.\u003C/p>\n\u003Cp>Okay. So now we got a list of to dos. How can we actually push this into our to do list? Let's add a form for rendering those to dos or how to add those to dos inside our list. So we got a form, thank you, to do dot name, to do dot description, submit button, add to do.\u003C/p>\n\u003Cp>Thank you, GitHub Copilot. Let's take a look at what we've got now. Cannot add properties of to name. That's because we don't have a to do. Right?\u003C/p>\n\u003Cp>So we'll go in. Let's add a new to do. This will just be a reactive object. And we need to change our v model from to do dot name. We'll do new to do dot name.\u003C/p>\n\u003Cp>That way we can keep everything savvy. That looks kind of rough. Not very pretty. Right? Let's change this.\u003C/p>\n\u003Cp>One of the UI libraries that I have baked into this starter is this is Nuxt UI library. Very handy little piece of kit. We got 16 minutes left. How can we show something impressive if we look at this library? Where did you go?\u003C/p>\n\u003Cp>There's like a a form group component or a form input already here. So we got a form group that gives us a label, and then we just wrap that. Okay. So let's take a look at that. We'll just open this up.\u003C/p>\n\u003Cp>We'll do new form group. We've got the name. That'll be an input. Title of the task or to do. We don't really need an icon for this, and we'll just v model out new to do dot name.\u003C/p>\n\u003Cp>And we'll do the same for description. Alright. So we'll just model this up. Description. Description of the task to complete.\u003C/p>\n\u003Cp>And we'll just do description. And then we've got, we'll just use their button component as well. Cool. Let's clean this up a little bit. Actually, let's add some padding to the whole form.\u003C/p>\n\u003Cp>So let's do something like p 8. Maybe we add a header here. I've got a built in VText component here. Let's make the size large, say task. And then we add some spacing between these items, space y 8.\u003C/p>\n\u003Cp>Alright. So we've got the form. We've got the actual task here. That's great. Alright.\u003C/p>\n\u003Cp>So this is a submit type, on our form. Maybe we wanna do at submit dot prevent, and then we're gonna build a method to submit that to do to our Directus API. So submit. Let's just call it add new to do. And let's go in and build this.\u003C/p>\n\u003Cp>Right? So we'll call this an async function, add new to do. And what is GitHub Copilot suggesting? We will, await, create item, add to dos, new to do. So we're gonna pass that.\u003C/p>\n\u003Cp>And then it is clearing out the items within that new to do so we can add a new one. This looks pretty good. Let's add one final thing. Maybe we want to wrap this in a try catch. We'll error out.\u003C/p>\n\u003Cp>And then the last thing that we probably wanna do is actually update the to do list. Right? So, one thing we can do here is wrap this function. Let's say async function, if I can actually spell, fetch to dos. Return data, get items to dos.\u003C/p>\n\u003Cp>Actually, let's just swap that. We'll return the data from that. Great. We'll log any errors. And now we're not seeing any to dos because we actually need to call that during the setup of this.\u003C/p>\n\u003Cp>So we'll call const to dos equals ref. And I could actually do something like this if we wanted to make it reactive where I just have to do's at the top here. And within this function, I could, to do's dot value, just update those to do's, which let's do that. And here, we'll say, wait. And this works because we are in we're using script setup with inside Nuxt review.\u003C/p>\n\u003Cp>So we could do fetch to do's. That will fetch the to do's when it sets up this component. And then when we add a new to do, we're going to fetch those to do's again. So if I go in and now let's test this out. New to do.\u003C/p>\n\u003Cp>Do a description here. We hit add to do. K. It did something. What did it actually do?\u003C/p>\n\u003Cp>Input types contains is not a function. No idea what we're doing here. Form at submit dot prevent is add new to do. This is a submit button. Got new to do dot name dot description.\u003C/p>\n\u003Cp>Why is this not showing? Right? Let's try try it again. Test to do. We'll look at our fetch request description, and it looks like this is okay.\u003C/p>\n\u003Cp>Post items to do's, payload status equals false. Right? What's what's the issue here? Why are we running into an error? And you could see because I've logged in as a different user in my application, Directus has also logged me out.\u003C/p>\n\u003Cp>So we'll just log back in really quickly. And again, we could see that the to dos are being created, but they are not being assigned to a team. Right? So we got 10 minutes and 30 seconds left to resolve this issue. If we look at our user, we could see that we have the team's array.\u003C/p>\n\u003Cp>Right? So we have the ID of that team. We just need to pass that along inside the request. You know, if if I did not have, like, a setup where a one user could be involved in multiple teams, you know, maybe I have a a preset within the API itself to to do that. You know, if if I've got a user that is a part of multiple teams, we have to pass that along, because it's not gonna know which team.\u003C/p>\n\u003Cp>So here inside this, we could also do something like this where we have a switch for this or, let's just pick up the team from the user. Right? So we've got the user here inside new to do, new to do dot team. Let's add the user dot, teams. It's just the first item inside the array.\u003C/p>\n\u003Cp>So something like this. Alright. Let's see. Test. Test.\u003C/p>\n\u003Cp>Test. Test. Test. Does that actually work? Looks like we're not getting any response from API team.\u003C/p>\n\u003Cp>Okay. There's the team. Okay. It's because we are the team is not an array. It's just a single value.\u003C/p>\n\u003Cp>Okay. Try it again. Test. Test. Add to do.\u003C/p>\n\u003Cp>What are we not doing? Right? So the team is actually we're not calling that data at all. New to do dot team equals user dot teams. Is that because that is a value?\u003C/p>\n\u003Cp>Let's refresh the page. Just test. To do's. Okay, it's it's not performing our post request. Cannot read values of undefined.\u003C/p>\n\u003Cp>So it's probably something like this. Let's just unref that user Dot teams 0. Test this again. To do's is a bad request. Okay.\u003C/p>\n\u003Cp>So we're passing something that Teams does not like. The team status is false. Why why is it not liking that at all? Team so if you look at our access control, let's try to diagnose this. We got the to dos.\u003C/p>\n\u003Cp>We can create to do's. We got access to create all the fields. And if we just delete some of these other ones, What's going on? Why can't we assign a to do a new to do to team a? Right?\u003C/p>\n\u003Cp>We should be able to do that. Test test test test test test. Todos is status is is false. Maybe that's it. Test.\u003C/p>\n\u003Cp>Test. It still does not like the fact. Invalid foreign key for the team. Okay. So maybe it should be an array then.\u003C/p>\n\u003Cp>Let's test again. Do not have permission to access this. Invalid foreign key for the team to dos. Why are we seeing this particular issue? Right?\u003C/p>\n\u003Cp>What are where are we at on the clock? We got 6 minutes. Let's resolve this thing and finish this one up. Right? So let's just leave the status out for now.\u003C/p>\n\u003Cp>Why are we not getting the proper response from Directus? Alright. So we go in, we look at our data model for our to dos. We've got a team. That's a mini to 1.\u003C/p>\n\u003Cp>If we just were to, like, log this out, let's say we give access to public access for all of our to dos, all the teams. And just to debug this a little bit, if I go into items, I do to dos, I could see that team is just, a single string. That's the UID for that string. And it should be one of these values, like 8bf or ending in a 69e. What are we doing?\u003C/p>\n\u003Cp>What are we passing inside that request? Test. Okay. Now I'm logged in as the the user again. Let me clear all the cookies.\u003C/p>\n\u003Cp>That could be what's going on. You know, if you're working with Directus in one window, you're using the API in another window, maybe you want to make sure that you're using incognito windows. So when you log in that you're not seeing something outrageous here. Let's clear our cookies out. Oh, and now we can see all the to do's because we are not logged in.\u003C/p>\n\u003Cp>And I could set up those permissions. So let's restrict those permissions again. We're just gonna remove all those. Great. And now we log in.\u003C/p>\n\u003Cp>We do team a user at example.com, go to password, go back a step, now we've got our to dos. Let's test. Alright. We're still getting the same thing. You don't have permission to access this.\u003C/p>\n\u003Cp>That's because it's not an array. We refresh, test, test. Again, invalid foreign key. Right? So what are we actually getting here for the team?\u003C/p>\n\u003Cp>If I look at our user, teams dot okay. So we have the user there. I don't understand why we're not able to access the value. Oh, let's add a team. We'll just give it a false string.\u003C/p>\n\u003Cp>User let's try user dot value dot teams dot 0. Maybe we just actually console log the the user. The user is right. Let's take a look at that. Alright.\u003C/p>\n\u003Cp>So we've got our to dos. We had test to do. Alright. So there's our user. Teams.\u003C/p>\n\u003Cp>Alright. Bad requests. Invalid foreign key for team in collection to dos. Something to do with passing the wrong team. So if we take a look at our teams, team a, Why are we getting the wrong value here?\u003C/p>\n\u003Cp>So team a should be this particular value. We're sending 5 e. Is that correct? 4e? No.\u003C/p>\n\u003Cp>I I don't know why we're where this value is coming from for teams for that specific user. Right? Team a user, do we add that user to a team that is not available? Team a. Oh, duh.\u003C/p>\n\u003Cp>That's because we're not actually getting the team user, we are getting the junction collection. So great idea, Brian. Let's go into our config for this and it actually should be the junction collection. So the, we need to access the team from the junction collection instead of the just the okay. Alright.\u003C/p>\n\u003Cp>So we can fix this. We'll go into our Nuxt config. Where are our fields for this? User fields, we're going to pick up the teams dot okay. So let's just refresh.\u003C/p>\n\u003Cp>Come on. Come on. Let's build. We got 30 seconds left to fix it. Come on, build, baby.\u003C/p>\n\u003Cp>Okay. So now within our function, we are going to go in and call instead of teams okay, then we're gonna use teams underscore ID. That should be it. And all these issues probably would have been prevented had we used TypeScript. But now we could see that.\u003C/p>\n\u003Cp>And with 54321 on the clock, you could see as I add to dos, boom, it is fetching those and updating them. Wow. So right at the clock, we have finished the multi tenant to do application. What would our next steps for this sort of thing be? Right?\u003C/p>\n\u003Cp>One probably hooking up subscriptions and and actually fleshing out this logic. So as far as next steps, let's just discuss those for a few. You know, add subscriptions. I can unstriker through those. We would add our subscriptions using something like Stripe.\u003C/p>\n\u003Cp>Clean up all of our data fetching, fetching and handling, and probably extract some of that logic into a middleware. But the major achievement here is being able to create that multi tenant role and permission setup within an hour, so that each user can only see their data. They can only interact with that data. So, great. You know, we took all 60 minutes on this one.\u003C/p>\n\u003Cp>The UI doesn't look fantastic, but as far as our back end, pretty much ready to go. I hope you'll join in for another episode of 100 apps 100 hours. Until then, I'll see you around.\u003C/p>","Welcome back to the next episode of 100 apps, 100 hours where we rebuild or recreate some of your favorite apps in 60 minutes or less or publicly fail trying. Sounds really scary when we say it that way. I am your host, Brian Gillespie, developer advocate at Directus, and today, we're gonna be building a multi tenant SaaS application. I realize that is not everybody's most favorite app, but it is the architecture that powers a lot of your favorite apps, and it's a question that comes up a lot within our community. So by multi tenant, what do we actually mean when we say that? It means that we have multiple different accounts or different tenants in this case, so we serve multiple different groups of users in one single application. And all of their data is separated. It's, you know, team a doesn't see the data or can't interact with the data for team b. It is totally doable on our back end as we're gonna see, and it's a popular convention for a lot of the SaaS apps that you will use so that we could share resources like a database, you know, an actual front end application without having to, you know, commission a brand new database for every single account that we have. Alright. Sounds great, sounds interesting, something that we want to build. How do we build it? What are the rules here? If you're new to the series, there are only 2 rules. We have 60 minutes to plan and build an application, no more, no less. And I do like to include a little planning ahead of time so we know, or before we actually build, not ahead of time. We're starting fresh here. You've got as much of a clean slate as I do. And then the second rule is use whatever you have at your disposal, whether that's Tailwind, online resources, AI, anything is fair game. We've got to get as far as we can in 60 minutes or less or look like an idiot. So sounds great. Everybody knows the rules. We're gonna be building the multi tenant SaaS app. Let's open this up and fire off the countdown. Here we go. So before we actually touch any code or any data models or, you know, anything in our back end, let's go through and and plan this out a little bit. And usually I like to start with our functionality. So let's shrink this down a little bit. And what functionality are we looking for out of this application? We want to create and manage tenants. In this case, let's call it, Teams is a pretty good convention. You know, accounts would be one that I see often organizations and in some applications. We wanna create and manage users within those teams. We wanna make sure that all data, make sure all data is scoped to each team. Right? So team a can access team b's data and vice versa to the team. And then, you know, a SaaS app needs to do something, so let's do just like the standard to dos. So we wanna create and manage to dos. Perfect. Alright. So there's our functionality. Nothing particularly fancy but it is tricky to set up some of this, especially around permissions. You know, that is a a big piece of the puzzle. Security is always very important. So, you know, how how do we set this up and how do we do it within an hour? Let's go through and now let's let's work on our data model a little bit. So we got our data model. And what are we gonna need as far as our tables or our different collections that we're gonna be using? So we will have, what, teams. Let's shrink this up a little bit. We will have users, we'll have to do's, and you know, we may not get to this but if I was gonna actually have this be built as a SaaS that I could charge people for, we'd probably have something like subscriptions. And we didn't include that in the functionality because I'm not sure exactly how far we can get in an hour, but we'll get to as much as possible. Right? So in this scenario, a user could be part of one team or, technically I guess we could build it so that a user can be in multiple teams. Multiple teams. Let's break that in half so we can actually see that. To do's belong to a team. To do's belong to a team assigned to a user. And then there's probably a a junction table here that is what? Users and teams. Oh, let's use something that's not connected. Right? So this is probably a table called something like teams users because it is a many to many relationship and just to get fancy and finish out our little arrows could look something like this. I'm definitely not an artist but you get the picture. Alright. So we've got teams, we've got users, we have to do's, we have team underscore users or or some of junction collection. We potentially have, subscriptions. So what do we have set up to actually build this? I'm using Directus for the back end of the application. I've got a super simple Nuxt application for the front end that we may or may not get to that and I've just got a little boiler plate that, uses the Directus SDK for that information. Let's jump right in. So if I pull up my Docker Compose and that's how I've got Directus set up locally, just through a simple Docker Compose file that you pull down from the documentation. And I'm gonna get logged in to our first user, and I'll just copy and paste this password here because I suck at typing a lot of times. Alright. So you can see that we've got a blank application. There is no data model. We've got a single admin user. We're starting from a clean slate. So if I just pull this up and we rearrange all these windows, maybe let's pull this over to the side so we can actually see what we're building, And I'll zoom in a little bit so you've got a better view. Maybe we don't put the clock in our face, so we're so we're not beating up against the clock. But how do we start? You know, let's start by creating this tenant collection. I'm gonna call this teams. You could easily call it tenants. You probably want to whatever makes the most sense in the context of your application is probably what I would use. Right? So the teams here, we're gonna generate a primary key called ID. We're just gonna use a UUID for that. As far as the status of that, yeah, we probably want a status for the team. We don't need a sort field. And then these others are just system fields that you can create, to give you a little shortcut. You know, when was it created? What was the user that created it? This specific team, blah blah blah. Alright. So as far as the team, what are we gonna need for the team information? We'll probably need a name for that team. That's great. Maybe we want to have a slug for the team. Could be a a good one to have. That way, we're not using a weird UUID, but instead a slug for the team. And let's go in and, adjust this a little bit. We're going to look for the interface section and Directus has the slugify option to make sure that slug input is URL safe. And one of the other things I'm gonna do here is just open up my little mouse pose tool. If you guys are ever wondering how I do this little highlight on the screen, that is a tool called mouse pose, very handy piece of kit. Alright so we've got our teams, now let's add our users, right? I could create a separate user collection but when I fire up Directus, it gives me a system collection for users that I can use to actually I could just reuse that for all of the authentication, all of the roles and permissions because what happens behind the scenes with Directus as my back end is it will mirror all these tables or these collections that we're creating inside the app to MySQL database, which is super handy. I I keep MySQL pure and, MySQL databases remains pure. And I've got built in REST and GraphQL APIs that are also automatically generated based on this data model. So tremendously powerful, enables you to build really quickly. So we're just gonna use that that system collection. I'm gonna I am gonna go in and create a new one. Let's just call it team users, and we'll generate a UID for that. We'll leave that one blank for now. Let's go in and add our to do's. So each to do we'll need, who's the date created, who was the user created it. We wanna give those a status as well, maybe a sort, so we can rank those in order of priority. So we've got the bare bones of our data model here, and we've got the again, we're gonna use the directus users system collection. How do we go and actually start setting up the tenancy? Right? Because right now, if anyone were to log in to this application, they would see all the data that we have available. So first, we are going to go in and create our relationships between these different collections, or which are just equivalent to tables within your Postgres database, let's first go through and do the teams and users. So a user can be in multiple teams. Let's go in and model that relationship. So we'll hit create field. We're gonna look for the many to many relationship inside Directus here, and let's just call this our users. Alright so the related collection is going to be users as well, actually it's not. It's gonna be directus_users, and that'll turn purple letting me know that that is an actual collection. And then we probably wanna show a link to the item. But instead of hitting save here, I'm going to hit continue in advanced field mode or advanced mode because I want to control what my Junction collection is named. So I'll just go in, we've already got this, it's called team underscore users, And I could go in and edit the, the foreign key fields here as well. It's just gonna be teams underscore ID, directus underscore users underscore ID. That's fine. Not a big deal. And if I wanted to, I can also add the reverse or the inverse relationship. Somebody will correct me on the comments. But, I can add that back to the Directus users collection so that I can reference that in any of our permissions that we're going to use later. We probably don't necessarily need a sort field, not a big deal. And the rest of this is pretty standard. So let's go ahead and save that. And now we have our users that are linked to, that direct us users table. So if I were to go in and create a team, I can see that I can add existing users or create new users for that team. Great. Alright. So next, let's take a look at teams and to dos. Right? So the to dos belong to a single team and I can assign those to dos to a user. So if we flesh out this data model a little bit more, we probably got a title or a name for this to do, let's use that. We probably have a description, so for that we'll just use our WYSIWYG editor inside the back end here. And I can even control what options we have as far as HTML content, but we'll just keep this the standard one for now. Alright. And then we've probably got a user that this has been assigned to. So we're gonna scroll down to our relational section. And depending on how we wanted to assign these, if we wanted to assign this task to or this to do to many users, you know, I would use either like a many to 1 or a one to many. In this case I just want to assign it to a single user. You know, whoever we assign this to is ultimately responsible for getting this thing done, and we're just gonna call this assign to. So we're going to use the many to one relationship within Directus, call it the assign to, and our related collection is going to be Directus users. So we'll hit save, that's who we assign this to. And now, let's go in and make sure this is applied to a team. So we need that relationship to a team so we can actually filter that out. Right? If team a is logged in or a user on team a is logged in, we don't want them to be able to see or interact with any of the to dos for team b, unless they are part of that team, which makes it a little more complicated to model as well. But we'll just call this, team or, you know, it could be team ID. Whatever. I'm gonna I'm gonna stick with team, and then the related collection is teams. Straightforward enough. Right? So if I were to go in now and we create a to do actually, let's just first create our team. Right? I've been talking about team a, there's the slug for team a, and team b. Cool. So now we have 2 teams, And I can go in and if I were creating a to do item, let's just say test item for team a. Here's a short description. I could assign this to a user which we'll come back and fix, but then I pick a team as well. Right? Now if I were a member of team a or team b, I wouldn't want to pick this, so we'll come back to that in a moment. But, basically, I can have a team that owns each one of these to dos. Great. So that's all set up. Right? How do we go in and manage those relationships? Because, you know, if we were to go in and create a user, let's just call it test user a for we'll we'll call it team a. Right? Team a user atexample.com. We're gonna do a real secure password here of password. And let's give them oh, the only role that we have now is administrator, so we'll sort that in a moment. But if we were to open this up and we go to team a user, so I'm in a incognito window. We'll go to password. Great. Okay. So and now you can see I could see all the teams, I can edit all the to dos. Right? So how do we solve for this problem? That is going to be using our user roles or the access control within Directus. So if we go to our access control, let's create a new role and call it user in this case. And they are going to have app access, you know, whether we enable app access or not. If I was building our front end and I I'm not sure if we're gonna have enough time to actually build the front end for this, but, you know, I could disable app access as I was using the APIs to to access this information. But we'll just call this user. And then we can go in and restrict our roles. Right? So now if I were to go in and, again, this is an incognito window on my left and the regular window on my right, so these have 2 different user sessions. But if I were to go in now to my team a user and change that from user or from administrator to user, you're gonna see that this person or this user, team a, does not see any content whatsoever and that is because we have not enabled any permissions for the user role. So we can go in and now let's start enabling some of these permissions. Right? So as far as Teams, we have create, read, update, delete, and the optional share, operation. But how do we how do we give them access to just the teams they're a part of? Right? Well, first, I need a relationship between our users and the teams, and and that's a piece that we forgot. Or did we? No. We've already fleshed that out. We've got our users there. Cool. So now I've got that relationship created, we can account for it inside our teams. Alright. So inside of permissions, let's go and we'll use if we use read access and we enable all access, what will we see? Right? Oh, that's the public permission. I guess I need to go into the user permission. Always love building against the clock. Right? You overlook some things. So here we can see all the teams now. That's not great because this user is a member of team a or at least should be. Let's make sure that we've got that set up. Right now they're a part of no team, so they can see all the teams, which is not helpful either. But let's make them a part of team a, and yet even though they're a part of team a, they can still see team a. So inside the that permissions, what we'll do, if if I actually go to the right one, we'll use the custom permission and we'll configure a rule that restricts that access. So the team, we can look and make sure that users, and I could drill into this, users dot directus user ID is one of, and we'll use a little shorthand here, current_user.id. So this is a dynamic variable within Directus that allows us to pick up the current user, so who is actually logged in, and we'll use the ID field. So this will allow us or should allow us to refresh and then we'll see that now this user can only see items or teams that are they're a part of, which is great. Right? They can't edit any of this information right now, but we could go in and restrict that as well. But just by doing this where we say our users and then we reference the user ID, which is gonna be an array in this case because we could have multiple users for one team, is one of dollar sign current underscore user dot ID. That allows us to build that permissions in. And I could even go through and restrict the specific fields that they could read about this as well if I didn't want them to have access to the slug or the other users, whatever. So now if we go into something like to dos, you know, we can enable all access for to dos, right, and we'll get the same effect where this one is assigned to a different team, they can still see that. So I think right now, this one is assigned to team a, I believe. And I could fix that as well where I go in, I go to my to dos and and instead of showing the actual ID, we wanna show the name of the team that's assigned to you. So we'll just on our display templates, we'll adjust that and go back, and we could see that, okay, this is assigned to team a. Let's create a new to do to do for team b. And again, I'm logged in as the administrator account over here. This is the individual user on the left. We'll save that. And if I were to go back to to dos now, they could still see to do for team b. Right? So how do we fix that? We go back into our access control and instead of having all access, again we use custom access. So we'll set up a rule where the team, and I could drill down and get the team dot ID, is again, we'll use is one of current user dot Teams? I can't remember what we named this, if it's team or team. Let's try team dot ID and see. Okay. So that breaks it to where they are not able to access any of it. Let's go back to our data model. We're gonna look at system collections, and we've got teams. Teams is the name of that. So if we go back, teams teams dot ID. Let's see if that sorts it out. It does not. Let's just check the syntax again. Is one of teams. Blah blah blah. Why is that not working? Team. Team team is okay. So we're hitting a snag here somewhere. Team dot ID. Let's try that. Team dotid is equal to current user dot teams is one of equals unexpected server error. So it has to be this one, Current user dot teams, or maybe it's just team. Alright. Let's take a look at our user directory. We've got our team. Teams hasn't been properly configured. Maybe something in the data model? Let's take a look. Teams is a many to many relationship. We've got team underscore users, team's ID. Oh, okay. Maybe that could be it. Team we've got to access through the junction collection, I believe. So we need to go back and, duh, we'll do current user dot team_users.directus. Nope. Team is teams_id. Team dotid. Let's try this. Team dot id is one of current user dot teams users or actually, maybe it is that teams dot ID. Still struggling. One of the the fine points of being against the clock. Right? We got 37 minutes, I'm going to cheat and not cheat, remember there are no rules, and we're just gonna reference one of the other, applications that I've built where I've referenced this before for a client role. So let's go in and look at this user dot contacts dot organizations dot ID, dot organizations dot underscore ID. So that is the collection there. That would be the organization's ID. Let's just take a look at the data model for this really quickly. Users. Okay. Trying to figure this out on a hurry. Users dot team users. No. Still really struggling here. Alright. So if we back this up, team, let's take a look at this one again one time. I just love building against the clock. Right? Let's let's just go back and evaluate what I've done. My brain is not thinking well today, I guess, so I haven't had enough coffee. So we'll go into our teams. We've got the users, so that's gonna be our junction collection. And then within the users collection or our junction table, it's gonna be we're identifying by the team's underscore ID. So thinking of it through that lens, our permission here instead of this whatever I've got here, it should just be something like this where it is the team underscore ID. So the team that's assigned to this to do, we get the ID from that team, and then we use the current user, and then we use the teams, which will be an array of objects, and then we get the teams underscore ID from that junction collection. So if I hit refresh, boom. Okay. Now we're cooking with gas. Let's just close this sidebar and continue building, right? So now I'm seeing only the to dos that have been assigned or or available to that specific team. Right? And if I want to see other users within that team, so within that junction collection, we're probably gonna need to do something similar here. So we can see users that are, where the teams dot id equals current user dot, oh, current underscore user dot teams dot teams underscore ID. So again, we're just referencing that relationship that we built and we'll probably do the same with our direct us users. Right? We don't want them to be able to see all the users. Where are you? Now I can't see any user. What's going on there? Test user for team a. Did we totally mess something up? Okay. So I could see these. We've got Let's go in and adjust the permissions for this. So we shouldn't be able to see, actually, that's gonna be on the the users collection anyway. Right? So users, we want the field permissions, the teams dot teams ID is one of current_user.teams._teams or dot teams underscore ID. Right. Great. So now we can only see users that are a part of the team that they're assigned to. Cool. Alright. So how do we actually go about creating to dos, right? This is not allowed inside this account. If we give all access, they should be able to create to dos now. But say we only we want to, restrict this team, right, or we wanna default this particular, team name, right, or team ID. What we could do is something like this where inside our to dos, we have field presets. So I could go in and say team is what, current, So we'll do this, like current underscore user dot teams dot ID. No. Dotteams_id. And maybe we get the first one, and that's how we assign. Let's see what this does just for testing. Alright. So we create a new to do. I should have access to this. Why is it saying we don't have permission access? Don't have permission to access this. Let's just try creating a test. Test to do. Hit save. Unexpected error occurred. Probably something to do with the preset that we've got saved here. Oh, I forgot the brackets. Could be it. Yeah. Still showing we do not have permission to access that. Great. So now we can go in and create this test to do. Alright. So we'll pick the team a, we'll assign that to team user a. Great. So now I can see those to dos. If we load up all the to dos in our master account or our administrator account, we could see that, I could see those to dos that are assigned for team b, but I cannot see those for team a because this user is not part of team a, right, and they cannot update that themselves. Great. So we would likewise just go through all of our different collections for our access control for the user role, and then we would just set those items. Right? So we could, whether we want this person to be able to edit Teams, yes or no. So again, I could copy this actual rule and I can get the raw value and I could use that. So we just copy paste this in. And now if I go to the team settings and I refresh, it doesn't look like this user can actually edit that. Users, direct as users is one of direct as users ID is 1 of okay. Oh, it's because there are no permissions enabled for the fields. Right? So now, with those field permissions updated, I can go in and this user can edit all of that information for that specific team. Likewise, for our to dos, we could go in and edit to dos. You know, you could set this up one of 2 ways where anybody within that team can edit this to do, or I could, you know, just let them edit to dos that we created or assigned. In this case, I'm just gonna use that same rule, copy and paste it in, and then I can go in and add all these fields that are available. Great. So we can edit those. You know, do we want to let them delete a team? Maybe we don't give them access to do that, but as far as the deleting to dos, go in and then as long as they're a member of that team, we can let them delete to dos in the account. So if I were to go here, now the user Team dot ID, that should be working. Teams dot ID. For some reason, this is not allowed for this specific user is one of current user dot Not sure why that is not available. It should be. Okay. If we give it all access, it is, But we're still logged in as that same user. So let's try to debug this a little bit, where team is one of current user dot teams, teams underscore ID. Now I can go in and delete that. Okay. So we've got that properly set up. Everything looks to be working correctly now. Where are we at, time wise? We got 26 minutes to, build out the rest of this SaaS application. Right? There's a a few other things, a few directions we could go, but as far as our functionality, we can create and manage tenants. We can create and manage users within those teams. And now we've made sure that all of our data is scoped to the individual team. And then, you know, technically they can go in and manage to do this. Right? But if I wanted to, flesh this out on the front end, like if I wanted to build an actual to do application on the front end, how would we make that work? Let let's just see how far we can get with that. So again, I've got a let me just close all the windows here. I've got this Nuxt application configured with a little bit of boilerplate already stood up just so we can take a look at how this works. But if I take a look, I've got a Directus module within this Nuxt application that is using the SDK. It does a few auto imports from the SDK and then just provides a composable called use Directus, that allows me to call the Directus SDK. You could of course call the API directly, but I like using the SDK just to standardize that across server or client runtimes. Alright. So if we take a look at our actual application, we've got nothing really fancy here. I do have an auth page. So if we go to auth/login, I've got a a simple login form that we can use. But, let's go open up our index page and start customizing this a bit, where if I wanted to read all of the tasks or the to dos from Directus. Alright. So I could do something like this where to dos equals await, what's my composable, use directus, and then we'll say read items. So I wanna read all the items from the to do's collection. And maybe I've just got, some options that will pass to that. And then here, let's just log out those to do's. Right? Let's see what this looks like. So if I open this up, you could see that we're getting an error, that says forbidden. So what does that actually mean? That is Directus telling us that the user for this application is not logged in. So what we could do is basically let's go to our login page, and let's actually just do this as well. So we'll get the user equals use state. This is a a helper with inside Nuxt, and I need to take a look at just what I've got set up here. It's just user. Alright. So we'll go back. We'll take a look at the user equals use state. And if I were to log the current user on this page, let's see what that shows. Maybe we just wait on that. We don't see anything at all. Right? So if I open up dev tools, we take a look at Vue, the user is undefined in this case. So let's just go in and let's log in using these credentials that we set up. Alright. So we look at our test user. We've got team a user at example with a very secure password, teammateuser@example.com with a password of password. I'm gonna log in. It's trying to redirect us to the portal, but that is not a page that we have right now. But now I can actually see the user because I am logged in. And if I were to just briefly swap these out, where we're just logging to do's, now I can actually see those to do's as well. So now that we're logged in, we're getting that information from Directus. Right? So let's do 2 things here and just check the clock very quickly. We're at 22 minutes. How do we set this up? Alright. So I'm just gonna sketch these out really quickly. We're going to show a list of to dos, and then we're going to have the form show a form to add to dos. This is your standard to do list functionality. Alright. So we got the user. If use if there's no user, let's just redirect to the login page. That's a good idea. Thank you, GitHub Copilot. And that will be await navigate to /auth/login. And, of course, if I was actually building this for real, I would take great care to modularize this and and extract some of this logic so it could be reused. You know, I'd probably stick this in a Nuxt middleware or something like that. So now if I were to go in and just delete the cookie that we're storing for the user, which is in here where it says direct us off, If I just nuke all the cookies, what's going on? Forbidden. If no user, maybe we need to wrap this up. Else, we're gonna fetch the oh, there is no argument for else. So this is still showing forbidden. Maybe we return dot navigate 2. If there is no user, just navigate dot 2. I don't understand what's going on here. If we nuke all this, should not be getting forbidden. Right? If user user dot ID, maybe. Okay. There we go. User was actually defined. There just weren't any properties within it. So we don't have a user ID. We're gonna redirect. Great. So again, if we do team a user at example.com, password, and log in. That's gonna redirect us to the portal. But if we go back to the home page, We logged in, do we not? Okay. Let's just scrap this for now. We'll go back to the home page, see what's going on. Okay. Great. So we want to show a list of to do's in this case, and we will do ul, to do list for to dos and to dos. Let's pick up the name. Then we have, let's actually wrap this in a p tag. We'll make this, what, text excel. Make it large. We'll make it bold. Great. Then we'll do a div and we have the description of that. So vhtml.todo.description. Prefer the self closing tags personally. And we'll use like the the Tailwind Pros class because I've got that set up. Here's the short description of this. Maybe we add a input type checkbox for to do. Just call it status. Alright. And let's go ahead and flex those 2. Right? So we'll wrap these again. Okay. Give it a little bit of gap and some padding and maybe a border. Let's start the items at the top. Cool. And maybe we wrap that as well. Okay. So now we got a list of to dos. How can we actually push this into our to do list? Let's add a form for rendering those to dos or how to add those to dos inside our list. So we got a form, thank you, to do dot name, to do dot description, submit button, add to do. Thank you, GitHub Copilot. Let's take a look at what we've got now. Cannot add properties of to name. That's because we don't have a to do. Right? So we'll go in. Let's add a new to do. This will just be a reactive object. And we need to change our v model from to do dot name. We'll do new to do dot name. That way we can keep everything savvy. That looks kind of rough. Not very pretty. Right? Let's change this. One of the UI libraries that I have baked into this starter is this is Nuxt UI library. Very handy little piece of kit. We got 16 minutes left. How can we show something impressive if we look at this library? Where did you go? There's like a a form group component or a form input already here. So we got a form group that gives us a label, and then we just wrap that. Okay. So let's take a look at that. We'll just open this up. We'll do new form group. We've got the name. That'll be an input. Title of the task or to do. We don't really need an icon for this, and we'll just v model out new to do dot name. And we'll do the same for description. Alright. So we'll just model this up. Description. Description of the task to complete. And we'll just do description. And then we've got, we'll just use their button component as well. Cool. Let's clean this up a little bit. Actually, let's add some padding to the whole form. So let's do something like p 8. Maybe we add a header here. I've got a built in VText component here. Let's make the size large, say task. And then we add some spacing between these items, space y 8. Alright. So we've got the form. We've got the actual task here. That's great. Alright. So this is a submit type, on our form. Maybe we wanna do at submit dot prevent, and then we're gonna build a method to submit that to do to our Directus API. So submit. Let's just call it add new to do. And let's go in and build this. Right? So we'll call this an async function, add new to do. And what is GitHub Copilot suggesting? We will, await, create item, add to dos, new to do. So we're gonna pass that. And then it is clearing out the items within that new to do so we can add a new one. This looks pretty good. Let's add one final thing. Maybe we want to wrap this in a try catch. We'll error out. And then the last thing that we probably wanna do is actually update the to do list. Right? So, one thing we can do here is wrap this function. Let's say async function, if I can actually spell, fetch to dos. Return data, get items to dos. Actually, let's just swap that. We'll return the data from that. Great. We'll log any errors. And now we're not seeing any to dos because we actually need to call that during the setup of this. So we'll call const to dos equals ref. And I could actually do something like this if we wanted to make it reactive where I just have to do's at the top here. And within this function, I could, to do's dot value, just update those to do's, which let's do that. And here, we'll say, wait. And this works because we are in we're using script setup with inside Nuxt review. So we could do fetch to do's. That will fetch the to do's when it sets up this component. And then when we add a new to do, we're going to fetch those to do's again. So if I go in and now let's test this out. New to do. Do a description here. We hit add to do. K. It did something. What did it actually do? Input types contains is not a function. No idea what we're doing here. Form at submit dot prevent is add new to do. This is a submit button. Got new to do dot name dot description. Why is this not showing? Right? Let's try try it again. Test to do. We'll look at our fetch request description, and it looks like this is okay. Post items to do's, payload status equals false. Right? What's what's the issue here? Why are we running into an error? And you could see because I've logged in as a different user in my application, Directus has also logged me out. So we'll just log back in really quickly. And again, we could see that the to dos are being created, but they are not being assigned to a team. Right? So we got 10 minutes and 30 seconds left to resolve this issue. If we look at our user, we could see that we have the team's array. Right? So we have the ID of that team. We just need to pass that along inside the request. You know, if if I did not have, like, a setup where a one user could be involved in multiple teams, you know, maybe I have a a preset within the API itself to to do that. You know, if if I've got a user that is a part of multiple teams, we have to pass that along, because it's not gonna know which team. So here inside this, we could also do something like this where we have a switch for this or, let's just pick up the team from the user. Right? So we've got the user here inside new to do, new to do dot team. Let's add the user dot, teams. It's just the first item inside the array. So something like this. Alright. Let's see. Test. Test. Test. Test. Test. Does that actually work? Looks like we're not getting any response from API team. Okay. There's the team. Okay. It's because we are the team is not an array. It's just a single value. Okay. Try it again. Test. Test. Add to do. What are we not doing? Right? So the team is actually we're not calling that data at all. New to do dot team equals user dot teams. Is that because that is a value? Let's refresh the page. Just test. To do's. Okay, it's it's not performing our post request. Cannot read values of undefined. So it's probably something like this. Let's just unref that user Dot teams 0. Test this again. To do's is a bad request. Okay. So we're passing something that Teams does not like. The team status is false. Why why is it not liking that at all? Team so if you look at our access control, let's try to diagnose this. We got the to dos. We can create to do's. We got access to create all the fields. And if we just delete some of these other ones, What's going on? Why can't we assign a to do a new to do to team a? Right? We should be able to do that. Test test test test test test. Todos is status is is false. Maybe that's it. Test. Test. It still does not like the fact. Invalid foreign key for the team. Okay. So maybe it should be an array then. Let's test again. Do not have permission to access this. Invalid foreign key for the team to dos. Why are we seeing this particular issue? Right? What are where are we at on the clock? We got 6 minutes. Let's resolve this thing and finish this one up. Right? So let's just leave the status out for now. Why are we not getting the proper response from Directus? Alright. So we go in, we look at our data model for our to dos. We've got a team. That's a mini to 1. If we just were to, like, log this out, let's say we give access to public access for all of our to dos, all the teams. And just to debug this a little bit, if I go into items, I do to dos, I could see that team is just, a single string. That's the UID for that string. And it should be one of these values, like 8bf or ending in a 69e. What are we doing? What are we passing inside that request? Test. Okay. Now I'm logged in as the the user again. Let me clear all the cookies. That could be what's going on. You know, if you're working with Directus in one window, you're using the API in another window, maybe you want to make sure that you're using incognito windows. So when you log in that you're not seeing something outrageous here. Let's clear our cookies out. Oh, and now we can see all the to do's because we are not logged in. And I could set up those permissions. So let's restrict those permissions again. We're just gonna remove all those. Great. And now we log in. We do team a user at example.com, go to password, go back a step, now we've got our to dos. Let's test. Alright. We're still getting the same thing. You don't have permission to access this. That's because it's not an array. We refresh, test, test. Again, invalid foreign key. Right? So what are we actually getting here for the team? If I look at our user, teams dot okay. So we have the user there. I don't understand why we're not able to access the value. Oh, let's add a team. We'll just give it a false string. User let's try user dot value dot teams dot 0. Maybe we just actually console log the the user. The user is right. Let's take a look at that. Alright. So we've got our to dos. We had test to do. Alright. So there's our user. Teams. Alright. Bad requests. Invalid foreign key for team in collection to dos. Something to do with passing the wrong team. So if we take a look at our teams, team a, Why are we getting the wrong value here? So team a should be this particular value. We're sending 5 e. Is that correct? 4e? No. I I don't know why we're where this value is coming from for teams for that specific user. Right? Team a user, do we add that user to a team that is not available? Team a. Oh, duh. That's because we're not actually getting the team user, we are getting the junction collection. So great idea, Brian. Let's go into our config for this and it actually should be the junction collection. So the, we need to access the team from the junction collection instead of the just the okay. Alright. So we can fix this. We'll go into our Nuxt config. Where are our fields for this? User fields, we're going to pick up the teams dot okay. So let's just refresh. Come on. Come on. Let's build. We got 30 seconds left to fix it. Come on, build, baby. Okay. So now within our function, we are going to go in and call instead of teams okay, then we're gonna use teams underscore ID. That should be it. And all these issues probably would have been prevented had we used TypeScript. But now we could see that. And with 54321 on the clock, you could see as I add to dos, boom, it is fetching those and updating them. Wow. So right at the clock, we have finished the multi tenant to do application. What would our next steps for this sort of thing be? Right? One probably hooking up subscriptions and and actually fleshing out this logic. So as far as next steps, let's just discuss those for a few. You know, add subscriptions. I can unstriker through those. We would add our subscriptions using something like Stripe. Clean up all of our data fetching, fetching and handling, and probably extract some of that logic into a middleware. But the major achievement here is being able to create that multi tenant role and permission setup within an hour, so that each user can only see their data. They can only interact with that data. So, great. You know, we took all 60 minutes on this one. The UI doesn't look fantastic, but as far as our back end, pretty much ready to go. I hope you'll join in for another episode of 100 apps 100 hours. Until then, I'll see you around.","published",[139],{"people_id":140},{"id":141,"first_name":142,"last_name":143,"avatar":144,"bio":145,"links":146},"791e1503-1d88-463d-9347-0b9192933576","Bryant","Gillespie","9013afc8-e8d7-4182-9b18-44db08117bb9","Developer Advocate at Directus",[147,149],{"url":131,"service":148},"website",{"service":150,"url":151},"github","https://github.com/bryantgillespie",[],{"id":154,"number":155,"year":156,"episodes":157,"show":168},"56dda5ff-2c3a-41ce-ae3a-580d6101026b",1,"2023",[158,159,160,122,161,162,163,164,165,166,167],"cb4e067f-9507-4e18-ab9a-435565f9e653","8434838a-8e4f-489a-8da2-fbf10de5de6a","c997b25e-400c-4350-bba4-f63853d844f7","dcb952e0-7d13-43ea-9b1c-f2ca63efd07d","0aae287d-3916-4d91-9310-25828998e562","8fed0eee-43f6-4767-8b77-79da7a059821","a311b57d-34e7-4073-9cf3-6a8c6c0f8b85","9271a4fc-addf-4400-9591-f2f2ec07bd79","30c48566-52cd-4728-a633-ca9675acc959","c067c012-5fe1-4d70-8946-8bfc8e1b22c0",{"title":169,"tile":170},"100 Apps In 100 Hours","fb0f9d45-be21-4634-94d4-2ef1cc5146f2",{"id":172,"slug":173,"season":174,"vimeo_id":175,"description":176,"tile":177,"length":178,"resources":8,"people":8,"episode_number":155,"published":179,"title":180,"video_transcript_html":181,"video_transcript_text":182,"content":8,"seo":183,"status":137,"episode_people":184,"recommendations":186},"9a3a8ffa-a27b-421c-93cf-3da2dcb726e9","crm","14fda5f2-95de-4dbe-a4e2-3fd956c21c19","936325383","In this intense one-hour challenge, watch as Bryant incredibly builds a full-featured custom CRM from the ground up using Directus. He builds contacts, organizations, deal pipelines, activities, and more.","f6b880a0-5cd2-45ad-beff-3117f0a78581",56,"2024-04-19","Mission: Customer Relationship Manager","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season.\u003C/p>\u003Cp>If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality.\u003C/p>\u003Cp>That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM.\u003C/p>\u003Cp>A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go.\u003C/p>\u003Cp>So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to.\u003C/p>\u003Cp>What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up.\u003C/p>\u003Cp>So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people.\u003C/p>\u003Cp>I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities.\u003C/p>\u003Cp>Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality.\u003C/p>\u003Cp>Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection.\u003C/p>\u003Cp>Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right?\u003C/p>\u003Cp>When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by.\u003C/p>\u003Cp>That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright.\u003C/p>\u003Cp>So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time.\u003C/p>\u003Cp>And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with.\u003C/p>\u003Cp>Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation.\u003C/p>\u003Cp>So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields.\u003C/p>\u003Cp>Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title.\u003C/p>\u003Cp>And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string.\u003C/p>\u003Cp>Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great.\u003C/p>\u003Cp>K. Phone, email. We'll put email above phone. Cool. Looks nice.\u003C/p>\u003Cp>Alright. Let's just take a look. Right? We've got first name. Go ahead.\u003C/p>\u003Cp>Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact.\u003C/p>\u003Cp>These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations.\u003C/p>\u003Cp>So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great.\u003C/p>\u003Cp>Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right?\u003C/p>\u003Cp>Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization?\u003C/p>\u003Cp>You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company.\u003C/p>\u003Cp>So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save.\u003C/p>\u003Cp>So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that?\u003C/p>\u003Cp>Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop.\u003C/p>\u003Cp>So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship.\u003C/p>\u003Cp>And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that.\u003C/p>\u003Cp>So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations.\u003C/p>\u003Cp>Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right?\u003C/p>\u003Cp>On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great.\u003C/p>\u003Cp>So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here.\u003C/p>\u003Cp>Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes.\u003C/p>\u003Cp>I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right?\u003C/p>\u003Cp>So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection.\u003C/p>\u003Cp>Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright.\u003C/p>\u003Cp>So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay.\u003C/p>\u003Cp>So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league.\u003C/p>\u003Cp>We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay.\u003C/p>\u003Cp>So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great.\u003C/p>\u003Cp>Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this.\u003C/p>\u003Cp>So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at.\u003C/p>\u003Cp>Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great.\u003C/p>\u003Cp>Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data.\u003C/p>\u003Cp>So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right?\u003C/p>\u003Cp>We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields.\u003C/p>\u003Cp>But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal.\u003C/p>\u003Cp>So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here.\u003C/p>\u003Cp>We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact.\u003C/p>\u003Cp>The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes.\u003C/p>\u003Cp>We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially.\u003C/p>\u003Cp>Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right?\u003C/p>\u003Cp>Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection.\u003C/p>\u003Cp>Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it.\u003C/p>\u003Cp>And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages.\u003C/p>\u003Cp>Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application.\u003C/p>\u003Cp>So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right?\u003C/p>\u003Cp>Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red.\u003C/p>\u003Cp>This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted?\u003C/p>\u003Cp>Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that.\u003C/p>\u003Cp>Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo.\u003C/p>\u003Cp>Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got?\u003C/p>\u003Cp>Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray.\u003C/p>\u003Cp>Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here.\u003C/p>\u003Cp>So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great.\u003C/p>\u003Cp>If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value.\u003C/p>\u003Cp>So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like.\u003C/p>\u003Cp>Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right?\u003C/p>\u003Cp>So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on.\u003C/p>\u003Cp>I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright.\u003C/p>\u003Cp>Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need.\u003C/p>\u003Cp>Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage.\u003C/p>\u003Cp>That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there?\u003C/p>\u003Cp>1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact.\u003C/p>\u003Cp>Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh.\u003C/p>\u003Cp>Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale.\u003C/p>\u003Cp>Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on.\u003C/p>\u003Cp>Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay.\u003C/p>\u003Cp>Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display.\u003C/p>\u003Cp>So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back.\u003C/p>\u003Cp>We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail.\u003C/p>\u003Cp>That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL.\u003C/p>\u003Cp>Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool.\u003C/p>\u003Cp>And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout.\u003C/p>\u003Cp>Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created.\u003C/p>\u003Cp>I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here?\u003C/p>\u003Cp>Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right?\u003C/p>\u003Cp>So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great.\u003C/p>\u003Cp>And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense.\u003C/p>\u003Cp>We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone.\u003C/p>\u003Cp>One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call.\u003C/p>\u003Cp>The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way.\u003C/p>\u003Cp>Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting.\u003C/p>\u003Cp>And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright.\u003C/p>\u003Cp>So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email?\u003C/p>\u003Cp>Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great.\u003C/p>\u003Cp>Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good.\u003C/p>\u003Cp>Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived.\u003C/p>\u003Cp>What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right?\u003C/p>\u003Cp>Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed.\u003C/p>\u003Cp>Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date.\u003C/p>\u003Cp>Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right?\u003C/p>\u003Cp>So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal.\u003C/p>\u003Cp>Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright.\u003C/p>\u003Cp>So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities.\u003C/p>\u003Cp>We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type.\u003C/p>\u003Cp>Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment.\u003C/p>\u003Cp>But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to.\u003C/p>\u003Cp>Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner.\u003C/p>\u003Cp>Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well.\u003C/p>\u003Cp>Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay.\u003C/p>\u003Cp>Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright.\u003C/p>\u003Cp>Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now.\u003C/p>\u003Cp>I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar.\u003C/p>\u003Cp>Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah.\u003C/p>\u003Cp>This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that.\u003C/p>\u003Cp>And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good.\u003C/p>\u003Cp>Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that.\u003C/p>\u003Cp>And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright.\u003C/p>\u003Cp>Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr.\u003C/p>\u003Cp>Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that.\u003C/p>\u003Cp>And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool.\u003C/p>\u003Cp>Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right?\u003C/p>\u003Cp>If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see.\u003C/p>\u003Cp>Business. Is there a business? There you go. That looks somewhat like a business. We've got activities.\u003C/p>\u003Cp>Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great.\u003C/p>\u003Cp>And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here.\u003C/p>\u003Cp>That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings.\u003C/p>\u003Cp>So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome.\u003C/p>\u003Cp>Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks.\u003C/p>\u003Cp>We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man.\u003C/p>\u003Cp>And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is.\u003C/p>\u003Cp>It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed.\u003C/p>\u003Cp>That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win.\u003C/p>\u003Cp>I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations.\u003C/p>\u003Cp>We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that.\u003C/p>\u003Cp>Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows.\u003C/p>\u003Cp>This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow.\u003C/p>\u003Cp>We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow.\u003C/p>\u003Cp>In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create.\u003C/p>\u003Cp>Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage.\u003C/p>\u003Cp>We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great.\u003C/p>\u003Cp>If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally.\u003C/p>\u003Cp>I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user.\u003C/p>\u003Cp>We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there.\u003C/p>\u003Cp>We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox.\u003C/p>\u003Cp>We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here.\u003C/p>\u003Cp>For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload.\u003C/p>\u003Cp>Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user.\u003C/p>\u003Cp>Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be.\u003C/p>\u003Cp>We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you.\u003C/p>\u003Cp>And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it.\u003C/p>\u003Cp>And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications.\u003C/p>\u003Cp>Send notification. Cool. Send notification. The find_user.id. Cool.\u003C/p>\u003Cp>Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no.\u003C/p>\u003Cp>Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there.\u003C/p>\u003Cp>That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us.\u003C/p>\u003Cp>Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage.\u003C/p>\u003Cp>Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal.\u003C/p>\u003Cp>It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens.\u003C/p>\u003Cp>Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name.\u003C/p>\u003Cp>Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great.\u003C/p>\u003Cp>So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome.\u003C/p>\u003Cp>Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways.\u003C/p>\u003Cp>Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win.\u003C/p>\u003Cp>That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season. If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality. That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM. A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go. So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to. What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up. So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people. I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities. Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality. Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection. Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right? When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by. That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright. So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time. And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with. Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation. So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields. Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title. And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string. Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great. K. Phone, email. We'll put email above phone. Cool. Looks nice. Alright. Let's just take a look. Right? We've got first name. Go ahead. Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact. These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations. So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great. Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right? Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization? You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company. So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save. So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that? Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop. So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship. And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that. So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations. Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right? On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great. So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here. Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes. I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right? So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection. Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright. So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay. So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league. We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay. So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great. Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this. So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at. Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great. Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data. So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right? We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields. But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal. So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here. We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact. The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes. We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially. Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right? Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection. Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it. And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages. Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application. So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right? Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red. This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted? Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that. Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo. Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got? Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray. Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here. So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great. If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value. So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like. Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right? So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on. I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright. Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need. Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage. That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there? 1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact. Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh. Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale. Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on. Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay. Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display. So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back. We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail. That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL. Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool. And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout. Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created. I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here? Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right? So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great. And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense. We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone. One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call. The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way. Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting. And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright. So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email? Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great. Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good. Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived. What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right? Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed. Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date. Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right? So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal. Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright. So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities. We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type. Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment. But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to. Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner. Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well. Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay. Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright. Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now. I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar. Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah. This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that. And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good. Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that. And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright. Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr. Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that. And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool. Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right? If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see. Business. Is there a business? There you go. That looks somewhat like a business. We've got activities. Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great. And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here. That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings. So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome. Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks. We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man. And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is. It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed. That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win. I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations. We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that. Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows. This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow. We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow. In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create. Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage. We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great. If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally. I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user. We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there. We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox. We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here. For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload. Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user. Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be. We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you. And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it. And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications. Send notification. Cool. Send notification. The find_user.id. Cool. Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no. Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there. That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us. Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage. Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal. It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens. Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name. Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great. So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome. Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways. Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win. That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.","592c22ad-1cb2-4d65-9bd7-e0d7c98fe4f2",[185],"e9e66fa8-0650-4e37-ae8b-74755fdd5dca",[],{"reps":188},[189,245],{"name":190,"sdr":8,"link":191,"countries":192,"states":194},"John Daniels","https://meet.directus.io/meetings/john2144/john-contact-form-meeting",[193],"United States",[195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244],"Michigan","Indiana","Ohio","West Virginia","Kentucky","Virginia","Tennessee","North Carolina","South Carolina","Georgia","Florida","Alabama","Mississippi","New York","MI","IN","OH","WV","KY","VA","TN","NC","SC","GA","FL","AL","MS","NY","Connecticut","CT","Delaware","DE","Maine","ME","Maryland","MD","Massachusetts","MA","New Hampshire","NH","New Jersey","NJ","Pennsylvania","PA","Rhode Island","RI","Vermont","VT","Washington DC","DC",{"name":246,"link":247,"countries":248},"Michelle Riber","https://meetings.hubspot.com/mriber",[249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,226,437,438],"Albania","ALB","Algeria","DZA","Andorra","AND","Angola","AGO","Austria","AUT","Belgium","BEL","Benin","BEN","Bosnia and Herzegovina","BIH","Botswana","BWA","Bulgaria","BGR","Burkina Faso","BFA","Burundi","BDI","Cameroon","CMR","Cape Verde","CPV","Central African Republic","CAF","Chad","TCD","Comoros","COM","Côte d'Ivoire","CIV","Croatia","HRV","Czech Republic","CZE","Democratic Republic of Congo","COD","Denmark","DNK","Djibouti","DJI","Egypt","EGY","Equatorial Guinea","GNQ","Eritrea","ERI","Estonia","EST","Eswatini","SWZ","Ethiopia","ETH","Finland","FIN","France","FRA","Gabon","GAB","Gambia","GMB","Ghana","GHA","Greece","GRC","Guinea","GIN","Guinea-Bissau","GNB","Hungary","HUN","Iceland","ISL","Ireland","IRL","Italy","ITA","Kenya","KEN","Latvia","LVA","Lesotho","LSO","Liberia","LBR","Libya","LBY","Liechtenstein","LIE","Lithuania","LTU","Luxembourg","LUX","Madagascar","MDG","Malawi","MWI","Mali","MLI","Malta","MLT","Mauritania","MRT","Mauritius","MUS","Moldova","MDA","Monaco","MCO","Montenegro","MNE","Morocco","MAR","Mozambique","MOZ","Namibia","NAM","Niger","NER","Nigeria","NGA","North Macedonia","MKD","Norway","NOR","Poland","POL","Portugal","PRT","Republic of Congo","COG","Romania","ROU","Rwanda","RWA","San Marino","SMR","São Tomé and Príncipe","STP","Senegal","SEN","Serbia","SRB","Seychelles","SYC","Sierra Leone","SLE","Slovakia","SVK","Slovenia","SVN","Somalia","SOM","South Africa","ZAF","South Sudan","SSD","Spain","ESP","Sudan","SDN","Sweden","SWE","Tanzania","TZA","Togo","TGO","Tunisia","TUN","Uganda","UGA","United Kingdom","GBR","Vatican City","VAT","Zambia","ZMB","Zimbabwe","ZWE","UK","Germany","Netherlands","Switzerland","CH","NL",1773850437781]