Deploying a decoupled restaurant review site to production with Strapi and


Node.js has become an increasingly popular language to build and deploy backend APIs. In a world of legacy CMSs adopting decoupled implementations, plenty of frameworks have sprung up to classify themselves as "headless" CMSs, designed from the start to provide an easy way to personalize content models, administer permissions and authentication, and serve a content API quickly.

Strapi, one of the leaders in this space, has recently released their v4 version of the framework, and with it can be deployed alongside a number of frontends within the same project, giving a drastically simplified development experience working with decoupled sites. In this workshop, we'll deploy a Strapi demo application, which has been configured to serve a restaurant review site.

Piece piece you will add database services, tests, and frontends, all within the safety of isolated development environments. At the end, each user will have a functioning decoupled site, and some greater understanding of working with decoupled sites in production.


Hey, I'm Chad Carlson from Platform SH. So who here has heard of Platform SH before? If you have, go and leave a message. If you haven't, no big. I'll just take the silences. Nobody's heard this before. Yeah, heard but not used. Okay. Great. How about Strapi? Does anybody already have some familiarity with Strapi? We'll do a little bit of overview when we get to that section. Yeah. Heard but not used. Yeah, I'm not surprised. Okay. All right. Created a sample. Cool. All right. So maybe the best thing to do here is start with, for those of you who have heard of Strapi or unfamiliar with, what we're going to be using for our demo today is based off of the official demo app that Strapi provides called Food Advisor. And so what this application contains is a Strapi application within this api subdirectory and a next.js app inside of client. And so inside of api, we're going to have a few commands there. You're going to seed some sample data initially to a SQLite database. And that's going to set up our backend and it's going to contain restaurants, authors, a few blog posts and categories and tags that go along with those collections. And then at the front end app, it's just a simple Next app that's set up to consume all that data from the Strapi api and presents a front end that looks sort of like this screenshot. And we will get to that at the end. So what I'd like to start with is go ahead and try and zoom this a little bit, go to the terminal where you are going to be working from. And when we interact with the platform CLI, everything's going to have this platform prefix here. And we're going to initially do login. And all that's going to do is it's going to generate a temporary SSH key for the CLI to use based off of a temporary token on your account. And so, oh, I already got ahead of myself. Sorry, guys. Once you have installed the CLI, go ahead and click this link right here. This will take you to the page to register an account for the workshop. And it'll take you to a screen to create a new project. Don't worry about doing that right now. We'll do that through the CLI here. But once you've created that account, you can log in on the email that you've used and authorize the CLI to use your account. And so we'll see now that I'm logged in. There's my email if you want to get in touch with me. All right. And so first, we're going to go ahead and get this demo repo. There is one SQL migration dump file in here, so it might take a second. And it should be a directory called node Congress. So in here is just like I described before in the original food advisor demo app, an api and client subdirectory. All the instructions, if you want to follow along with the repo or in this doc subdirectory on your instructions. So I'm going to go ahead and use the CLI here to create a project. And when I do that, I can say no Congress. I'm going to give it a region. This is just pick something that's close to you or to go ahead and most of these are going to be by default on your side, but we're going to pick a development project default for environments, storage, and a main default branch. And then it'll ask you, do I want to make that project remote for the repo we're working with? Go ahead and do yes. And then we'll confirm it and then PlatformSH will start building that project for you. Hey, I'm on deep. No, at this point, we have just the 30 day free trial. We're in kind of development phase of trying to get a free tier for our platform. But right now it's the free trial. But if you contact me and you want to continue playing with it, I know Slack channel or on my email, which I can attach here. It's not difficult for us to extend the trial for you continue playing with it. So we got a URL here for what we call the management console. So I can open a new tab and give it into the bar and we'll go ahead and be taken to the individual project. And so all this is going to be is going to be our deployment area for the repository. It doesn't matter if we integrate a repository on GitHub, we push something locally like we're going to do in this workshop. The project is effectively the equivalent of our repo. And around it, it's going to detect a few configuration files to automatically provision infrastructure based off what we commit and to handle some of the inheritance that's going to make a platform do what it does. That includes access permissions, environment variables, and that same infrastructure and the data within it across all of the environments that will roughly correspond to each branch of our repo. So we'll see here I have a main environment that is inactive because I haven't pushed anything to it for my main branch because that's the only branch I have on this repo. Now that we have that set up, let's take a look at the repository that we just cloned. All right. Like I showed before, we have an api and a client subdirectory. I just want to make sure that I got covered here. So what we're going to do first is start off with StratB. We want to make sure that we have our data source set up before we do anything else. And so like I said, that's within the api subdirectory here. For those of you not familiar with Strapi, it has for v4 onward a configuration subdirectory that mostly what we will be concerned with how it's configured is this database.js file, which in this case says all of our data is going to come from a SQLite database in the .tmp directory, which we haven't created yet. And then there's going to be a source directory that is going to be where components are defined. So when I talked previously about we're making effectively a restaurant review app that we're going to deploy. So there will be content types like restaurants or blog posts or categories or reviews. And so within our components, we have restaurants. Sorry. We have a blog page, an article that defines an individual content type for one of those entities I described. We have categories, we have some global pages, we have locations, restaurants, and a restaurant main page and review. So this repo already has all those collections defined for you ahead of time. What we're going to do is we're going to see that .tmp database, that SQLite database. So I'm going to go ahead and go into api. Shadrack, is there anything else that you wanted to say about the structure of Strapi while I kind of continue doing this? Yeah, sure, sure. Do I need to share my screen? Okay, you need to do this. So the way Strapi is structured is Strapi is actually, hold on, let me just pull out this here. Before Shadrack gets started, what I'm going to do is install dependencies, run the scene command and run this api locally. For those of you who are on OS 10, check the repo because there is this catch that I know I hit sometimes on my computer. If you go ahead and copy this command, you'll notice that this will halt and so you'll just need to export this environment variable and then everything should be fine for installing the dependencies there. Okay, yeah, thanks. So Strapi is just basically like any CMS that you can think of, but this time it's running on Node. The way it's built is built to basically work as a back end, a headless CMS basically to work as a back end for whatever front end that you might need to connect it with and Strapi has the power to also work standalone. So if you need a standalone CMS, just a CMS, Strapi also has the power to do that. So if you check the api, if you check the api folder, you would see that it's built, it's separated based on, the folders are basically named based on whatever work they are doing, but the most important part of these particular folders is the source folder and the config folder. And okay, let's say the database folder, but that's when you run database migration. So the source folder and the config folder, the config folder can consist of whatever type of configuration that you want specifically on your Strapi app. So how your admin should look like, what the api tokens that you need, that's where it should be. Then the api, so the api is just basically the way Strapi 4 looks like. Before Strapi 4, the api, there's a particular api file called api.js in the build folder where it's usually, it was not there before, it's just basically in Strapi 4 where they are trying to do like separation of concerns so that your admin, your database file or your admin file does know you choked up. So all of this, if you've used Strapi 3 before, you would see that all of these particular files are just looking new, but they are actually part of the server.js and the admin.js files. They just, what Strapi did with fashion 4 is they separated all of these particular functions into several files so that if you need to make any single change or any, it would easily change it, basically separation of concerns. So the next thing is cron tax. So if you need a cron, so if you need a cron basically to maybe update your articles particular time of the day, publish an article at the time of the day, that's what your cron tax file is for. And if you look at this, it's already defined here to make a particular publish in a particular time of the day, then database.js is your best friend generally. I know everybody uses the CMS because they need a content management system. You need something that can store your content. So database.js file consists of the configuration for your database. Configuration for SQLite is different from the way you would do it with Postgres and the way you would do it with mongodb. Strapi 4 does not have support for mongodb, you could still use SQLite, MySQL, and Postgres on Strapi. So what here is basically doing is trying to connect to your database, running database. Platform message gives services as database as service basically. So to connect to that database, basically, you can modify this particular file here. But since we've not yet pushed to platform message yet, this particular file here basically is just connecting to your local SQLite database. Basically connected to your SQLite database. When we push to it, as we go further in the workshop, I think Ivan was asking a question, you'd see where we would create a relationship, a database relationship, and try to see how we would connect it with Strapi. So that's what database.js file is for. Then middleware is basically if you need an extra middleware, then plugins. So the plugins file generally is for, Strapi has support for installing several plugins. So when you install a plugin, you want to create your own plugin, your custom plugin. So the way Strapi is built is you can install plugins from the admin dashboard, but you can also build your own personal plugins. So if you have any special requirements, any special updates you need to do, any special company you need to create for yourself, the plugins.js file is basically where you'd probably want to do that. Now, like every node.js file, the server.js is the most important part of it because we need to run a node.js server. When you start up server.js is basically connecting, creating one, two, three, several ports for our host to listen on. And that's basically just a simple server that has a host and a post generally. That's entirely what the config file is for, which is sort of like one of the most important part of the file. Then the source folder, which is where a lot of the whole magic goes on from the UI, et cetera, for the admin dashboard, the extensions your admin dashboard needs. So if you bootstrap a normal Strapi app, a lot of the content you see here would actually not be there because it's bare. But since we already have a built application, you would see that the way it's really, really separated into various files, admin api components, extensions, and your index of VS, which is your entry point, it's really, really easier. So Strader now just started the Strapi server. So if you check the repo, there's an instruction there on how to get to this particular point. So you can just run the and develop on the api, inside the api folder, then your admin accounts create the following credentials with node workshop, then the email, like admin at, then the password, you can use what is being displayed there. And after that, I would like you, Chad, to go to this particular link, the articles, the api slash articles. Can you go there, Chad? Yeah, sure. The only thing I was going to add is I ran the yard and seed command and what it did is initialize this SQLite database. And it gave me all of our images for our restaurant review app and this public uploads. As for these credentials, just so you know, you can change these later, you should probably use these. It's not a big deal if you don't, but later on in the demo, these will be the credentials that you'll have to use, and you can update it. So I just kept them consistent here. So you wouldn't have to remember a couple different passwords. Like I said, we can change them later. Yeah, I logged in with, I created an admin user with those creds, and that gave me the dashboard. And you'll see that this has the same structure reflected that was in the source api subdirectory here. And our seed command has initialized all of this initial data to go along with all these different collection types, including the images that were a part of that seed. And so what Cheddar was asking me to do is Strapi provides this dashboard essentially on top of the database we're using, which at this point is SQLite, but we'll switch it to a different service. And the whole point here is we're going to begin serving an api on top of this. So you'll see inside the instructions, I have this right here. So this is our local server at 1337. Here's the dashboard, but instead, I'm just going to go straight to the article's collection type. And we'll see for that seeded data there. Sorry, go ahead, Shadrack. You want to go? You want me to keep going? Oh, yeah, yeah, sure. So this basically, these articles you're seeing here is what we got from seeing the particular data in if you navigate to CNP, you see data.db. So the data.db file is basically what has given us these particular articles. So what we are going to do essentially, what the entire concept of this talk is, this workshop is we are going to take this data, this particular article data we are seeing here, the images, the files, and we're going to deploy it, then serve it on a front end. That's basically the entire idea behind this talk. So we're going to add the front end in this talk and connect it to Strapi and see this particular data displayed in this very, very interactive form. Then we're going to take the entire app and deploy it on Platform Message as a decoupled application. So that's basically the entire goal of this talk on this workshop right now. I'm used to doing talks. Sorry. So let's go into Platform a little bit. We have a local server. Great. Now we just need to get this deployed. So we have already, let's make sure everything looks okay here. Sweet. So every application is going to have at least three configuration files on the platform that are associated with three different container types. So the first one will be how do we want traffic directed to our application? In this case, I want all traffic to go to an application container directly, which in this case, we'll name in a second Strapi for this backend, at this placeholder domain. So what's interesting about this placeholder is that when we in a little bit create a branch, we'll get a development environment to go along with that branch. And a URL is just going to be generated and substituted for this default placeholder. So that's what that default means there. We're also going to get an ID. We can see in a little bit where that becomes in handy, but essentially associating this route with an ID. Yeah. Sure. Yeah. So I just needed to point out that the upstream, the name of your title of your upstream has to match with the title of your application in your platform.yaml file. So whatever name you are giving, so in the api here, whatever name you are giving the upstream here, it has to match whatever name you're going to give in your platform configuration file. So you have to put that in check because I've seen like a lot of people, even myself, like beginning using platform for the first time and getting errors because we omitted that particular information. So you have to keep that in mind going forward. Yeah. So we'll see that here. So just remember this label here for Strapi. Then we also have one other route definition, which is a redirect from the www subdomain. And all of this together is going to end up at a generated URL at this api subdomain on our environment. So the next configuration file is the platform.yaml file that Shadrack just described. So in this case, I'm going to have an application container that's named Strapi so that traffic from our router container goes to the application. It's going to contain within it node.js12. Like I said before, this is a major version. In between deployments, you don't have to define minor and patch versions of the runtime language of the app container. We will deploy them for you as they are released. There are stages of an application container. There are build stages and deploy stages. We're all familiar with these. So in here, what I have defined is I'm going to override some of the default behavior of a node.js container, which in this case is npm. And then from that, I'm going to define what's called a build hook. And in that build hook, I'm going to say, install my dependencies using yarn, which I've installed at the beginning. And I'm going to build the application. Then I can define a start command. In this case, we don't have any deployment steps, which would be in a deploy hook. We'll see in a little bit. But we are going to have a start command. In this case, I'm going to pass the production environment variable and start the application. The only other relevant things that might be worth looking at here is that platform tries to provide a little bit of assurance when you deploy applications. When I said that your project is effectively repository, that means that all of the logic that goes into infrastructure and individual builds is tied to commit hashes, to the slugs that you have associated with an individual commit. So what gets built on top of that is a rule. If you're in the build hook, you can write to the file system. But as soon as the build hook is finished, everything is read only at that point. And when we do it that way, we're going to be able to do some interesting things. Mainly, we can associate all of our configuration for our infrastructure and all the code in our repository to a single commit ID. And then we can reuse that slug that's created from the commit hash, and we can move that build around wherever we want it to. In this case, when we create a branch, we'll get an exact same deployment that we have on production. When we do a merge, we'll be able to take what we had in the development environment and effectively move it to production without any of the fear that when we do that, that we're going to ruin our production site. So because of that, you may actually need to write to the file system at runtime. If you do, we have it defined in these mounts here. So we just showed in the local situation that we have this .tmp file or directory where the database is. So we can define a mount here that allows us to continue to write that database at runtime. If it's not defined explicitly within your mount, it's not going to have write access. And so that's kind of the assurance that gives you repeatable builds and a great deal of security when you actually deploy this thing. Same thing for the uploads that have our images in it because at runtime, we may want to add more images when we create new articles, for example. So I think that's a decent overview of our configuration. Let's go ahead and stop the local server. And we're going to commit to the project. So in this case, you should have a remote to find this platform. I believe I have all the commands inside the instructions listed as platform. So I'm going to commit the changes. I guess I didn't make any changes as I was going through this. All right. And then we're going to go git push platform main. And then we're going to go ahead and push this up to project. So go ahead and do that yourself. This is going to take just a minute as we build this application for the first time on the back end. It's going to take a moment. So go ahead and do it yourself on your local until we can all catch up to the same point. And we'll see that that environment is getting initialized from that commit just now. And so based off of our configuration file, we'll see the activity of the push, all the commits I have on the repo, the application name, the runtime that I'm using, and then a tail end of the hash that's used to identify this specific build. And so in this case, build will be a combination of application code, everything that we have in those platform configuration files, like the app demo file, roots and services, all lumped into one hash. So we'll see when we branch that we can actually reuse this hash and save some of our time on builds. Right now, we're doing the same installation locally, and we're doing compilation. I do see that since we started, we gained a couple new people. So hi, welcome. I'm Chad Carlson from Platformers H. I'm with Shadrach Akintayo. If you go ahead and check out the chat in Zoom, or preferably inside of discourse, it'll have a link to the repository that we're working with, which is Platformers H dash workshops, slash node Congress, and then some starting steps. And then you can get to this same page, the instructions page that we're working from here. Some of these deployments will take a little bit. So I'm sure you guys can catch up if you've come a little late. Okay. Not sure why that happened. Okay. All right. First deploy. So what we have here is the finished activity for the commit for our main environment, the generated URL. And we're actually going to see a picture of what our cluster looks like right now. One thing that I didn't describe initially was there's a third configuration file called a services YAML file. This is where we're going to define our MySQL database. But right now we don't have anything. So we don't see it in the cluster. We just see a node.js app for Strapi. And we have the router container inside of our cluster. We go to our generated URL. Okay. TLS handshake didn't go through there. We see that we have our deployed Strapi app that we had locally. If I go ahead and go to admin, we'll see the same login sheet that we had before. Before we do that, we want to migrate some of that data that we generated before, which we have locally. So we can do that by taking those directories that we defined as having write access right now, those mounts, and uploading the documents that we seeded locally. So in this case, I'm going to, let's do this one first. I'm going to upload my local api, and I'm going to upload everything inside that .tmp, which is that SQLite database to the mount upstream. Go ahead and continue. And then I'm going to do the same thing for the photos. We'll see all those getting uploaded. So let's go ahead and refresh this and create our user again. There's that in case you didn't have that in front of you again. Okay. Okay. Okay. Okay. Okay. Did anybody else get in the same error on your side? I'll tell you what the issue is in a second as soon as I track it down. Have you seen what the error is? Listen, have you seen what the error is? Yeah, I saw it before, but I thought I fixed it in my app demo. Okay. Okay. What did you put in the environment file? Do you have an .emv file already? Okay. I think, hold on, you don't need that much, but that should work. I don't think you need to same link them here since already I got stuck on it still. So that's why I was putting it there. Okay. I think that it's just not all these finished. Okay. Okay. Did you just reload? Okay. Okay. While child is still working on this, does anybody have any question they'd like to answer? You can just, you know, drop it in the Zoom chat or in Discord, really anywhere you want. And does anyone's talk somewhere, rather than Chad? Yeah, you guys are probably all stuck on the same stuff here. I'll figure out what the issue is on this configuration. So just let me know. Okay. Yeah, it's deploying successfully, but. Yeah. This is really annoying. Okay. Okay. Chad Rock, are you doing this on your son? Yeah, I'm trying to. Okay. Can you check the console again? Check the app logs. Check the app logs. Well, it's the same thing. Okay. That's good. I think this might just be scrappy error. Did you, like, when you were working on this, did you encounter this particular error? Only one other time. All right, guys, I'm going to try and see if that was the issue. Give me just one second. Does anybody have any questions? Okay. All right, I'm just doing this to check to make sure this can fix this initial step on your all side, but if this doesn't fix the issue with SQLite, I'm just going to move on because that's not the purpose of this demo. Anyways, this was just supposed to show you that we're going to take what we had initially locally and put it on our production environment, but we're going to switch that out with an actual production service here next. So, like I said, if fixing this SimLink I had set up doesn't fix the issue, we're just going to go ahead and move on. Let's see, fingers crossed. Oh, not going to do it. We're going to move on. Okay, so what we don't see here is that SQLite database on production, but that's okay. Oh, my camera's still off because what platform is really useful for is for using production services. So that's what we're going to add now and we're going to forego the SQLite stuff. So on the next step, we're going to create a new environment and what it's going to do is by copying this command with the platform CLI, it's going to create a new branch locally and it's going to make an exact copy of what we had on production that didn't deploy, but it's going to put it on this new development environment and we'll see exactly what I said before. If we looked back at our initial activity, we're going to see this commit hash on the production environment and that because we haven't changed anything with an additional commit in creating this new environment because we just did a branch, we're going to rebuild or sorry, we're going to reuse the build from that production environment. So in this case, we're going to forego all the dependency installation that we had before and everything's just going to get moved to this new space. So if I go back to the project level of the management console, we'll see that that updates environment is about to be created. Now, while that's going on, let's go ahead and make some changes. So like I and Shadrack talked about before in Strapi, going to this api subdirectory, one of the most important parts of our configuration is this database.js file. And so right now it's loading from this SQLite database, or at least is locally. What we want to do is we want to change this to accept or to save our data to an Oracle MySQL database. So if you go into the doc subdirectory of the repo, you will find this file database MySQL.js. So go ahead and copy that file, go back to the database.js file and paste it there. So let's look at what's going on here. First thing is it's going to load a library that's already been installed in the repo called platform shconfig. So when we define a service, we're going to put that in the services YAML file in a second. And that's going to do things like give it a name, tell us how much disk space is there. Then we're going to place a definition called a relationship in our application definition. Now, once we do that, that's actually going to expose all the credentials to access that service container inside the application container. And it does that through environment variables. In this case, there will be a base64 encoded JSON object called platform relationships. And what this library does is it decodes it. In this case, it's a module for node.js so that inside of our application, we can easily access those credentials and use them in our application. So for here, let's go ahead and do some definitions of the rest of it so that we can see how this all fits together. Right now, I have an empty services YAML file. But if I instead go into my docs, we'll see that I have a new services YAML file that I can place there in this .platform hidden directory. And here's our configuration to get a new MySQL container. I give it a name. I say the type, which version I want, and how much disk is associated with it. And that's it. As soon as we push this, we get a whole new change in our infrastructure on this updates environment. So keep in mind this name for the service, dbMySQL. I need a way to get to, what's the right way to put it, to allow access to this service container within the application container. Nothing else in the world will be able to access this other than the application container. So again, if I go inside my docs directory, and if I go to the Strapi app YAML file, we'll see something that's pretty close to what we had before, but with a few changes. So I'm going to go back into api, to my platform app YAML file, and paste it in there. In this case, we have pretty much the same build hook. The only real change that's happened here is the definition of a new relationship, which in this case is going to be pointing at the dbMySQL service container that has the following type, and we're going to name every way that I interact with that container through this relationship MySQL database. And so with that relationship name, we'll see that that's what we have here. MySQL database as the relationship name. This block right here is relevant because in order to make sure that these builds are reusable across environments and do things like I described before, of saving that build ID on branch and merge events, one of the other restrictions other than no write access at runtime is that these containers can't talk to each other during build. And so what we do is we temporarily tell Strapi, hey, continue to use the SQLite configuration while you're building, and then once we get into a deploy state on platform SH, which is with this statement here, we're going to then load that relationship and grab the credentials from the environment, from the environment variables there, and then use that to connect to the database. And that's what we'll see inside of our activity when we push here. We're going to use the default build hook or default SQLite database until those service containers are available, and then switch over as soon as they are. So I have defined a relationship, a new service, and I've told Strapi how to connect to it. And that should be it. So what I'm going to do is I should be on the updates environment or branch there. I should have changed these three files. So we'll go ahead and push those changes up. And so what I have included inside of this repo is a dump from one of these service containers called FoodAdvisor SQL. Essentially, what I did is I took the SQLite database that seeds automatically, and then I used a pretty interesting tool that I found recently, and it's a SQLite to MySQL conversion library for Python, uploaded it to a service container because it interacts with it when it does its conversion with the actual live service container, and then I downloaded that dump file. There should be some documentation of how I did that inside this example repo as well. We'll just take a minute to deploy those changes. Hopefully, you have the same going on on your side. I'm trying to think if there's anything interesting to put in here. I guess you saw while I was trying to troubleshoot the SQLite issue. You saw me SSHing into the application container. You're able to do that for every single environment. So here I'm on the updates environment. It's like we're still in build, so I won't be able to do it until this thing deploys. We have these drop-downs here that give the URL for where our roots configuration directing traffic, a raw SSH link, and then commands to actually clone the repo from the project itself. What you saw was me just using the platform CLI, just using platform SSH, and then especially at the end of this, when we have multiple application containers going, you can specify which app you want to troubleshoot an SSH into. So you see here, we have that if statement that I was talking about during the build step where we're just going to continue to use the SQLite database until this thing hits runtime. All right. Our service graph hasn't updated yet. All right. Now we see that it has. We now have a router container, the application container, and the SQL database. I believe initially these TLS certificates are not liking my environment names. This thing's going to fail. That's because we have a service container, but we don't have any data in it. So it doesn't know what to do with it. So we're going to do a migration. We're going to copy this command, and we'll see that this is not in this folder, but if we go into api, we should... Oh, sorry, not api. We're going to go into docs, and then in docs, we're going to add that foodadvisor SQL dump. So we are on the updates branch in the updates environment. So I'm going to run platform SQL, not main. We're just going to use the current environment, foodadvisor SQL. So then we're just going to give that a second to upload that dump file into our service container and actually migrate all of our restaurant data onto the updates environment's Chappie app. A lot of data, a lot of data. All right. So now the data is there. This is going to be fine. We're going to go to our admin login, and the dump is already going to contain those same credentials that we had earlier. This is why I was prefacing it as using them, because then you don't have to reset it. So I'm going to do admin example, admin one, two, three, four. Looking good. I'm going to go ahead and log in. And so now our Chappie app has all the same content that we had locally, but it's been migrated now to a Oracle MySQL container. And so we have everything that we had locally, everything is already published. So now what we want to do is merge this into production. So let's get the command for that again. So we're going to use the CLI again, and we're going to merge the updates environment into production. Yes, I do. All right. And so that is going to take just a second. And so while that is happening, I'm just going to preface it, that part of the benefit of having this development environment is that we could test our migration outside of our production app. So we will have to run it one more time for production. But if our migration failed for some reason, we wouldn't have a broken production site, obviously, because we tested out on this development environment. So we'll go ahead and give that a second to merge into production. While that is happening, though, we can start to work on this front end because we have a live api now. Same as I did before, I can go to api articles, and there's all our articles on our development environment. This is deploying, so I'm okay. Go here. Still in the updates branch. So in this case, I am going to go into the client subdirectory now for the first time. And so here we have a next.js app. We see that we don't have anything specific to on it except for this one script, which I'll go to in a second. So what we're going to do is just check out what this looks like. We'll see that for the front end, we have this end development file that is expecting this locally running Strapi instance. So instead, we are going to update this to what we have deployed. So it's going to be this. We're going to remove this trailing slash because it cares a lot about that. We'll see the same thing on our production environment there. So I'm just going to do two at once so that we can keep this rolling. When you get a chance in between these, go ahead and just do what we did before. We're going to desktop, my node Congress, docs. And I'm going to... I'm still in the updates branch. Just going to run that same migration, but this time to the main environment. I actually think it's going to give me... Okay, we're good. So when you get a chance, go ahead and do the same thing. You can put this environment flag to make sure that you import to the right environment, but with the same import. Like I said, that gives you some protection so that you can test out imports ahead of time on a development environment. But it's a reflection of the way our model works. So every time you create a new development environment, you get all the same infrastructure and code on a development environment. And it comes with all of the data that happens to be in the production environment, but data doesn't flow up. Makes sense. I mean, we do a merge, the code and infrastructure moves up the chain to a parent environment, but not the data. So we test out that migration development and go to production. Go ahead, Shadrack, sorry. Yeah, sorry for interrupting. So I'm curious, what if I have a development environment that is more updated than my main environment and I try to merge them, right? What would platform automatically do the updates for me, or is there going to be a merge conflict? I'm sorry, can you say that again, man? So if I have updates, like you know how, you know, imagine different branches work, when there's a merge, it would when there's a conflict, it will do merge. So is there a situation where the development environment and main environment will have a merge conflict? I guess that could happen, in which case that's why it's always a good idea. When I do projects, typically I'll set up an integration to GitHub or GitLab because it'll give you better visibility of how to resolve those conflicts. But should you get into that situation, there is a part inside of the management console and that you can get through the CLI, which is called sync. So this gives you the ability to resync either just selectively data or resync code. So if you did something and you broke something and you want to, you know, restart your current environment, this is what it does, effectively pulls down to that environment. So you continue working. Yeah, exactly. Okay, so I went ahead and installed the dependencies on my front end app. I updated this development file, environment file on the front end. And I'm going to give you a look at what this final deployed app is going to look like. I'm going to run yarn dev within the client subdirectory after updating that environment file. Let me double check here. Yeah, my production app is okay. I'm going to go back to updates. And this case, my front end is here at port 3000. So I'll give it a second to build the pages from what I have on Strapi. So here's the app that I was talking about. So here's the app that I was talking about here. So it is pulling data from that api articles, api restaurants to build out a restaurant review site. This is Strapi's demo. I can go to the restaurants list page and go ahead and take a look at it. Let's go to Kinsey in San Francisco and see hours, information, total reviews, collection of pictures that are all part of our Strapi backend, and comments left by a bunch of users with an average rating for the restaurant. I also can check out the blog posts that are served up. Check out those individually, and this front end app will automatically take the content there to create individual pages for blog posts as well, according to the styles in the template. So that all looks good there. So I'm going to go ahead and shut down the server, and I'm going to platformize this. So one of the first things that we'll need to change here is we will have to tell PlatformSH how to direct traffic to this front end app once we actually deploy it. In this case, that will mean adding another set of routes pointing to a new application container. So we had this pair for the api subdomain that our local front end was just pulling from, pointing at the Strapi app, and now we'll use the route domain in the WW subdomain to define a new upstream to a new app called next.js. We're going to give it an ID, and that's going to be the extent of our new route's configuration. But we need one of these two. We need something that tells Platform how to build and deploy it. So I am going to go ahead again into my doc subdirectory and go to a file called file. And then in clients, I'm going to add a new file and include that. So once again, let's go through this. I have a name of the application. I have a version of node.js. We're going to restrict this to yarn. Once again, I'm going to install those dependencies. And in this case, we actually do have some more steps here. So let's go through those. During build, I am going to grant permissions to this script here in the build hook to run in my deploy hook. And what this is going to do in the same way that there was a config reader library that read from our environment variables to pull service credentials, there are other variables available there, like the platform routes, which is going to be important because we want the front-end app to dynamically detect the backend URL of Strapi so that it can use it in its own build. But that value is going to change in every environment that we're in. So I guess I can take a second to show this. Here's our updates environment. Any URL that's generated here is going to contain this platform environment, not quite a hash, but it's going to have a unique ID with the environment name. And it'll be the same for every application that we have inside this environment. Whereas if we go to our main environment, it'll be a little different. We'll have main here. So our backend URL is going to change depending on the environment we're in. And we want to leverage the fact that we can create as many environments we want to dynamically define the backend URL. So what this script is going to do is it's going to read our platform routes environment variable. And within there, it's going to have essentially what is actually generated and filled in for these default terms. And it's going to pull out the one with the ID api. And then it's going to write to a .m file that the next JS app is going to use, a public api URL, which is effectively going to be exactly what I put here. It's going to be the current environment backend minus this trailing slash. And then it's going to put a preview secret here. I'm not going to do much in preview secrets in this workshop, but it's going to set up this .m file. And then it's going to run that script in the deploy hook. Again, in the build hook, no other containers are available, but they are available in the deploy hook. So that's when we can actually run it and grab these values. Then we're going to build the application. There's also a start command, which in this case, we're going to start the next JS server. We're going to do a quick rebuild and start using the built-in variable port, because all app containers run on port 8888 on platform SH. And then, yeah, other than that, we're going to have a few settings that dictate how much memory is used inside of our deploy hook and start command. And that's going to give us enough space to actually pull all the data from our backend Strapy app and build the frontend. I believe that that's all the steps that we need to change here. I updated the roots YAML. I updated the app YAML file. Make sure I have it in the right place. Yep. And so we'll do another push there. So what we will see inside this environment, because this thing is moving now, is yet another application container show up in our cluster. So in this case, it'll be our frontend that's pulling data from the backend, that's pulling and storing data from our database. So every environment that we create from here on out is going to have the same configuration until we explicitly change the version of any of our service or app containers. So I'm going to go ahead and let this go for a second, and I'm going to mute because I think my wife is making smoothies in the background and it's probably very noisy. And then as soon as this is completed, we'll go ahead and check out what deployed. Yeah, I was wondering what that noise was. Yo, I didn't know next.js takes this long to build. Well, the thing is that because these backend and service containers aren't available at build time, we have to delay it to a stage where they are. And so there's a little bit of a handshake that needs to happen during our first migration step. This isn't how long it takes on push to push. It's just right here that it seems to have just a little bit of extra time to set up this frontend for the first time. What I may do is redeploy this guy. There we go. All right, so that finally caught up. And just because it took so long, I'm going to give it one redeploy to make sure that we're okay here. All right, and now it won't take this long from here on out. But we should be able to view our service tree. My mic is on, right? Our service tree that now contains both Strapi, next.js, and MariaDB container. We have a new URL here that has our frontend app. So now we have an isolated environment where our next.js frontend is pulling from the current instance of Strapi on this environment. I'm going to go ahead and merge so that we can get this up to production. And we'll see all the same stuff that we saw locally, even a little bit faster now that it's not restricted to our local instance. So here's that same Kinsey restaurant, all the reviews that go along with it. The blog for our backend, everything's looking good. Let's go back to the articles. I can filter by categories. I guess I don't have any American categories. Which ones do I have here? All right, I have one with the European category tag. I'm going to go back to restaurants. Let's see what my filtering looks like here. I want to look at Guatemala restaurants here. And I can view an individual restaurant that fits that criteria. And so this should be nearly done merging to production. Get us something nice and production ready instead of that SQLite database issue. Let's see what else we have on this app. I'm going to go back to the place. I'm going to buy European food. The mint lounge. Everything looks good. Move through all the different images. I haven't looked at this a lot to see if there's any image highlight feature on that slideshow. And this allows you to start repo documentation. So everybody following there have trouble getting to this last step here of actually deploying the multi-app environment. It's okay if you just wanted to watch now and then download this later and try it yourself. Just obviously be aware that there's something we need to fix on that SQLite stage. But it looks like everything to do with the production services and actually deploying this thing is looking good here. So once again, we're just going to need to do this first build to get it to production. And then we will be all set to start developing from here on out. Next is a pretty interesting front end. I've been doing it a lot more. When I started doing this to coupled stuff on platform, I was working a lot with gatsby and I only recently switched to next.js to prepare this demo app. But then also as a front end for Drupal, which has been pretty interesting, a new tool came out recently specifically for Drupal and next.js. But I've liked it. I don't want to say a lot more than gatsby, but it's been solving my multi-app deployment demos a lot better than that situation did. And it really gets stuck there on the first deploy. I got to figure out what's going on there. It looks like we're stuck on trying to start. Well, like I said, because of that first time where it's not available at all, it's essentially writing the back end URL to a symlink to EMV file. So the very first time it does it, that data is not available to even create that file. So it's essentially starting the server with an under-configured EMV file. And so it takes a second, it seems like, on the first push when migrating to fill out the file in time to actually make that connection. Mm-hmm. Mm-hmm. Mm-hmm. Mm-hmm. Yeah, it's still... Yeah. Let's just still give that a second to figure out what it's going to do with that container, I guess. But yeah, I would be curious to hear everybody else's deployment solutions here, because most of the time when I see a pattern like this, it's putting the front end on Vercell and deploying the back end elsewhere. And so what's interesting about our solution is that everything stays within the same repo so that you can create a development environment. And then whether you're somebody who's changing the back end api schema and you're trying to make sure that the front end is still compatible with what you changed it to, you can do that with full copies of both the front and back end in that environment or vice versa. If you're working on the front end, you can have your own development copy of that api application inside the development environment. There we go. All right, so we have our api. Let's go ahead and refresh. We've got our next.js front end. And then we should have... Great. We have our production environment, a decoupled architecture between an next.js and Strapi application. So now we're at this point free to add a domain to this production environment and continue on with our development. And so since we've completed the migration to production, since we have all of our infrastructure and code set up on production, if we go either through the CLI or through the management console, we can now come here and say... Let's say we want to add a shop to the front end application of some kind that doesn't specifically go through Strapi but uses something external like shopify. But we want to still have a copy of the api that doesn't necessarily conflict with anything going on production-wise. We now will be able to provision this development environment called add shop that will have the exact same infrastructure and all the same data that we migrated to production on this development environment. All of the restaurants, all the blog posts, any of the data that we might want at our disposal when we're writing tests for the changes in this isolated environment will be available to us there in that isolated environment. That is... Yeah, that's pretty much what I wanted to cover today. Hopefully, you guys were able to follow in on your pace or you check the recording later or check the repo for the instructions and set up the pattern yourself. One other thing that I did want to add is if you go to our... Get out of here. You go to our Platform SH templates organization on GitHub and search for Strapi. You will find actually a number of different templates so that are used the base Strapi 4 or Strapi 3 backend by itself. And then this one needs to be updated. But these two here are two separate kinds of front ends that are pulling data from the Strapi backend, just like I'm showing in this demo. Otherwise, obviously, you can get rid of your search and go to javascript and see quite a number of different ones that we have there. Koa Express. This was a really interesting one I got to work with, which was deploying a project that used a little bit of that SimLink logic that I showed in there to set environment variables. It does the same thing in here to actually handle that handshake that takes place when you register a GitHub app for the same time. From this template, you can set up a Probot GitHub app on a platform project that then will re-register from each development environment. Say you make a comment on pull requests GitHub app that you want to use within your organization, but you want to change some of its behavior. You can create a development environment and register a development version of that GitHub app that exists in isolation and check that new, excuse me, that new behavior for what you've changed there. Hopefully, this has been interesting and useful. We set aside time. We don't need to stay for the full three hours, but Shadrach and we're already planning on being available to help anybody either get to the point that I got to here or ask any questions about platform message or what Node is like on platform. Happy to stick around and answer any of those questions and really appreciate you guys all coming out. Yeah, thanks everybody for coming in, coming out for this workshop. If you need any help, please just let us know and we are available to, you know, if you have any specific question, you need to understand something, particular concept on platform message. Happy to answer your questions. Also, hold on, it's going to... Yes, here's how to do it. What's up? Yeah, I just want to talk about the community channel, the platform message community. So we have a community, they discuss if you have, if you're looking for any specific question, maybe you're going forward trying to use platform message. This community is, our community link is very, very useful, divided into various, from how to guides, to tutorials, questions and answers. You can come here and check it, you definitely must have found a way to answer your question on the community link. So please feel free to do that. Yeah, same goes for Slack. You can go to and join our Slack workspace. Shedrack, myself and the rest of our DevRel team will be there, along with, I mean, you could have a conversation with the CTO potentially inside of our public Slack channel. We're all there to help people get used to platform message, because I know it's a little maybe different than some other deployment platforms that you're used to. But if you are able to accept some of the few rules that I talked about of write access, being revoked post-build, handling things through environment variables, maybe a little extra time during the initial migration, you really get into a position where you can leverage these isolated identical development environments and do some pretty interesting work. And, you know, go into our repository here and say, well, I have a front end on node 14, and I have a strappy back end on node 12. But if I go to the documentation, and I go to the node.js section, I see we actually have support for 16. So I can go ahead and go into both of these app.yml files and say, let's try out strappy on 16. Push that, and you actually get everything completely identical to what we had on production with the one change of now it runs in a node 16 container. And I mean identical, I mean that node 16 container will inherit the relationship to the same database, to a copy of that database and will have that same data. It will also be smart enough to detect that I've already built that front end application on node 12. I have that commit hash associated with that build ID. So I can skip rebuilding the front end. I can just give it a refresh start command because we have a new version of the back end. And I can test, all right, let's test on node 16. Same for the front end. And just really easily provision infrastructure that way in these isolated environments. Well, I guess I'll go here, showed you the, yeah, these were the variations I was talking about. I had done before, so simple gatsby front end and then two other variations or three total on the multi-app pattern. In this case, gatsby pulling in from wordpress, CMS, Strapi and Drupal. So there's a lot of resources here to see the same pattern in our GitHub organization. Thanks, Irving. Yeah, we'll stick around for a bit. If you guys have any questions, like feel free to ask them. Chedrakh and I are definitely available to point you to some resources or, you know, heck, I was planning on being around. So if there's something else interesting that you want to demo of, I'm happy to do that. But yeah, a lot of resources around multi-app and node on the multi-app, I'm happy to do that. But yeah, a lot of resources around multi-app and node on platform. Thanks, Shane. Yeah, hopefully you guys were able to follow along. If not, check out the repo and do on your own time. I tried to be pretty detailed with the instructions. We'll take a look to see what was going on there with the SQL light step. But like I said, it wasn't the point of the workshop anyways. Oh, I wanted to check. Do you see that, Chedrakh? There's a error there on the pull-in that the date is invalid on these articles. Do you see this? Oh, I guess they're on the comments. Yeah, I can see it's checked. That's the review. Go to one of these. There's no field for dates, apparently. That's why. So my dump was good except for the create it at an updated date. That's pretty good, though. The last time I did this, I wasn't able to get a full dump of all the data prepared for MySQL. And I found, what was it? I don't know if any of you are listening come across this. I don't know if you do demos like we do. But the food advisor repo comes with that seed command to put together the SQL light database. But what I really wanted to show you was this production service of using MySQL. And I googled it a few dozen times trying to figure it out. And I found this one this week. So it's a Python package that I installed via pip. And so what I did was I installed it. And then one of the things that you can do when you start developing this locally, which is obviously the next step, is that you can update this database.js file to not be so strict on being actually on platform. And you can open a tunnel to your services. So what's my branch right now? It's still on updates. So I can do platform tunnel single, which will then give me access to the running service container on my development environment. So I obviously don't do this in production. And then where did I have my notes here? This one. No. It doesn't matter, I can just look it up in the documentation. So I can open a tunnel to the application. All right, so I can do this, so now that I have the tunnel open, I can open a... So now I have mocked environment variable for platform relationships, which I can decode the same way and pipe it through this jq library, which I'm leveraging inside the app container too, and get my credentials for the service container that's running live on my environment. And so then what I did is I used that open tunnel with the credentials to then run SQL 8.3, MySQL. I pointed at the data db file that came with the food advisor demo. I gave it the name, which in this case on platform the name is this path variable, the path credential, user, and port. It's still localhost, so I just need user, and then in this case when I open the tunnel I'm at 30,000 instead of 3306. And then the only other flag that I had to put was without foreign keys, and then I entered obviously the password that comes up in the credentials. And so that actually converted that SQLite database file. I just didn't even convert it to a dump, it just connected to my database and just uploaded it for me to the right expression so that it uploaded the whole thing. And so then for this demo we have... I really don't like this little thing in Zoom. Oh yeah. Yeah, it's not my favorite. Inside document... Go to services, and then we were using MySQL, and then I use our export command to then just dump what I had converted from SQLite to the live database container and then uploaded that to the repo so that you all could use it. That made me happy because that was definitely the last time I did a version of this workshop. I kind of had to use a shortened version of the data, but this helped get pretty close to the SQLite experience actually uploaded to the service container. That's not necessarily relevant, other than a cool tool I found recently that's interesting to any of you. Yeah, beyond any of that... What's that, Sugar? Yeah, so when you were updating the workshop, I was just wondering how you plan to migrate the SQLite to MySQL database. So it's really... That tool is quite useful. Yeah, it helped me out there. I said the last time I kind of manually pieced together like 20 rows of this gigantic SQLite database and gave up there, but this was able to convert it pretty well. And it looks like that's the only thing that didn't get converted was dates I don't know if it's dates everywhere. Let's go to blog posts. These have dates on them? No, I guess the only thing in here in this sample that has dates are the reviews. So that was the only thing that didn't come through, looks like. Tom, did you recommend any serverless DB? Shadrack, you might know better than me about that. Do you recommend any serverless DB service? Not exactly. I do not really know so much about serverless because I just played with like a bunch of just Netlify functions and threads, but I cannot honestly recommend any, to be honest, because I've not really used serverless a lot. I'm still on that whole server-based train, even though serverless has a server. Yeah, it's quite... I don't have any recommendations, but it's just funny how serverless actually runs on the server. Irving, what are you trying to use it for? Because yeah, I don't have a ton of experience with it either. I came in with mostly Python background and then learning PHP. No, no code. I think I've seen a lot of people using DynamoDB, I think. I've seen a lot of people using DynamoDB, but that's like the only time I've seen that. Of course, I mean no code. Yeah. Oh, yeah. I've seen Forna. Yeah. Irving, so yeah, we wouldn't consider it just hosting. I mean, part of what we're trying to provide is all of the orchestration that would go along with you rebuilding this workflow on actions or any other webhooks that you would need to attach to your repository that you're doing your development. Now that I've made this migration, I can copy this all over to GitHub and make an integration to the same project. I already have my association between a branch or a pull request equals a development environment, so I can make it run alongside all of my existing CI tasks, all of the tests that I have set up for individual environments. I can wait for a deployment to then do any visual regression or any other tests that have to do on the deployed environment. And I created this environment, add shop, when I was talking. And if I go here and I go to the camera's off again. So this was the production app that we just built. This was the environment we were building it on. After I finished all that and got it on a production, I created this branch. So now that I have this branch and can start doing work, like I said, changing the infrastructure, I get all of the build that I did before, but I also get all the data. This is a fully built out identical copy of the production environment. So it's hosting, but it's also this inheritance paradigm, I guess, like that I will get all of my production data on every development environment that I create. And I can tie it into my logic of Git that I'm already using. And that same inheritance happens for things like I can set, let's say for my production environment, I can set environment variables that are specific to this app. So let's say I needed some credentials to go to shopify that were production. I may not want those to be inherited by anything, but I could very well create a staging environment that I then make children off of in on that staging environment and give it its own shopify test credentials that will be inherited by every development environment as well, like built into our api. So those kinds of things, the inheritance, the abstraction of all the devops tasks just kind of beat Git with infrastructure as code that's managed. So you can't change anything of your infrastructure until you commit it, but you don't have to commit it down to the patch version. security updates are applied automatically. So we tend to consider it as more than just hosting. So if I go to my production environment, I can schedule or take individual backups of the environment, and I can actually use those backups to then sync to child environments. One of the interesting things on top of isn't it interesting that you can have a front end and back end in the same project is that this standard of a branch equals an environment and how inheritance works is it makes enforcing standards of how your team and developers work on a single project really easy and auditable security wise, but this api can be scaled out to as many applications as you want. So you could take this decoupled pattern of a front end and a back end that has a similar CMS back end, and you could make a SaaS product that then initializes as many on-demand projects for customers as you wanted to by the same project create initialized from this repo, and then you get these isolated projects that within them have isolated environments that, say, your in-house developers can make customizations and personalizations for whoever the customer may be, and establish this fleet of apps that have all the devops already cooked into it rather than you having to build them for scratch, just like for the single site case for 100 sites, and then you can do things like define commands to run yarn upgrade on every one of your applications in this upstream repo, and then just say there are my 100 sites, run yarn upgrade on all of them, and keep the upgrades in a development environment, run my tests. If any of them fail, they need inspection, but if they pass, then merge into production and keep your dependencies up to date that way. So it's a devops platform, I guess, is how we look at it with all those little features in there. Sure thing. Yeah. Please try out this demo. Come take a look at our website and join our Slack, because you'll find Shadrach and I both in there, and we're happy to talk about what it is you're deploying, unless you want to share it now, and then help you see how what you're working on might fit or run, at least, on platform. Awesome. Thanks, everybody. Do you know, Shadrach, does this, because I saw this environment variable, the one you said that was too much. Yeah. Where is it? Well, it's on both sides. Thanks, Nazeem. Thank you for coming. I have a preview secret on the Strapi side, and I'm also supposed to have a secret on the client side. It's the one I rewrote, because with Next, I'm supposed to be able to ping that front end and refresh what's been updated on the back end, right? Yeah. Well, I mean, let's give it a try. I doubt it'll work, but it could. Let's see. Article. These are my blogs, I guess. Wait, let's clean up all these tabs before I do it on a different environment. Let's go to Edge Shop. Here's my api, and here's my front end. Right. I'm building the same thing for a Drupal back end, and I really want this to figure this part out, because doing preview with a gatsby front end was not simple. Yeah, definitely. I think I've tried that too. Yeah, like I had to run a development server on non-production environments and set up all kinds of stuff, but from what I understand, Next doesn't work like that when you and I have talked. So I'm going to go to Articles, and let's see, is that Blog? Why are Chinese hampers? Okay. Let's go to that. Let's live preview. Let's see. So that just does this, and then I'm editing a published version. Okay. Hey, look at that. Yeah. Sweet. If any of you guys stuck around, that's already set up. I was hoping that it would be. Yeah, next.js is pretty sweet. Yeah, it was such a pain to try to do the same thing with a gatsby front end. It still is nice, but some different logic had to be done. Maybe I'll revisit it. Maybe that's just, I needed a little time to figure out the difference. Iframe. Iframe. You should check the workshop code. For what? You need to see what they did there. Hold on. For which part? For the live preview, how do we able to implement this? It looks like it's already implemented. You know, at least it'll do an update. I guess I didn't know, I wasn't familiar yet if in v4 strappy, I got like a preview URL set up. So I could view a draft version. Maybe that's something I got to do. This is for three. Preview. Is that what it is? Preview, secret, extensions. Okay, so it's just that I need to include something on the strappy index file. This is for v3. Maybe there's a better way now, but it looks like I need to add something. Or I didn't say this first. Oh, I have to add, this isn't in here. Oh, I have to make one for each collection that then gives me a preview button. I probably have to change how I handle drafts and published articles. Let's see, it's automatically changes your draft. Yep. So I'm assuming this is gone. Okay. So I'm assuming this is gone. Entering the draft version. Yeah, I'd be curious because that's what was interesting about the Drupal back-end version, is that there was sort of some, because Drupal already has like that built-in logic to give you a preview URL. And so it sort of leverages that to create, which it looks like, so does next, which is what Drupal is leveraging. Like it'll give you this. Where is it? Where'd it go? Gives you this api preview endpoint that you could look at like a draft copy, essentially. I'm going to have to look into that. Do you like that? It already updated. Yeah. Anybody still hanging around? Let me know if you want to see something. Otherwise, I'm just kind of exploring all the changes in Strapi v4. I haven't had a chance to look at yet. So thank you again for coming. Thank you everybody for coming. Thanks Gustavo. You're the last one, my friend. You got any questions or anything from us? I'm going to take your silence as a no. Thank you, Gustavo. Take care of yourself. Appreciate you coming. Again, feel free to reach out to us. We'll be at the rest of Node Congress for the next two days. So if you think of anything, go ahead and drop us a line inside the thread or ping us directly. And have a great rest of your day. All right. Thanks again.
134 min
15 Feb, 2022

Watch more workshops on topic

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career