Claude Code Plugins

Community-maintained marketplace

Feedback

backend-engineer

@wasintoh/toh-framework
34
0

>

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name backend-engineer
description Supabase integration specialist. Handles database schema, authentication, Row Level Security (RLS), real-time subscriptions, and storage. Connects existing UI to real backend. Only called AFTER UI exists with mock data. Triggers: connect database, connect Supabase, add auth, make login, backend integration, real data, authentication, database schema.

Backend Engineer

Connect beautiful UI to real data. Supabase-first approach.

## The Integration Promise

Working UI with mock data → Connect Supabase → Real data flows automatically

We DON'T redesign. We DON'T add features. We connect what exists.

NEVER ask: - "Which database should I use?" → Supabase (our standard) - "What's the schema?" → Derive from existing TypeScript types - "What type of auth do you need?" → Supabase Auth with social providers

ALWAYS do:

  • Create Supabase client configuration
  • Generate schema from existing types
  • Setup RLS policies
  • Replace mock API calls with Supabase queries
## Initial Setup

1. Install Dependencies

npm install @supabase/supabase-js

2. Environment Variables

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...

3. Client Configuration

// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient<Database>(supabaseUrl, supabaseKey)

4. Type Generation (after creating tables)

npx supabase gen types typescript --project-id xxxxx > src/types/supabase.ts
## Database Schema Patterns

Derive from TypeScript Types

// Existing type from dev-engineer
interface Product {
  id: string
  name: string
  description: string
  price: number
  stock: number
  category: string
  isActive: boolean
  createdAt: Date
  updatedAt: Date
}

// Becomes SQL
-- SQL for Supabase
create table products (
  id uuid default gen_random_uuid() primary key,
  name text not null,
  description text,
  price decimal(10,2) not null default 0,
  stock integer not null default 0,
  category text not null,
  is_active boolean not null default true,
  created_at timestamp with time zone default now(),
  updated_at timestamp with time zone default now()
);

-- Auto-update updated_at
create or replace function update_updated_at()
returns trigger as $$
begin
  new.updated_at = now();
  return new;
end;
$$ language plpgsql;

create trigger products_updated_at
  before update on products
  for each row execute function update_updated_at();

Common Tables

-- Users (extends Supabase auth.users)
create table profiles (
  id uuid references auth.users(id) primary key,
  full_name text,
  avatar_url text,
  role text default 'user',
  created_at timestamp with time zone default now(),
  updated_at timestamp with time zone default now()
);

-- Auto-create profile on signup
create or replace function handle_new_user()
returns trigger as $$
begin
  insert into profiles (id, full_name, avatar_url)
  values (
    new.id,
    new.raw_user_meta_data->>'full_name',
    new.raw_user_meta_data->>'avatar_url'
  );
  return new;
end;
$$ language plpgsql security definer;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function handle_new_user();
## Row Level Security (RLS)

Always Enable RLS

-- Enable RLS on all tables
alter table products enable row level security;
alter table profiles enable row level security;

Common Policies

Public Read, Authenticated Write

-- Anyone can view products
create policy "Products are viewable by everyone"
  on products for select
  using (true);

-- Only authenticated users can insert
create policy "Authenticated users can create products"
  on products for insert
  to authenticated
  with check (true);

-- Only owners can update (if user_id column exists)
create policy "Users can update own products"
  on products for update
  to authenticated
  using (user_id = auth.uid());

User-Owned Data

-- Users can only see their own data
create policy "Users can view own orders"
  on orders for select
  to authenticated
  using (user_id = auth.uid());

create policy "Users can create own orders"
  on orders for insert
  to authenticated
  with check (user_id = auth.uid());

Role-Based Access

-- Admins can do everything
create policy "Admins have full access"
  on products for all
  to authenticated
  using (
    exists (
      select 1 from profiles
      where profiles.id = auth.uid()
      and profiles.role = 'admin'
    )
  );
## Authentication

Setup Auth Provider

// src/lib/auth.ts
import { supabase } from './supabase'

export async function signInWithEmail(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  })
  if (error) throw error
  return data
}

export async function signUp(email: string, password: string, fullName: string) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: { full_name: fullName }
    }
  })
  if (error) throw error
  return data
}

export async function signInWithGoogle() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: `${window.location.origin}/auth/callback`
    }
  })
  if (error) throw error
  return data
}

export async function signInWithLine() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'line' as any, // LINE needs custom setup
    options: {
      redirectTo: `${window.location.origin}/auth/callback`
    }
  })
  if (error) throw error
  return data
}

export async function signOut() {
  const { error } = await supabase.auth.signOut()
  if (error) throw error
}

export async function getCurrentUser() {
  const { data: { user } } = await supabase.auth.getUser()
  return user
}

Auth Context

// src/providers/auth-provider.tsx
'use client'

import { createContext, useContext, useEffect, useState } from 'react'
import { User, Session } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'

interface AuthContextType {
  user: User | null
  session: Session | null
  isLoading: boolean
}

const AuthContext = createContext<AuthContextType>({
  user: null,
  session: null,
  isLoading: true,
})

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [session, setSession] = useState<Session | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    // Get initial session
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session)
      setUser(session?.user ?? null)
      setIsLoading(false)
    })

    // Listen for changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setSession(session)
        setUser(session?.user ?? null)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  return (
    <AuthContext.Provider value={{ user, session, isLoading }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => useContext(AuthContext)

Protected Routes (Next.js Middleware)

// src/middleware.ts
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({ req, res })
  const { data: { session } } = await supabase.auth.getSession()

  // Protected routes
  if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url))
  }

  // Redirect logged-in users from auth pages
  if (session && (req.nextUrl.pathname === '/login' || req.nextUrl.pathname === '/register')) {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }

  return res
}

export const config = {
  matcher: ['/dashboard/:path*', '/login', '/register']
}
## Database Queries

CRUD Operations

// src/lib/api/products.ts
import { supabase } from '@/lib/supabase'
import { Product, CreateProductInput, PaginatedResponse } from '@/types'

export async function getProducts(
  page = 1, 
  pageSize = 10,
  search?: string
): Promise<PaginatedResponse<Product>> {
  let query = supabase
    .from('products')
    .select('*', { count: 'exact' })
  
  if (search) {
    query = query.ilike('name', `%${search}%`)
  }
  
  const from = (page - 1) * pageSize
  const to = from + pageSize - 1
  
  const { data, error, count } = await query
    .range(from, to)
    .order('created_at', { ascending: false })
  
  if (error) throw error
  
  return {
    data: data ?? [],
    total: count ?? 0,
    page,
    pageSize,
    totalPages: Math.ceil((count ?? 0) / pageSize),
  }
}

export async function getProduct(id: string): Promise<Product | null> {
  const { data, error } = await supabase
    .from('products')
    .select('*')
    .eq('id', id)
    .single()
  
  if (error) throw error
  return data
}

export async function createProduct(input: CreateProductInput): Promise<Product> {
  const { data, error } = await supabase
    .from('products')
    .insert(input)
    .select()
    .single()
  
  if (error) throw error
  return data
}

export async function updateProduct(
  id: string, 
  input: Partial<Product>
): Promise<Product> {
  const { data, error } = await supabase
    .from('products')
    .update(input)
    .eq('id', id)
    .select()
    .single()
  
  if (error) throw error
  return data
}

export async function deleteProduct(id: string): Promise<void> {
  const { error } = await supabase
    .from('products')
    .delete()
    .eq('id', id)
  
  if (error) throw error
}

Real-time Subscriptions

// Subscribe to changes
export function subscribeToProducts(
  callback: (payload: any) => void
) {
  return supabase
    .channel('products_changes')
    .on(
      'postgres_changes',
      { event: '*', schema: 'public', table: 'products' },
      callback
    )
    .subscribe()
}

// Usage in component
useEffect(() => {
  const channel = subscribeToProducts((payload) => {
    console.log('Change received!', payload)
    refetchProducts()
  })
  
  return () => {
    supabase.removeChannel(channel)
  }
}, [])
## File Storage

Upload Files

// src/lib/storage.ts
import { supabase } from './supabase'

export async function uploadFile(
  bucket: string,
  path: string,
  file: File
): Promise<string> {
  const { data, error } = await supabase.storage
    .from(bucket)
    .upload(path, file, {
      cacheControl: '3600',
      upsert: false
    })
  
  if (error) throw error
  
  // Get public URL
  const { data: { publicUrl } } = supabase.storage
    .from(bucket)
    .getPublicUrl(data.path)
  
  return publicUrl
}

export async function deleteFile(bucket: string, path: string): Promise<void> {
  const { error } = await supabase.storage
    .from(bucket)
    .remove([path])
  
  if (error) throw error
}

Image Upload Component

// src/components/image-upload.tsx
'use client'

import { useState } from 'react'
import { uploadFile } from '@/lib/storage'
import { Button } from '@/components/ui/button'
import { Upload, X } from 'lucide-react'

interface ImageUploadProps {
  onUpload: (url: string) => void
  bucket?: string
}

export function ImageUpload({ onUpload, bucket = 'images' }: ImageUploadProps) {
  const [isUploading, setIsUploading] = useState(false)
  const [preview, setPreview] = useState<string | null>(null)

  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return
    
    setIsUploading(true)
    setPreview(URL.createObjectURL(file))
    
    try {
      const path = `${Date.now()}-${file.name}`
      const url = await uploadFile(bucket, path, file)
      onUpload(url)
    } catch (error) {
      console.error('Upload failed:', error)
      setPreview(null)
    } finally {
      setIsUploading(false)
    }
  }

  return (
    <div className="space-y-2">
      {preview ? (
        <div className="relative inline-block">
          <img src={preview} alt="Preview" className="w-32 h-32 object-cover rounded" />
          <button
            onClick={() => setPreview(null)}
            className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1"
          >
            <X className="h-4 w-4" />
          </button>
        </div>
      ) : (
        <label className="flex flex-col items-center justify-center w-32 h-32 border-2 border-dashed rounded cursor-pointer hover:border-primary">
          <Upload className="h-8 w-8 text-slate-400" />
          <span className="text-xs text-slate-500 mt-2">Upload image</span>
          <input
            type="file"
            accept="image/*"
            onChange={handleUpload}
            className="hidden"
            disabled={isUploading}
          />
        </label>
      )}
    </div>
  )
}
## Migration Checklist (Mock → Supabase)

Setup Phase

  • Create Supabase project
  • Add environment variables
  • Install @supabase/supabase-js
  • Create supabase client

Schema Phase

  • Convert TypeScript types to SQL
  • Create tables in Supabase
  • Setup triggers (updated_at, etc.)
  • Generate TypeScript types from schema

Security Phase

  • Enable RLS on all tables
  • Create appropriate policies
  • Setup auth providers (if needed)

Migration Phase

  • Replace mock API functions with Supabase queries
  • Update Zustand stores to use new API
  • Test all CRUD operations
  • Add real-time subscriptions (optional)

Verification Phase

  • All pages load with real data
  • Create/Update/Delete works
  • Auth flows work (if applicable)
  • RLS policies work correctly