Securely Mutate Supabase Data with Remix Actions

Jon Meyers
InstructorJon Meyers
Share this video with your friends

Social Share Links

Send Tweet
Published a year ago
Updated a year ago

To mutate data in Remix, we use an Action function. In this lesson, we look at creating a Remix <Form /> to submit the content of a message as a post request to our Action function.

Additionally, we use the server-side Supabase client from the Remix Auth Helpers package to make an authenticated request to Supabase, writing the new message to the messages table.

Futhermore, we run into an issue with RLS as we have not yet written a policy for the insert action. By setting this to the authenticated role and ensuring that the user_id column is equal to the value returned by the auth.uid() function - a function we get from Supabase to retrieve the ID for the user attempting to insert a new row - we ensure that users can not write a message on someone else's behalf.

Lastly, we use the auth.uid() function to set the default value of the user_id column to the ID of the user attempting to insert the new row in the messages table.

Code Snippets

Write data to Supabase

export const action = async ({ request }: ActionArgs) => {
  const response = new Response();
  const supabase = createServerSupabase({ request, response });

  const { message } = Object.fromEntries(await request.formData());

  await supabase.from("messages").insert({ content: String(message) });

  return json(null, { headers: response.headers });

Form to submit to action

<Form method="post">
  <input type="text" name="message" />
  <button type="submit">Send</button>

Add policy for insert

create policy "users can insert their own messages" on "public"."messages"
as permissive for insert
to authenticated
wuth check (user_id = auth.uid());

SQL code snippets can be run against your Supabase database by heading over to your project's SQL Editor, pasting them into a new query, and clicking RUN.


Instructor: [0:00] Let's add the ability for our users to send a message. Back over in our routes index.tsx file, we want to render out a form with a capital F. This one comes in from @remix-run/react. The method for this form is going to be post and inside our form, we want an input box which is of type text. [0:19] We'll give it the name of message and then add a button with the type of submit and the text send. The method of this form being anything other than Git means that Remix will try to submit this form to an action for this route. And so, we'll declare that above our loader here with the same boilerplate for creating a server side Supabase client.

[0:44] We take in the request from our action_args and we can fix up the import to come in from @remix-run/node. We then create an empty response and then use that to create our server Supabase client, and then send a response from our action with those Supabase headers set.

[0:58] We can then call Object.fromEntries and pass it a call to await our request.formData, which will give us a [?] bit object with key-value pairs for all of our different form inputs. We can then destructure our message. Then, await a call to supabase, telling it from the messages table, we want to insert a new row, setting the content column to be the value of our message.

[1:27] We just need to wrap that in a call to String to ensure we are getting a string value for our message. TypeScript is not happy here because it's expecting us to also pass across a user_id. We're going to fix this in Supabase itself in a moment so we can ignore this one.

[1:43] If something goes wrong while we're trying to insert this value, we'll get back an error. If we have an error, then we just wanted to console.log it out. We could work out who the currently signed in user is and pass this along with our request to insert a new message.

[1:58] Since Supabase already manages auth and therefore knows who this user is, we can actually implement this in Supabase itself. If we come over to the Table editor and look at our messages table, we can edit our user_id column. If we scroll down, we can set the default value to be auth.uid().

[2:16] This is a special function that we get in Supabase that will return us the currently signed in user or in this case, the user who is currently trying to insert a new record. We can now click Save to update our column.

[2:27] If we go back to VS Code, we'll still be seeing that type error. That's because we need to regenerate our types. If we quit our currently running server and then run our command to regenerate our types, we can then start our development server again. If we restart our TypeScript server, we should see that TypeScript is happy again.

[2:44] However, if we go back to the browser and actually try to insert a message. Let's say, new message, and then open up our console and go over to the network tab. Before we click send, when we try to write this new message to Supabase, we're getting a 200 here. If we refresh the page, we're not actually going to see that new message.

[3:03] If we have a look at our server console, we're going to see this error here. New row violates row-level security policy for table messages. This is because in Supabase, we've enabled row-level security for our messages table. We've written a policy for select, but all insert update and delete requests are automatically being denied.

[3:23] Let's create a new policy for insert. The name is going to be users can insert their own messages. We want to enable the insert action. The target roles are again going to be authenticated. Gives a message relies on there being a signed in user.

[3:39] Now, for our with check expression, we want to make sure that the user ID column in the messages table is equal to whatever we get back from auth.uid function, which again, gives us the currently signed in user, trying to perform this action. This just means that a user won't be able to send a message as someone else.

[3:58] Let's click review and then save policy to run that SQL on our database. Now, if we go back to our application and try to send that message again, we'll see it appears at the bottom here. When we refresh, this information is persisted in the database which we can also confirm by going to our table editor and then looking at our messages.

[4:16] We can see that new row has been successfully written to the database with the currently signed in user attached.

ed leach
ed leach
~ 9 months ago

Jon, when I try to set the default value of messages - user_id to auth.uid() or (auth.uid()) I get an error: failed to update pg.columns with the given ID: must be owner of event trigger pgsodium_trg_mask_update

Markdown supported.
Become a member to join the discussionEnroll Today