Build a Realtime Chat App with Remix and Supabase
The following are notes from an Egghead course I took.
Create a Supabase Project with a Table and Example Data
- Created a new database on supabase called Chatter
- In the table editor, create a new table
messages
, and add columns forid
,created_at
, andcontent
.id
should be a uuidcreated_at
should defaultnow()
and never be nullcontent
is text and should never be null
Setting Up a Remix Project
- Create a new remix project
- Choose âJust the basicsâ
- Choose Vercel as the service
npx create-remix chatter
- For the remix project, you can find the main file in
index.tsx
Query Supabase Data with Remix Loaders
npm i @supabase/supabase-js
- Add supabase env vars to .env, which can be found in the Project Settings > API. Link
SUPABASE_URL={url}
SUPABASE_ANON_KEY={anon_key}
- Create a
utils/supabase.ts
file. CreatecreateClient
function - A â!â can be used at the end of a variable so typescript doesnât give us errors, if we know those will be available at runtime, like env vars
- Supabase has row-level security enabled, meaning you have to write policies in order for the user to do CRUD operations (
SELECT
,INSERT
,UPDATE
,DELETE
, andALL
).- We added a policy to allow all users to read.
- Create the loader in the index page, using
import { useLoaderData } from "@remix-run/react";
, which will allow us to query supabase using the utils.supabase.from("messages").select()
reminds me a lot like mongodbâs client.
Generate TypeScript Type Definitions with the Supabase CLI
- Add Type Safety checks using the Supabase CLI
brew install supabase/tap/supabase
# Or call upgrade if you already have it
brew upgrade supabase
- Create an account token
- Then login to CLI tool using that access token
supabase login
- Generate types based our project for Typescript
supabase gen types typescript --project-id akhdfxiwrelzhhhofvly > db_types.ts
- We have to re-run this command every time we have DB updates
- Now we use the
db_types.ts
into oursupabase.ts
file by adding a type to thecreateClient
function - You can infer types by using
typeof
in Typescript. This is useful for showcasing whatdata
âs type is in theIndex
functional component. - To make sure the data is always present, or an empty array rather than of type
null
, we use a null coalescing operator on the original datareturn { messages: data ?? [] };
Implement Authentication for Supabase with OAuth and Github
- Enable Github OAuth using Supabase
- In the supabase project, go to Authentication > Providers
- Choose Github
- In Github, go to Settings, Developer Settings > OAuth Apps
- Create âChatterâ. Copy the Authorization callback URL
- In supabase, enter the Client ID, Client Secret, and the Redirect URL.
- The generated secret in Github goes away after a few minutes, so be quick
- Create the login component in
components/login
and then add two buttons for logging in and out.- The handlers should be
supabase.auth.signInWithOAuth
andsupabase.auth.signOut
- The handlers should be
- Add the login component back into the index component.
- Youâll notice a
ReferenceError
in thatprocess
is not defined because that should only run on the server. - Change the
supabase.ts
file tosupabase.server.ts
file. This shows that the supabase file should only be rendered on the server. - The
root.tsx
component has anOutlet
depending on the route based off theroutes
files (file-based routing)
- Youâll notice a
- In the root component, we add the context in
Outlet
for the supabase instance.- This can now be used in the
login
file usinguseOutletContext
. - Types can be added by exporting it from root.
type TypedSupabaseClient = SupabaseClient<Database>;
- This can now be used in the
- supabase uses Local Storage to store the OAuth tokens.
- You can also check the users in the supabase project
Restrict Access to the Messages Table in a Database with Row Level Security (RLS) Policies
- Add a column to our database called
user_id
and add a foreign key to it, withusers
and the key beingid
. - Disable Allow Nullable by adding the logged in user id to the first two messages. This can be found in the users table.
- Re-run the db_types script
supabase gen types typescript --project-id akhdfxiwrelzhhhofvly > db_types.ts
- Update the policy by changing the target roles to be authenticated.
- Now only signed in users will be able to view the data.
Make Cookies the User Session Single Source of Truth with Supabase Auth Helpers
- Auth tokens by default are stored in the clientâs session, not on the server.
- Remix is loading from the serverâs session, which is null
npm i @supabase/auth-helpers-remix
- We need to change the mechanism for the token to use cookies
- Auth helpers allows us to use
createServerClient
andcreateBrowserClient
to create the supabase instance correctly, based if itâs on the client or server.- You need
request
andresponse
added in thesupabase.server.ts
- We need to do the same thing in the loader in
root
andindex
- You need
- Auth helpers allows us to use
Keep Data in Sync with Mutations Using Active Remix Loader Functions
- Thereâs no update for pressing the button because the client doesnât update the information after the initial load.
- Remix has a revalidation hook.
- Supabase has a auth state change hook
- Combining these together, on server and client token change (either new token, or no longer has the token), then refetch data from loaders.
Securely Mutate Supabase Data with Remix Actions
- To create a new message, we add
Form
from remix, which has a methodpost
.- This is reminiscent of how forms worked alongside the HTML spec before
- An action is created to insert the message, include the response headers from before (passing along the cookie)
- The message wonât send yet until the supabase policy is set, so we add a policy for
INSERT
and make sure the user is authenticated and their user_id matches the one in supabase.
Subscribe to Database Changes with Supabase Realtime
- Supabase sends out updates via websockets when there is a change to the database
- Itâs called Replication
- We create a new component,
RealtimeMessages
that can subscribe to thoseINSERT
changes on all tables- We set messages in a
useState
hook, and any changes we will change them with auseEffect
- A second
useEffect
subscribes to these supabase changes
- We set messages in a
const channel = supabase
.channel("*")
.on(
"postgres_changes",
{ event: "INSERT", schema: "public", table: "messages" },
(payload) => {
const newMessage = payload.new as Message;
if (!messages.find((message) => message.id === newMessage.id)) {
setMessages([...messages, newMessage]);
}
}
)
.subscribe();
Deploy a Remix Application to Vercel from a GitHub Repository
- Create a Github repo
- Thereâs a Github CLI tool,
gh
, that can handle this gh repo create chatter --public
- Thereâs a Github CLI tool,
- Init the repo, commit, and push
- Go to Vercel, add the project and env variables
- Go to Github and add the redirect URL
- Go to Supabase and add authentication url redirect
- Final URL
Written by Jeremy Wong and published on .