Template do pack (fullstack/07-supabase-nextjs.md). Orienta o agente em stacks fullstack e arquitetura ponta a ponta alinhado a esse contexto.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add artubss/SKILLS-CLAUDE-CODE --skill tpl-fullstack-supabase-nextjs --agent claude-codeInstalls 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 automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/artubss-tpl-fullstack-supabase-nextjs)More formats (shields.io, HTML) on the badges page.
---
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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!