Skip to main content

Overview

EpiNeko uses Supabase for authentication, database management, and Row Level Security (RLS). The integration provides separate client and server utilities for optimal performance and security.
Supabase utilities are located in src/utils/supabase/ with separate implementations for client-side, server-side, and middleware usage.

Environment Variables

Add these environment variables to your .env.local file:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
Both environment variables are required. The application will throw an error if they are missing.

Client-Side Usage

Use the client-side Supabase client for browser-based operations in Client Components.

Creating a Client

// src/utils/supabase/client.ts:3
import { createClient } from '@/utils/supabase/client';

const supabase = createClient();

Implementation Details

// src/utils/supabase/client.ts:1
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');
  }

  return createBrowserClient(
    supabaseUrl,
    supabaseKey
  )
}

Usage Examples

'use client';
import { createClient } from '@/utils/supabase/client';

export default function LoginForm() {
  const supabase = createClient();

  const handleLogin = async (email: string, password: string) => {
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });

    if (error) {
      console.error('Login error:', error.message);
      return;
    }

    console.log('Logged in user:', data.user);
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const formData = new FormData(e.currentTarget);
      handleLogin(
        formData.get('email') as string,
        formData.get('password') as string
      );
    }}>
      {/* form fields */}
    </form>
  );
}

Server-Side Usage

Use the server-side Supabase client for Server Components, Route Handlers, and Server Actions.

Creating a Server Client

// src/utils/supabase/server.ts:4
import { createClient } from '@/utils/supabase/server';

const supabase = await createClient();
The server client is an async function that must be awaited. It handles cookie management automatically.

Implementation Details

// src/utils/supabase/server.ts:1
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Ignore errors in Server Components
            // Middleware will handle session refresh
          }
        },
      },
    }
  )
}

Usage Examples

// app/dashboard/page.tsx
import { createClient } from '@/utils/supabase/server';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const supabase = await createClient();

  // Get authenticated user
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    redirect('/login');
  }

  // Fetch user's library
  const { data: library } = await supabase
    .from('user_library')
    .select('*')
    .order('updated_at', { ascending: false });

  return (
    <div>
      <h1>Welcome, {user.email}</h1>
      <LibraryList items={library} />
    </div>
  );
}

Middleware Integration

The middleware updates the user session on every request, ensuring authentication state is always fresh.

Implementation

// src/utils/supabase/middleware.ts:1
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) => 
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // Refresh the auth token
  await supabase.auth.getUser()

  return supabaseResponse
}

Using in middleware.ts

// middleware.ts
import { updateSession } from '@/utils/supabase/middleware'

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
The middleware automatically refreshes the user session on every request, keeping authentication state synchronized between client and server.

Authentication Patterns

Sign Up

const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'securepassword',
  options: {
    data: {
      full_name: 'John Doe',
      username: 'johndoe',
    }
  }
})

Sign In

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'securepassword',
})

Sign Out

const { error } = await supabase.auth.signOut()

Get Current User

const { data: { user } } = await supabase.auth.getUser()

Database Operations

Insert

const { data, error } = await supabase
  .from('user_library')
  .insert({
    anime_id_jikan: 5114,
    title: 'Fullmetal Alchemist: Brotherhood',
    status: 'watching',
    score: 9
  })
  .select()
  .single()

Select

// Select all
const { data, error } = await supabase
  .from('user_library')
  .select('*')

// Select with filters
const { data, error } = await supabase
  .from('user_library')
  .select('*')
  .eq('status', 'watching')
  .order('updated_at', { ascending: false })

Update

const { data, error } = await supabase
  .from('user_library')
  .update({ score: 10, status: 'completed' })
  .eq('anime_id_jikan', 5114)
  .select()
  .single()

Delete

const { error } = await supabase
  .from('user_library')
  .delete()
  .eq('anime_id_jikan', 5114)

Type Safety

Generate TypeScript types from your database schema:
npx supabase gen types typescript --project-id your-project-ref > src/types/database.types.ts
Then use them in your code:
import type { Database } from '@/types/database.types'

const supabase = createClient<Database>()

// Now all queries are fully typed
const { data } = await supabase
  .from('user_library')
  .select('*')
// data is typed as Database['public']['Tables']['user_library']['Row'][]

Best Practices

Common Issues

Error: Missing Supabase environment variablesEnsure both NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are set in your .env.local file.
Error: Cookie manipulation in Server ComponentsThis warning is expected and can be ignored. The middleware handles session refresh automatically.

Database Schema

View complete database schema

Row Level Security

Learn about RLS policies

Supabase Docs

Official Supabase documentation

Library Service

High-level library operations