No history yet

Project Setup

Initializing the Project

We'll start by scaffolding a new Next.js 14 project. This command sets up a modern, production-ready foundation using the App Router, TypeScript, and Tailwind CSS right out of the box.

npx create-next-app@latest my-ai-app

When prompted during the installation, make sure to select Yes for using TypeScript, ESLint, and Tailwind CSS. The App Router will be the default, which is exactly what we need for this architecture.

Setting Up the Backend

For our backend, we'll use Supabase. It provides a PostgreSQL database, authentication, and auto-generated APIs, which lets us move quickly. First, create a new project in your Supabase dashboard.

Once your project is ready, navigate to the API settings (Project Settings > API). You'll need two pieces of information: the Project URL and the anon public key. These are your application's credentials to communicate with Supabase.

Never commit secret keys or credentials directly into your code. We'll use environment variables to keep them secure.

Create a file named .env.local at the root of your Next.js project. This file is ignored by Git and is the standard place for storing environment variables locally.

# .env.local
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_PROJECT_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

The NEXT_PUBLIC_ prefix is a Next.js convention. It exposes these variables to the browser, which is necessary for the client-side Supabase client. Any variable without this prefix is only accessible on the server, providing an extra layer of security.

Next, install the Supabase libraries. We'll use @supabase/ssr, a helper library specifically designed to manage authentication state across the different rendering environments of the Next.js App Router.

npm install @supabase/supabase-js @supabase/ssr

Configuring Universal Clients

The key challenge in a modern framework like Next.js is managing user sessions seamlessly between the server and the client. The @supabase/ssr package simplifies this by handling cookie-based authentication. We'll create a few utility files to instantiate the Supabase client wherever we need it.

First, create a utils/supabase directory in your project. Inside, we'll make three files: one for the client, one for the server, and one for middleware.

A client-side client is needed for interactive components that run in the user's browser.

Create utils/supabase/client.ts for use in Client Components. This function initializes a singleton instance of the Supabase client for the browser.

// utils/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

A server-side client is required for Server Components, Route Handlers, and Server Actions.

Now, create utils/supabase/server.ts. This version is more complex because it needs to read and write cookies to persist the user's session across server requests. The component that uses this client provides the cookie store.

// utils/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'

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

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options })
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: '', ...options })
          } catch (error) {
            // The `delete` method was called from a Server Component.
          }
        },
      },
    }
  )
}

Finally, we need middleware to refresh the user's session cookie. Middleware runs before a request is completed, making it the perfect place to ensure the auth state is current. Create middleware.ts in the root of your project.

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

export async function middleware(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value
        },
        set(name: string, value: string, options) {
          request.cookies.set({
            name, value, ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name, value, ...options,
          })
        },
        remove(name: string, options) {
          request.cookies.set({
            name, value: '', ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name, value: '', ...options,
          })
        },
      },
    }
  )

  await supabase.auth.getUser()

  return response
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

With these three files, we now have a robust system for interacting with Supabase from anywhere in our Next.js application, with authentication state correctly synchronized.

UI with Shadcn

For the user interface, we'll use Shadcn UI. It's not a component library in the traditional sense. Instead, it's a collection of reusable components that you copy into your own project, giving you full control over their code, styling, and behavior.

To initialize Shadcn UI, run the following command:

npx shadcn-ui@latest init

This will guide you through a configuration process. It will detect that you're using TypeScript and Tailwind CSS. It will ask a few questions to set up your components.json file, which manages component dependencies and paths. Accepting the defaults is a great starting point.

Now, adding a new component is a one-line command. For example, to add a button component:

npx shadcn-ui@latest add button

This command adds the button.tsx file to your components/ui directory. You can now import it and use it like any other React component, and you're free to modify the source file to match your project's specific needs.

Our project is now fully configured with a modern frontend, a powerful backend, and a scalable UI system. We're ready to start building features.