Create a new nextjs app
npx create-next-app blue bird
Cleanup initial code
npm i @supabase/auth-helpers-nextjs @supabase/supabase-js
-
⚡️ Create a new supabase account
-
⚡️ Create a new project (
blue bird
) -
⚡️ Create a new table
tweets
(for properties referdatabase.types.ts
types) -
Connect to database by providing database url , anon (refer
.env.example
) -
Get tweets from database
Note
Initially we get [ ]
empty data as RLS ( Row Level Security)
in applied on tweets
table
-
⚡️ Create a policy to expose data to users/public
-
Create oAuth using github from settings/developer-settings/oauth in github
-
⚡️ Enable github oauth provider in supabase
-
Implement github oauth provider using supabase methods
-
Save session to cookies using middleware.ts
// `middleware.ts` at root level
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
const supabase = createMiddlewareClient<Database>({ req, res });
await supabase.auth.getUser();
return res;
}
-
Redirect to Login page if session is expired or session not found or not authenticated
-
Implement Authentication using login and protected routes by redirecting to login
-
refresh/refetch data on login/logout by
router.refresh()
on logout
Note
router
is return through @next/navigation
's useRouter
You can generate types of supabase table using cli
npx supabase login
Create lib
folder
npx supabase gen types typescript --project-id project-id-from-supabase > lib/database.types.ts
expose lib/database.types.ts
globally from global.d.ts
Important
Update types by running below command every type you modify( add columns , change type or null checks) tables in supabase
npx supabase gen types typescript --project-id project-id-from-supabase > lib/database.types.ts
Note
All login user information in stored in user
tables of auth
type automatically
whereas tweets
table is public type
- ⚡️ Create a foreign-key
user_id
column fortweets
- ⚡️ Link it with
id
column ofusers
table ofauth
type - ⚡️
cascade
the column - ⚡️ Make it
not null
Caution
It may give error when creating foreign key as not null as existing record don't have that value , add user_id
to existing records and edit column to make it not null
Now every tweet is linked to a user
If you see we still don't have a user friendly data of users and users
table private data , we cant use this directly.
So, we create a new table profiles
table
- ⚡️ With foreign key
id
as primary key and links toid
ofusers
table ofauth
type - ⚡️ Add
name
,username
andavatar_url
columns toprofiles
astext
andnot null
Now we have to insert user data to profiles
table every time a new user is created
-
⚡️ Create a new function
- give a name
- add definition
- in adv settings > security definer
create function public.create_profile_for_user() returns trigger language plpgsql security definer set search_path = public as $$ begin insert into public.profiles (id, name, username, avatar_url) values ( new.id, new.raw_user_meta_data->'name', new.raw_user_meta_data->'user_name', new.raw_user_meta_data->'avatar_url' ); return new; end; $$;
-
⚡️ Create a new trigger
create trigger on_auth_user_created after insert on auth.users for each row execute procedure public.create_profile_for_user();
Note
Don't forget to change tweets
table user_id
foreign key from auth.users.id
to profiles.id
Only authenticated users can create tweet
-
⚡️ Create a new policy for tweets table to insert tweet for authenticated user
-
Add a new tweet form component with input and form action
// form action
const addTweet = async (formData: FormData) => {
"use server";
const title = String(formData.get("title"));
const supabase = createServerActionClient<Database>({ cookies: cookies });
const {
data: { user },
} = await supabase.auth.getUser();
if (user) {
await supabase.from("tweets").insert({ title: title, user_id: user.id });
revalidatePath("/"); // refetch the data from db after insert
}
};
-
⚡️ Create a new table
likes
with the following columns -
⚡️ Columns: id(primary key), create_at (time stamp), tweet_id (foreign key links to
tweets
's id), user_id (foreign key links toprifiles
's id) -
⚡️ create new policies for likes
- Any one can read likes (public)(Select)
- Authenticated users can insert like (Insert)
- Authenticated users can delete like (Delete)
Tip
Run Supabase cli comman to update database types
-
Create a new component
Like
to display and add likes (client component) -
Get likes data by following code , as
likes
is linked totweets
table
const supabase = createServerComponentClient<Database>({ cookies });
const { data } = await supabase
.from("tweets")
.select("*, profiles(*), likes(*)");
-
Read no of likes as length of like property from data/tweets.
[!TIP] refer datatype of likes from
database
types for more clarification) -
To Insert likes add following like button handler
const toggleLike = async () => { const supabase = createClientComponentClient(); const { data: { user }, } = await supabase.auth.getUser(); if (user) { await supabase .from("likes") .insert({ user_id: user.id, tweet_id: tweet.id }); } ddd; // `tweet` is coming as prop from page which data property };
currently user can add as many likes as he can but can't unlike
- Add an extra property on tweets data
user_has_liked_tweet
const tweets =
data?.map((tweet) => ({
...tweet,
user_has_likes_tweet: !!tweet.likes.find(
(like) => like.user_id === session.user.id
),
likes: tweet.likes.length,
})) ?? [];
- Modify like button handler to toggle
insert
anddelete
record based onuser_has_liked_tweet
property
const toggleLike = async () => {
const supabase = createClientComponentClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (user) {
if (tweet.user_has_likes_tweet) {
await supabase.from("likes").delete().match({
user_id: user.id,
tweet_id: tweet?.id,
});
} else {
await supabase
.from("likes")
.insert({ user_id: user.id, tweet_id: tweet?.id });
}
}
};
Tip
We can rename properties from select, for example
const { data } = await supabase
.from("tweets")
.select("*, author:profiles(*), likes(user_id)");
// we can access profiles property from author
Create an intersection type for Like props
type Tweet = Database["public"]["Tables"]["tweets"]["Row"];
type Profile = Database["public"]["Tables"]["profiles"]["Row"];
type TweetWithAuthor = Tweet & {
author: Profile;
likes: number;
user_has_likes_tweet: boolean;
};
Now page file may contain type errors , resolve this by [!editing](#Like & Unlike)
const tweets =
data?.map((tweet) => ({
...tweet,
author: Array.isArray(tweet.author) ? tweet.author[0] : tweet.author,
user_has_likes_tweet: !!tweet.likes.find(
(like) => like.user_id === session.user.id
),
likes: tweet.likes.length,
})) ?? [];
- ⚡️ Enable database publications for tweets table
Tip
In Supabase goto Database
-> Publications
(previously Realtime
) -> in table click on tables in last column -> Enable for tweets
table
- Add this effect to
Tweets
component, to subscribe totweets
table
const router = useRouter();
const supabase = createClientComponentClient();
useEffect(() => {
const channel = supabase
.channel("realtime tweets")
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "tweets",
},
(payload) => {
router.refresh();
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
- Change
localhost
tolocation.origin
and force dynamic wherever usingcookies
from headers.
export const dynamic = "force-dynamic";
// localhost to domain
// "http://localhost:3000/auth/callback" → `${location.origin}/auth/callback`
- run
npm run build
to check successful build
Tip
If you get issue regarding location
, run
npm i -D encoding
- Change
localhost
todomain
after deployment in Supabase and github oAuth application