So far, we've developed a Canvas component along with a LiveBlocks integration which accepts multiple participants. But there is a problem. So right now I'm demonstrating this with two different users both of them in the same organization. But watch this. I'm going to copy the URL of this workflow and I'm purposely going to switch my organization.
So as you can see, this organization doesn't have a single workflow. But since I know the URL, I'm just going to paste it here. And would you look at that? Even though I'm in a completely different organization, I still have access to this organization's workflow. Not only do I have access, but I can actually delete the nodes as well.
So what we need to focus on in this chapter is how to scope the workflow access both in our database Fetcher but also in the Liveblocks room provider so that only participants from certain organizations can access and modify our workflows and rooms. First, let's go ahead and protect our page.tsx which actually renders the LiveBlocks room and the workflow shell which eventually renders the canvas component. So inside of your dashboard workflows ID page.tsx. Right now the only thing we do is we extract the workflow ID from the params. That's the only thing we do.
So let's go ahead and add Auth from Clark Next.js server and not found from Next.js navigation. Then let's import getWorkflow function from our workflows data and then let's go ahead and extract the organization ID from out and throw the not found redirect if not available and then let's attempt to fetch the workflow using the currently scoped organization ID and the ID of the workflow we extracted from the params. If the workflow is not found, return not found. Go ahead and double check that you have the get workflow inside of your features workflows data.ts so get workflow first accepts organization ID and then the ID. If yours is the other way around that's perfectly fine, just make sure that you pass the props in the correct order.
So just by adding this change we should see a significant improvement. Go into one of your existing workflows and copy the URL. Then go ahead and create a new organization or just switch to some other one that you have. And what you're supposed to see now is this, workflow not found. The workflow you're looking for doesn't exist or may have been deleted.
So make sure that you try with refresh as well. There we go. So now unless you are inside of the proper organization, you will not be able to load that workflow. So that's one big problem solved. But somehow someone could still access the LiveBlocks room ID and manually render that room and render the canvas.
So we have to protect the live blocks room as well. You can find the documentation on how to add authentication to live blocks on the same page where we set up React flow using live blocks and Next.js. So here at the bottom, you can click setup authentication and add user information. Now this specific guide isn't really tailored to Next.js so what I would actually recommend is scrolling a bit down and finding the authentication guide for your framework Or you could have just clicked on authentication right here, but I just want to show you how I find my way around the docs page. And in here you can find Next.js.
So let's go ahead and first install this package and then let's add the secret key. Once you install the package, ensure that you have added the LiveBlocks secret key right beneath your LiveBlocks public key. Since this is a secret key, it doesn't need and it shouldn't have the next public prefix. So make sure that it's exactly as written here. Once again, if you are connected to the docs, or should I say if you're logged in while you're reading the docs, it will automatically populate your secret key with your project, but in case you are not, you can find inside of the API keys your secret key and just copy it here and then paste it.
Now we could follow the rest of the documentation manually, but remember that we have LiveBlocks best practices skill in which we actually have all of this information available and ready for our agents to consume instead. Let's go step by step and first create the Auth endpoint route. Set up an authentication endpoint in API live blocks Auth route.ts and make it ready for ID token permissions. Use clerk SDK for authentication utils. Use clerk's organization ID in group IDs for organization scoped access.
Only create the auth endpoint in route.ts. Do not perform any wiring or modification slash creation of other files. And make sure to include LiveBlock's best practices. What this should do is create a single file route.ts located in this exact folder structure. And this is the API route which was created.
First let's check the location. API live blocks auth route.ts. Perfect. Now inside we have auth and current user from clerk. We have live blocks from our new package.
We set up a live blocks client. We export an asynchronous post function. We extract the user ID and organization ID, throw if either of those doesn't exist, we extract the user information and then we use LiveBlocks identify user passing along the user ID which we extracted from Clerk and grouping or scoping this access to an organization ID. And then we populate the user info. Now, your agent could have done this differently.
For example, just full name would have been fine, But my agent decided to use a lot of fallbacks. So first it tries the full name, then it tries the username, then it tries the email address, and last it falls back to anonymous. Okay, let's leave it like that. And for the avatar, it simply uses image.url. And it returns a new response with the body and the status.
So this is the only file which should have been created and what's important is that it uses Liveblocks identify user and that it has these two parameters. One thing I don't like from this implementation is the fact that this LiveBlocks client is initialized in the file. I would rather that it has its own file inside of the lib folder. So I would like to have LiveBlocks.ts in here. So I'm gonna highlight these lines and in the same conversation, I'm going to tell it, extract the LiveBlocks client initialization in its own lib LiveBlocks.ts so it is reusable.
There we go. Now my API endpoint imports from the new lib live blocks and now I can reuse this file whenever I need to access the live blocks client. What we have to do now is we have to wire the new API endpoint into our room component. So right now we have LiveBlocks provider which uses the public API key, which means that the LiveBlocks room isn't actually verifying whoever joins this room. We need to connect it to the endpoint which we've just created to make sure that the user has, first of all, a user ID, meaning it's logged in, and second, that it has an organization ID so that we can start scoping users to a specific organization.
And the prompt for that would be the following. Wire the new API out endpoint and then tag the room TSX. Since I'm doing this in the same conversation, I don't have to add the LiveBlocks best practices skill, but if you're doing this in a new conversation, then make sure to include the LiveBlocks best practices. If you want to, you can add it here as well. I think it will know that it's already loaded and then it won't do it twice, but adding the scale won't hurt.
And what this should do is it should get rid of the prop public API key and replace it with the out endpoint and the out endpoint should point to API live blocks out which is the exact folder structure for our route.ts. Now This isn't everything we have to do and as you can see in the message that my agent sent me here is that this right now won't work because ID token out means that rooms are private by default. So they actually aren't joinable right now. What we need to do is we need to call getOrCreateRoom and we need to pass the group ID using Organization ID which has access to join that room. And we need to do this in a server component and the perfect place for that is our workflows page.tsx with which we started this whole chapter.
So the prompt we can use here is wire up get or create room with org ID access and then tag that page.tsx. Just make sure it's the correct page. And this should completely finish the wiring of Liveblocks out to our components. So you should now have the following function, Liveblocks get or create room. Pass in the ID from the params, then open the options, set the default accesses to an empty array, group accesses scoped to this organization ID and give it a permission of room right.
The metadata is optional, but if your agent passed anything here, it won't hurt. And I like how it created this inline because this isn't going to be reused anywhere. If yours created a separate util for that, It's fine, but you can tell it to inline directly inside of page.tsx. And it should have also added the LiveLocks import. That's why I wanted to extract that in the lib because I knew I was gonna reuse it in another server component here.
And as you can see, my agent just told me the full ID token flow is now wired end to end. So we have a room component, which now uses the out endpoint. Now that out endpoint identifies the user and scopes it under its organization ID and then whoever tries to visit the workflows ID attempts to call get or create room using their organization ID so we can check whether they are scoped to access this or not. So I'm going to show you the full six files which were modified so you can compare with yours. Starting with the package.json we have LiveBlocks node package added.
Then inside of our app API LiveBlocks out, we have the route.ts, which is responsible for authenticating the user, extracting the user information and running LiveBlocks identify user scoped to the organization ID. We then have an extracted LiveBlocks util in lib LiveBlocks. We have a room.tsx modification where we remove the usage of public API key and instead we use the out endpoint which points to our new endpoint. And last we have page.tsx which now protects the database access to a specific workflow scope to the organization id and we use our live blocks util get or create room for that workflow ID scoped with the correct group access for that organization. Now let's test it out.
What you should test first is two users who belong to the same organization and have access to the same workflows and then try and meet each other in one workflow. Both should be able to load this live blocks room and you should see each other's changes in real time. Now we already know what happens if I try to copy the URL and switch my organization. We get this error. Workflow not found.
But this isn't anything new. We know why this happens. It happens because of this block right here. But that isn't what we just spent a couple of minutes implementing. So let's actually hide this function and let's hide the metadata if you have any so you don't have errors.
Because what's gonna happen now is there is no longer a database protection but only the live blocks protection. So let's see if this group access actually works what happens now? So I purposely hid this block and now I'm gonna refresh in this correct organization where everything should still work fine but what happens in this other user? If I refresh, as you can see I no longer get that not found, but if I click on the error here, you will see that correctly Liveblocks has responded, you have no access to this room. That is because the auth endpoint has failed because this user is currently scoped to a different organization meaning that it shouldn't be able to load this Liveblocks room.
So our ID token actually works. Brilliant. And now we can of course revert these changes so we have both the database level protection and the Liveblocks cloud level protection using IDTokenAuth. So go ahead and commit all of these changes so we have a clean slate for the next feature we have to develop. Even though our current implementation works as expected, there is one problem when it comes to preserving the data in LiveBlocks about these rooms and everything inside of them.
As you can see, as we developed our Liveblocks implementation, at first we had some public rooms and then once we've implement ID, token access, we now have restricted rooms. But as you can see, still all of these rooms are restricted to the same organization ID, default. Even though their definitions sound similar, organizations or groups serve two different purposes. Groups are used to decide who has access to a room, whereas organizations are used to scope where Liveblocks data and resources should be stored. This isn't immediately obvious, but Liveblocks is also its own database.
And when you click on a room, you will see that they have several data tables here like comments, document, metadata, etc. If you go inside of document and select live blocks storage as the option, you will actually see React flow object inside. So that is why it's important to compartmentalize in the proper organization ID so that sensitive data isn't held in one shared pool with all other rooms in your database. Luckily, we can achieve this very easily in just a few lines of code. First go inside of your LiveBlocks Auth endpoint and when you identify the user also append the organization ID and then go inside of your page where you invoke get or create room and do the same here.
I would recommend going inside of your live blocks dashboard and actually deleting the existing rooms at this point just to ensure that no conflicts are created. Then once you have a clean slate, go ahead and visit your app at localhost 3000. Go ahead and click on any of the workflows or simply create a new one. Nothing much should change in the UI, but if you take a look at the LiveBlocks dashboard, you will now see that the resource is properly scoped to this organization. And if it says default here, just go ahead and give it a refresh and then it's going to load the actual organization to which this is scoped.
In the organization's documentation page, we can find the recommendation on how to refresh Liveblocks initialization whenever an organization needs to change. What they recommend is simply invoking location.reload. Even though I wasn't really able to encounter any stale information, as I can see everything works just fine. So the problem in theory should be if you refresh and load the initial page in this scenario, right? So no workflows.
And then switch to a proper organization and click on the one which we've created previously. In theory, sometimes it can happen that a stale organization ID gets access here. So if you want to prevent any chance of that happening, you can revisit your app sidebar component, find the organization switcher, and go ahead and give it the following props. What this is going to do is it's going to refresh the page every time an organization changes, an organization is left, or an organization is created. Meaning that it's going to invoke this location.reload, increasing the chances, or should I say reducing the change the chances of an organization ID being stale.
So now that all of the authentication and organization scoping logic is wired let's fix one more issue. When I switch between my workflows I can see that there are two different loading states. The first one is a spinner and then the second one is just a text loading. That is because we have two loading states. The first loading state is invoked from the page.tsx in our workflows ID page.
The loading state is invoked every time we await something. So that spinner first loads out, then it loads get workflow. And then once the room loads, we render the fallback. And that's where the loading text appears. So go ahead and change that fallback to render a spinner from components UI spinner.
So make sure you add this import and render this div which makes it full screen and centered. And that will essentially make it look the same as our loading component here. And then if you try, you will see that it kind of twitches, but it looks much better than the previous example we've had. Let's review these four modifications we've done. We've added organization ID to our live blocks out route when we identify the user.
We've done the same in the workflows ID page when we do get or create room. We've added these three props to the organization switcher and we modified the room component so that the fallback renders a spinner rather than the loading text. Remember that we also added a new environment variable key. So go ahead inside of your railway, click on the raw editor and add live blocks secret key update variables. You don't have to redeploy because now we're going to commit.
Actually you do have to click deploy to apply the change, my apologies. So click deploy anyway and then go ahead and stage, commit and push your changes.