Crossroad Development

two roads crossed to make an X

Supabase Auth Structuring

2023-07-03

Researching how user authentication is achieved in Supabase becomes vast quickly. One thing I will say before I start laying out the different paths you can take, which are dependent on the need at the time, is that every developer tool now has so much support and documentation for various frameworks and like one closed repo for the vanilla javascript implementation. I feel like the hype cycle I’m predisposed to avoids front-end frameworks because of performance and accessibility concerns. I might be salty, but I’ll stick to regular javascript for my demonstrations, but a lot of this post is going to be centered on the possibility of not being able to choose javascript at all.

When I got down to the nuts and bolts of it, I realized there was a “happy path” to building the architecture of your app with Supabase. Back to the idea of shipping less javascript to the client, I was thinking I could do a simple build with a premade template and do some of the Auth on the Node side and that way would only need a little function that POSTs the log-in info, etc. So the question becomes how are you going to architect your app, and how much is your time worth? There are about 2 scenarios I see playing out for most developers. You want Supabase to be practically your whole backend. That means you don’t need much more than row-level security on a database and all of the functionality is client side. A generally normal use case would be if you had a game and you just needed users to be able to look each other up, become friends and update leaderboards, nothing too dynamic and nothing that hooks into outside APIs. Though I think with Postgres functions you can actually do quite a lot without needing a backend server, I’m just more familiar with Node than Postgres at this point, which leads to the multiple different ways you can use a backend server in conjunction with either the supabase-js client library or the REST endpoints available on your project.

From what I understand the library is just a wrapper around these endpoints anyway, which comes up to about 19kb from the CDN but it caches. There are a large number of languages that the client library is provided for, but all of those have a way to make an HTTP request as well. Either way, you will want the client, even if it was on a mobile app or native desktop app to access the Supabase server for basically all the auth functionality, and then in turn pass their token to your backend server for verification. The documentation refers to this as a three-tier architecture. Now, once the server has the token parsed in from a POST request, it can process it one of three ways. Either you make an extra network hop to return the user status given a token, use the appropriate algorithm and JSON web token key from your Supabase project page to computationally verify the user, or use yet another dependency like Clerk, which kind of brings its own user system, but it also acts as a middleware if properly configured to hash the JWT. I used the basic jasonwebtoken package from npm, nested in a try-catch block (important because errors will otherwise crash the server), and it worked right away, so that’s the way I’m going to go with this project, no extra network hop, no extra complexity.

So, let me explain the project, and then I’ll step into using the vanilla javascript library version 1, and a node server just to show endpoint verification because, in a more complex project, you might need that to update private tables or use other server-side libraries in conjunction with Supabase. I’ve set up a few endpoints and picked up a dashboard template from Creative Tim to get rolling a little faster. We aren’t going to need the icon packs or any of the libraries that come with the template besides the base CSS and the bootstrap script, make sure the Supabase client is linked in the head of the pages as well. The goal for this example is going to be the obligatory todo app. Basically, after we have the sign-in and sign-up pages going, you can start adding users to your database. Then you have to think about how you will organize the table that will hold the to-do items for all users. Setting the security policies for this table is also quite important. I just added to the generated fields, title (text),uuid (text that is a foreign key linked to the unique user id in the auth table), content (text), and completed (bool). An important note, you’ll want the foreign key to cascade on delete that way if a user deletes their account, their data is automatically deleted, also I added a check so rows couldn’t be inserted with empty strings for title or content. Then I set my row-level security policies for each of the CRUD options. Create policy should have the target role authenticated, with the check expression true. The read path should also target authenticated, with the check expression (auth.uid() = uuid). That means it only shows rows where the uuid column matches the user’s uid. This will be useful with the other paths as well. The update path also targets authenticated with the (auth.uid() = uuid) expression being used in both check and using expressions. Finally, the delete path uses authenticated for the target and the same (auth.uid() = uuid) for the using expression.

Let’s focus on the auth flows for a minute. Every page that deals with Supabase needs the client library (until I can dive deep into the REST API) except the sign-up page because we’re going to validate the password strength on our node server since I can’t find a way to change the password policy directly in Supabase. Also, I’m going to collect a username field that we’re going to send to the auth table as metadata. I read in the documentation you can set up a Postgres trigger to add that metadata to a separate table, like a profile table. If you’re interested in that, you could just make a user profile input flow and pull any metadata from the initial sign-up to fill that profile table. The sign-up page should send those three parameters to the node server, where we can check that all the fields are populated (the required attribute in a form doesn’t protect against malicious use) and that the password security requirements are met. We do this with a regex and return corresponding error messages to the failure points. You could do this on the client side, and we later will, but in your application, there might be private tables that only interact with your server to perform the more secure functions. The link the user gets in their sign-up email with verify the user on Supabase and redirects to the URL set in the project settings of Supabse. You can also customize the message to the user or hook up to an SMTP mail server so the email comes from a domain related to your app. I would recommend it because the sign-up emails are going to the inbox, but password reset ones are getting outright blocked or thrown to spam in my testing. Then you build a log-in page that uses the auth.signIn method that takes a JSON object containing the email and password, then just change the location’s pathname member to the route for the main todo app. The final piece of auth for this demonstration is signing out, which I have found works best if you not only use the API method but also localStorage.clear() afterward to clear the token from the browser.

Now we should work with the meat of the application and all but one of the functions are going to be running on the client side. In fact, this whole project could be client-side code, but I sprinkled in an auth function to hit the server to test out the JWT verifier and redirect the user if there isn’t a valid user token available. Aside from that, we have an update function that retrieves all rows from the table, and because of our RLS policy, we only get results with our user id. Then it parses it into a table with buttons for removing, updating, and completing. This a good use of modularization that way we call the update function after every successful database call to get the most up-to-date information and render it in the table. The update function is passed the row id from the table so any changes can be applied to the correct row. Same format for the delete and complete functions. However, the update is unique in that it creates two new forms in place of previous data and collects the values from both when submitting changes. A lot of this is fine-tuning the front end, so this template really sped things up. Also, if you really want to grock this, it helps to dig around in the project, I’ve hosted it on Replit and Github. I think it’s a decent jumping-off point if you plug in your Supabase info and build a similar table. The Replit is nice because you can see it running and follow some of the flows without downloading it and running npm install, node index. Feel free to fork the project, just remember the template is MIT Licenced, you just need to include it in any way you publish the resulting software. And if you have any questions reach out.

Comments