Skip to content
Back to skills

Tpl Fullstack Supabase Nextjs

ASecurity

Template do pack (fullstack/07-supabase-nextjs.md). Orienta o agente em stacks fullstack e arquitetura ponta a ponta alinhado a esse contexto.

  • 11 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 8, 2026
developmenttypescriptrustsqlreactnextjstestingapidatabasebackendfullstack

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add artubss/SKILLS-CLAUDE-CODE --skill tpl-fullstack-supabase-nextjs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tpl Fullstack Supabase Nextjs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tpl Fullstack Supabase Nextjs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/artubss-tpl-fullstack-supabase-nextjs/badge)](https://www.skillsdirectory.com/skills/artubss-tpl-fullstack-supabase-nextjs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tpl-fullstack-supabase-nextjs
description: Template do pack (fullstack/07-supabase-nextjs.md). Orienta o agente em stacks fullstack e arquitetura ponta a ponta alinhado a esse contexto.
metadata:
  version: 1.0.0
  source_template: fullstack/07-supabase-nextjs.md
  generated_by: install_pack_templates_as_claude_skills
---

# PROJECT: Next.js 15 + Supabase (Auth + DB + Storage + Realtime)

Skill gerado a partir do pack `templates-claude-code`. Arquivo de origem: `fullstack/07-supabase-nextjs.md`. Use como baseline e adapte ao projeto antes de mudancas grandes.

## Conteudo do template

## STACK
- **Framework:** Next.js 15 (App Router)
- **Backend:** Supabase (Auth, PostgreSQL, Storage, Realtime)
- **Language:** TypeScript 5.x (strict)
- **Client:** @supabase/ssr (Next.js-specific helpers)
- **Styling:** Tailwind CSS v3 + shadcn/ui
- **Type generation:** supabase gen types typescript
- **Testing:** Vitest + Playwright

---

## PROJECT STRUCTURE
```
src/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   └── signup/page.tsx
│   ├── (protected)/
│   │   └── dashboard/page.tsx
│   └── api/
│       └── auth/
│           └── callback/route.ts    # OAuth callback
├── lib/
│   ├── supabase/
│   │   ├── client.ts               # Browser Supabase client
│   │   ├── server.ts               # Server-side client (cookies)
│   │   └── middleware.ts           # Session refresh middleware
│   └── utils.ts
├── components/
│   ├── AuthButton.tsx
│   └── RealtimePosts.tsx           # Realtime subscription component
├── hooks/
│   └── useRealtimeSubscription.ts
└── types/
    └── database.types.ts           # Generated by supabase CLI
middleware.ts                       # Next.js middleware (session refresh)
supabase/
├── migrations/
│   └── 001_init.sql
└── seed.sql
```

---

## ARCHITECTURE RULES
1. **Two Supabase clients** — `client.ts` (browser, single instance) and `server.ts` (SSR, reads cookies); never mix them.
2. **Row Level Security is mandatory** — every table has RLS enabled with explicit `ENABLE ROW LEVEL SECURITY`; deny-by-default.
3. **Server client for mutations in Server Actions/Route Handlers** — always use server client for sensitive operations.
4. **Middleware refreshes sessions** — `middleware.ts` calls `supabase.auth.getUser()` on every request to refresh tokens.
5. **Type-safe via `database.types.ts`** — generated types used throughout; regenerate after every schema change.
6. **Storage buckets have policies** — no bucket is `public` without intent; RLS-like bucket policies enforced.
7. **Realtime subscriptions only in Client Components** — clean up with `useEffect` return function.

---

## SUPABASE CLIENT SETUP

```typescript
// src/lib/supabase/client.ts (singleton browser client)
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '~/types/database.types'

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

// src/lib/supabase/server.ts (server-side, reads/writes cookies)
import { createServerClient } from '@supabase/ssr'
import { cookies }            from 'next/headers'
import type { Database }      from '~/types/database.types'

export async function createClient() {
  const cookieStore = await cookies()
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll:    () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}

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

export async function middleware(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: () => request.cookies.getAll(), setAll: (c) => c.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options)) } }
  )
  await supabase.auth.getUser()  // refreshes token automatically
  return supabaseResponse
}

export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }
```

---

## ROW LEVEL SECURITY POLICIES

```sql
-- supabase/migrations/001_init.sql

-- Users can read all profiles; only own profile is updatable
CREATE TABLE profiles (
  id         UUID REFERENCES auth.users PRIMARY KEY,
  username   TEXT UNIQUE NOT NULL,
  avatar_url TEXT,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Public profiles are viewable by everyone"
  ON profiles FOR SELECT USING (true);

CREATE POLICY "Users can insert their own profile"
  ON profiles FOR INSERT WITH CHECK (auth.uid() = id);

CREATE POLICY "Users can update their own profile"
  ON profiles FOR UPDATE USING (auth.uid() = id);

-- Posts: public read; auth write own
CREATE TABLE posts (
  id         UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  title      TEXT NOT NULL,
  body       TEXT NOT NULL,
  author_id  UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
  published  BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Published posts viewable by all"
  ON posts FOR SELECT USING (published = true OR author_id = auth.uid());

CREATE POLICY "Authors can insert posts"
  ON posts FOR INSERT WITH CHECK (auth.uid() = author_id);

CREATE POLICY "Authors can update their posts"
  ON posts FOR UPDATE USING (auth.uid() = author_id);

CREATE POLICY "Authors can delete their posts"
  ON posts FOR DELETE USING (auth.uid() = author_id);
```

---

## REALTIME SUBSCRIPTION

```typescript
// src/components/RealtimePosts.tsx
'use client'
import { useEffect, useState } from 'react'
import { createClient }        from '~/lib/supabase/client'
import type { Database }       from '~/types/database.types'

type Post = Database['public']['Tables']['posts']['Row']

export function RealtimePosts({ initialPosts }: { initialPosts: Post[] }) {
  const [posts, setPosts] = useState(initialPosts)
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase
      .channel('posts-realtime')
      .on('postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'posts', filter: 'published=eq.true' },
        (payload) => {
          setPosts(prev => [payload.new as Post, ...prev])
        }
      )
      .on('postgres_changes',
        { event: 'DELETE', schema: 'public', table: 'posts' },
        (payload) => {
          setPosts(prev => prev.filter(p => p.id !== payload.old.id))
        }
      )
      .subscribe()

    return () => { void supabase.removeChannel(channel) }
  }, [supabase])

  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
```

---

## STORAGE BUCKET RULES

```sql
-- Storage: user avatars (private, own access only)
INSERT INTO storage.buckets (id, name, public) VALUES ('avatars', 'avatars', false);

CREATE POLICY "Avatar images are accessible by owner"
  ON storage.objects FOR SELECT
  USING (bucket_id = 'avatars' AND auth.uid() = owner);

CREATE POLICY "Users can upload their own avatar"
  ON storage.objects FOR INSERT
  WITH CHECK (bucket_id = 'avatars' AND auth.uid() = owner AND (storage.foldername(name))[1] = auth.uid()::text);
```

```typescript
// Uploading a file (Server Action)
async function uploadAvatar(file: File) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not authenticated')

  const ext  = file.name.split('.').pop()
  const path = `${user.id}/avatar.${ext}`
  const { error } = await supabase.storage.from('avatars').upload(path, file, { upsert: true })
  if (error) throw error

  const { data } = supabase.storage.from('avatars').getPublicUrl(path)
  return data.publicUrl
}
```

---

## ROUTING TABLE

| Method / Trigger | Path / Event | Auth | Action |
|-----------------|-------------|------|--------|
| GET | /login | No | Auth.js sign-in page |
| GET | /api/auth/callback | No | OAuth token exchange |
| GET | /dashboard | Yes | RSC with server client |
| POST | Server Action: createPost | Yes | Insert via server client |
| POST | Server Action: uploadAvatar | Yes | Upload to Storage |
| Realtime | posts INSERT | No (RLS) | Push to client state |
| Realtime | posts DELETE | No (RLS) | Remove from client state |
| GET | /storage/avatars/* | Yes (RLS) | Serve private avatar |

---

## QUALITY GATES
- [ ] `supabase gen types typescript --local > src/types/database.types.ts` — in sync
- [ ] `tsc --noEmit` — zero errors
- [ ] Every table has `ENABLE ROW LEVEL SECURITY` + explicit policies
- [ ] `supabase db push` — migration applied to remote without errors
- [ ] Realtime components clean up subscriptions on unmount
- [ ] Storage buckets are `public: false` unless intentionally public
- [ ] `playwright test` — E2E auth flow passes
- [ ] No `anon` key on server — use service role key only in trusted server contexts

---

## FORBIDDEN
- ❌ `supabase.auth.getSession()` for auth checks — use `getUser()` (validates JWT server-side)
- ❌ Service role key exposed in client code or `NEXT_PUBLIC_*` env vars
- ❌ Tables without RLS policies — never `DISABLE ROW LEVEL SECURITY`
- ❌ Browser Supabase client in Server Components or Route Handlers
- ❌ Direct `supabase.from('users')` on `auth.users` — use `profiles` table exposed via RLS
- ❌ Realtime subscriptions opened in Server Components
- ❌ `POSTGREST_*` bypass headers in client queries
- ❌ `public` storage bucket without explicit policy review

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in development →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.