DEV Community

jesus manrique
jesus manrique

Posted on • Originally published at guayoyo.tech

Spec-Driven Development with Claude Code: Auth & Data with Supabase — Part 2 of 3

Series: Spec-Driven Development with Claude Code — Part 1 · Part 2 · Part 3



In Part 1, we set up our project structure but left it without logic. Today we implement authentication and data — the two layers that turn a wireframe into a real app.

The pattern stays the same: spec first, code second.

Step 1: Set Up Supabase

Before touching any code, we configure the backend.

  1. Go to supabase.com and create a new project
  2. Once created, go to Project Settings → API and copy:
    • URL
    • anon public key
  3. Paste them into your .env:
SUPABASE_URL=https://yourproject.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJI...
Enter fullscreen mode Exit fullscreen mode
  1. In the Supabase dashboard, go to Authentication → Providers and make sure Email is enabled (disable email confirmation for development).

Step 2: The Authentication Spec

We create a dedicated spec for the auth module. File specs/auth.md:

# Spec: Authentication Module

## Dependencies
- @supabase/supabase-js (client)

## Composable: useAuth

### State
- `user: Ref<User | null>` — current user
- `loading: Ref<boolean>` — initial load state
- `error: Ref<string | null>` — error message

### Methods
- `signUp(email, password): Promise<{ user, error }>`
- `signIn(email, password): Promise<{ user, error }>`
- `signOut(): Promise<void>`
- `initAuth(): Promise<void>` — restores session on load

### Behavior
- initAuth() is called once via a Nuxt plugin
- signUp/signIn validate fields before calling Supabase
- All Supabase errors are translated to user-friendly messages
- signOut clears local state + Supabase session
Enter fullscreen mode Exit fullscreen mode

Now we ask Claude Code to implement:

> Read @specs/auth.md and @specs/catalogo-guayoyo.md. Implement the useAuth composable in composables/useAuth.ts exactly following the spec. Also create the Nuxt plugin to initialize the session in plugins/supabase.client.ts.
Enter fullscreen mode Exit fullscreen mode

Claude Code generates:

// composables/useAuth.ts
import type { User, AuthError } from '@supabase/supabase-js'

const user = ref<User | null>(null)
const loading = ref(true)
const error = ref<string | null>(null)

export function useAuth() {
  const client = useSupabaseClient()

  function translateError(err: AuthError): string {
    const map: Record<string, string> = {
      'Invalid login credentials': 'Invalid email or password',
      'User already registered': 'This email is already registered',
      'Password should be at least 6 characters': 'Password must be at least 6 characters',
    }
    return map[err.message] ?? err.message
  }

  async function signUp(email: string, password: string) {
    error.value = null
    const { data, error: err } = await client.auth.signUp({ email, password })
    if (err) {
      error.value = translateError(err)
      return { user: null, error: error.value }
    }
    user.value = data.user
    return { user: data.user, error: null }
  }

  async function signIn(email: string, password: string) {
    error.value = null
    const { data, error: err } = await client.auth.signInWithPassword({ email, password })
    if (err) {
      error.value = translateError(err)
      return { user: null, error: error.value }
    }
    user.value = data.user
    return { user: data.user, error: null }
  }

  async function signOut() {
    await client.auth.signOut()
    user.value = null
  }

  async function initAuth() {
    loading.value = true
    const { data: { session } } = await client.auth.getSession()
    user.value = session?.user ?? null
    loading.value = false
  }

  return { user, loading, error, signUp, signIn, signOut, initAuth }
}
Enter fullscreen mode Exit fullscreen mode

Notice: the code follows the spec exactly. Error translation, exact method signatures, correct types. Nothing "almost."

Step 3: Implement Auth Pages

We ask Claude to create the forms:

> Read @specs/catalogo-guayoyo.md (US-1 and US-2). Implement /login and /register with validated forms, error messages, and redirect to /products. Use useAuth from the previous step. Tailwind for styling. No logic in the template — everything in <script setup>.
Enter fullscreen mode Exit fullscreen mode

Claude generates pages/login.vue:

<script setup lang="ts">
definePageMeta({ layout: 'auth' })

const { signIn, error, user } = useAuth()
const router = useRouter()

const email = ref('')
const password = ref('')
const submitting = ref(false)

// Redirect if already authenticated
if (user.value) {
  await router.replace('/products')
}

async function handleSubmit() {
  submitting.value = true
  const { error: err } = await signIn(email.value, password.value)
  if (!err) {
    await router.push('/products')
  }
  submitting.value = false
}
</script>
Enter fullscreen mode Exit fullscreen mode

And pages/register.vue with similar logic, validating password ≥ 8 characters and match.

⚠️ Key SDD insight: The spec said exactly what to validate and how to behave. Claude Code didn't have to "guess" anything. The result works on the first try.

Step 4: Create the Products Database

In the Supabase Dashboard, go to SQL Editor and run:

-- Create products table
CREATE TABLE products (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  price NUMERIC(10,2) NOT NULL CHECK (price > 0),
  category TEXT NOT NULL,
  image_url TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Insert sample data
INSERT INTO products (name, price, category, image_url) VALUES
  ('Premium Guayoyo Coffee', 12.99, 'Beverages', 'https://images.unsplash.com/photo-1559056199-641a0ac8b33e?w=400'),
  ('Electric Arepa Maker', 34.50, 'Appliances', 'https://images.unsplash.com/photo-1585937421612-70a008356fbe?w=400'),
  ('"Shipping Code" T-Shirt', 24.99, 'Clothing', 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400'),
  ('RGB Mechanical Keyboard', 89.99, 'Technology', 'https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=400'),
  ('"No Friday Deploys" Mug', 15.99, 'Accessories', 'https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=400'),
  ('Braided USB-C Cable 2m', 9.99, 'Technology', 'https://images.unsplash.com/photo-1610397704400-33e5573b3a2a?w=400');

-- Enable Row Level Security
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Policy: authenticated users can read products
CREATE POLICY "Authenticated users can read products"
ON products FOR SELECT
TO authenticated
USING (true);
Enter fullscreen mode Exit fullscreen mode

Run this SQL and your database is ready in 30 seconds.

Step 5: Data Access Spec + Implementation

New spec specs/products.md:

# Spec: Products Module

## Composable: useProducts

### State
- `products: Ref<Product[]>` — product list
- `loading: Ref<boolean>` — loading state
- `error: Ref<string | null>` — error message

### Methods
- `fetchProducts(): Promise<void>` — fetches all products

### Behavior
- Ordered by created_at descending (newest first)
- Handles states: loading, error, empty
- Uses Supabase client for queries
- No re-fetch if products already loaded (implicit cache)

### Product Type
- id: string
- name: string
- price: number
- category: string
- image_url: string
- created_at: string
Enter fullscreen mode Exit fullscreen mode

Ask Claude Code:

> Read @specs/products.md. Implement useProducts in composables/useProducts.ts and generate TypeScript types from the spec.
Enter fullscreen mode Exit fullscreen mode

Result:

// composables/useProducts.ts
export interface Product {
  id: string
  name: string
  price: number
  category: string
  image_url: string
  created_at: string
}

export function useProducts() {
  const client = useSupabaseClient()
  const products = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function fetchProducts() {
    // Implicit cache: don't re-fetch if we already have data
    if (products.value.length > 0) return

    loading.value = true
    error.value = null

    try {
      const { data, error: err } = await client
        .from('products')
        .select('*')
        .order('created_at', { ascending: false })

      if (err) throw err
      products.value = data as Product[]
    } catch (e: unknown) {
      error.value = e instanceof Error ? e.message : 'Error loading products'
    } finally {
      loading.value = false
    }
  }

  return { products, loading, error, fetchProducts }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Generate TypeScript Types from Supabase

For maximum type safety, generate types directly from the database:

npx supabase login
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts
Enter fullscreen mode Exit fullscreen mode

This reads your actual database schema and generates precise TypeScript types. Now Product isn't a manual interface — it's the exact type of your table.

// types/supabase.ts (auto-generated)
export interface Database {
  public: {
    Tables: {
      products: {
        Row: {
          id: string
          name: string
          price: number
          category: string
          image_url: string
          created_at: string
        }
        Insert: { /* ... */ }
        Update: { /* ... */ }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now our composable is 100% typed:

import type { Database } from '~/types/supabase'

type Product = Database['public']['Tables']['products']['Row']
// 👆 Type derived from the REAL database, not invented by hand
Enter fullscreen mode Exit fullscreen mode

What We Achieved in Part 2

  • ✅ Supabase Auth working: registration, login, logout, session persistence
  • ✅ Forms with validation, error messages, and redirects
  • products table with sample data and Row Level Security
  • useProducts composable with loading/error/empty states
  • ✅ TypeScript types generated from the actual database
  • ✅ Everything implemented from specs — not from loose prompts

In about 45 minutes, you went from an empty scaffold to an app with real authentication and real data.


In Part 3: UI, Deploy & The Living Spec, we build the catalog UI with Tailwind, wire everything together, deploy to production, and learn the most important SDD practice: keeping the spec alive as the project evolves.


References:

Top comments (0)