Overview
EpiNeko uses Supabase Auth for secure, scalable authentication. The system supports email/password authentication with a unique username-based login feature.
All authentication operations are handled through Next.js Server Actions, providing a seamless developer experience without the need for separate API routes.
Authentication Architecture
EpiNeko implements a multi-layered authentication system:
Supabase Client Setup
EpiNeko uses two different Supabase clients depending on the execution context:
Browser Client
Used in client components for client-side operations:
src/utils/supabase/client.ts
Usage in Client Component
import { createBrowserClient } from '@supabase/ssr'
export function createClient () {
const supabaseUrl = process . env . NEXT_PUBLIC_SUPABASE_URL ;
const supabaseKey = process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ;
if ( ! supabaseUrl || ! supabaseKey ) {
throw new Error ( 'Missing Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY' );
}
return createBrowserClient (
supabaseUrl ,
supabaseKey
)
}
Server Client
Used in server components and server actions for server-side operations:
src/utils/supabase/server.ts
Usage in Server Component
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient () {
const cookieStore = await cookies ()
const supabaseUrl = process . env . NEXT_PUBLIC_SUPABASE_URL ;
const supabaseKey = process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ;
if ( ! supabaseUrl || ! supabaseKey ) {
throw new Error ( 'Missing Supabase environment variables' );
}
return createServerClient (
supabaseUrl ,
supabaseKey ,
{
cookies: {
getAll () {
return cookieStore . getAll ()
},
setAll ( cookiesToSet ) {
try {
cookiesToSet . forEach (({ name , value , options }) =>
cookieStore . set ( name , value , options )
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
Always use the appropriate client for your context. Using the browser client in server components or vice versa will cause errors.
Authentication Flows
User Signup
The signup flow creates a new user account and automatically generates a profile:
src/app/login/actions.ts
Signup Form Example
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
export async function signup ( formData : FormData ) {
const supabase = await createClient ()
const email = formData . get ( 'email' ) as string
const password = formData . get ( 'password' ) as string
const username = formData . get ( 'username' ) as string
// Use the username from form or extract default from email
const finalUsername = username || email . split ( '@' )[ 0 ]
const { error } = await supabase . auth . signUp ({
email ,
password ,
options: {
data: {
full_name: finalUsername ,
username: finalUsername ,
}
}
})
if ( error ) {
console . error ( 'Signup error:' , error . message )
redirect ( '/error' )
}
revalidatePath ( '/' , 'layout' )
redirect ( '/' )
}
User metadata (username and full_name) is passed during signup and automatically used by the database trigger to create the profile.
User Login
EpiNeko supports login with both email and username:
src/app/login/actions.ts
Login Form Example
export async function login ( formData : FormData ) {
const supabase = await createClient ()
const identifier = formData . get ( 'identifier' ) as string
const password = formData . get ( 'password' ) as string
let email = identifier
// If the identifier is not an email, look up the associated email
if ( ! identifier . includes ( '@' )) {
const { data : profile } = await supabase
. from ( 'profiles' )
. select ( 'email' )
. eq ( 'username' , identifier )
. maybeSingle ()
if ( profile ?. email ) {
email = profile . email
}
}
const { error } = await supabase . auth . signInWithPassword ({
email ,
password ,
})
if ( error ) {
console . error ( 'Login error:' , error . message )
redirect ( '/error' )
}
revalidatePath ( '/' , 'layout' )
redirect ( '/' )
}
User enters identifier
The user provides either an email or username along with their password
Identifier validation
The system checks if the identifier contains ’@’ to determine if it’s an email
Username lookup
If it’s a username, the system queries the profiles table to find the associated email
Authentication
Supabase Auth validates the email and password combination
Session creation
On success, a session is created and cookies are set
User Logout
Simple logout functionality that clears the session:
src/app/login/actions.ts
Logout Button Example
export async function signOut () {
const supabase = await createClient ()
await supabase . auth . signOut ()
revalidatePath ( '/' , 'layout' )
redirect ( '/' )
}
Session Management
Middleware
EpiNeko uses Next.js middleware to automatically refresh user sessions on every request:
src/middleware.ts
src/utils/supabase/middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware ( request : NextRequest ) {
return await updateSession ( request )
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.* \\ .(?:svg|png|jpg|jpeg|gif|webp)$).*)' ,
],
}
The middleware automatically refreshes the authentication token on every request, ensuring users stay logged in without manual intervention.
Profile Management
Automatic Profile Creation
Profiles are automatically created when a user signs up through a database trigger:
supabase/migrations/20260218_initial_schema.sql
-- Profiles Table
create table public .profiles (
id uuid references auth . users on delete cascade not null primary key ,
updated_at timestamp with time zone ,
username text unique ,
full_name text ,
avatar_url text ,
website text ,
constraint username_length check (char_length(username) >= 3 )
);
-- Trigger function
create or replace function public .handle_new_user()
returns trigger as $$
begin
insert into public . profiles (id, full_name, username, avatar_url)
values (
new . id ,
new . raw_user_meta_data ->> 'full_name' ,
new . raw_user_meta_data ->> 'username' ,
new . raw_user_meta_data ->> 'avatar_url'
);
return new;
end ;
$$ language plpgsql security definer;
-- Trigger
create trigger on_auth_user_created
after insert on auth . users
for each row execute procedure public . handle_new_user ();
Row Level Security (RLS)
Profiles are protected with Row Level Security policies:
-- Enable RLS
alter table public . profiles enable row level security ;
-- Public profiles are viewable by everyone
create policy "Public profiles are viewable by everyone." on public . profiles
for select using (true);
-- Users can insert their own profile
create policy "Users can insert their own profile." on public . profiles
for insert with check ( auth . uid () = id);
-- Users can update own profile
create policy "Users can update own profile." on public . profiles
for update using ( auth . uid () = id);
RLS policies ensure that users can only modify their own profiles while still allowing public viewing of all profiles.
Getting the Current User
Fetch the authenticated user in server components:
Server Component
Server Action
import { createClient } from '@/utils/supabase/server'
export default async function ProtectedPage () {
const supabase = await createClient ()
const { data : { user } } = await supabase . auth . getUser ()
if ( ! user ) {
redirect ( '/login' )
}
return < div > Welcome , {user. email } </ div >
}
Protecting Routes
Create protected routes by checking authentication status:
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
export default async function ProtectedPage () {
const supabase = await createClient ()
const { data : { user } } = await supabase . auth . getUser ()
if ( ! user ) {
redirect ( '/login' )
}
// Protected content
return < div > This is a protected page </ div >
}
Always validate authentication on the server side, never rely solely on client-side checks for security.
Customization
Adding Social Authentication
To add OAuth providers (Google, GitHub, etc.):
Enable the provider in Supabase Dashboard (Authentication > Providers)
Add the OAuth redirect URL to your provider’s app settings
Update your login page with social buttons:
export async function signInWithGoogle () {
const supabase = await createClient ()
const { error } = await supabase . auth . signInWithOAuth ({
provider: 'google' ,
options: {
redirectTo: ` ${ location . origin } /auth/callback` ,
},
})
}
Email Verification
Enable email verification in Supabase Dashboard (Authentication > Email Templates):
const { error } = await supabase . auth . signUp ({
email ,
password ,
options: {
emailRedirectTo: ` ${ location . origin } /auth/callback` ,
}
})
Password Recovery
Implement password reset:
export async function resetPassword ( email : string ) {
const supabase = createClient ()
const { error } = await supabase . auth . resetPasswordForEmail ( email , {
redirectTo: ` ${ location . origin } /auth/reset-password` ,
})
}
Troubleshooting
Ensure middleware is properly configured and running on all routes. Check that cookies are being set correctly.
Username login not working
Verify that the username is stored in the profiles table and is properly indexed. Check that the email column is added to profiles.
Profile not created on signup
Check that the database trigger is installed correctly. Verify that user metadata is being passed during signup.