supabase-integration — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited supabase-integration (Agent Skill) and scored it 91/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} is the classic direct prompt-injection phrasing. Placed in a skill body that the agent reads as trusted instructions, it tries to make the agent abandon its prior rules and follow whatever comes next — a full system-prompt override.
ignore/disregard/forget … previous instructions sentence.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
npm install @supabase/supabase-js
# Local development
npm install -g supabase
supabase init
supabase start # starts local Postgres + Auth + Storage// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import type { Database } from './database.types' // generated types
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Server-side client (Next.js App Router)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createServerSupabase() {
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)
)
},
},
}
)
}supabase gen types typescript --local > lib/database.types.ts
# Or for remote:
supabase gen types typescript --project-id <project-id> > lib/database.types.ts// SELECT with filter
const { data: users, error } = await supabase
.from('users')
.select('id, name, email, created_at')
.eq('active', true)
.order('created_at', { ascending: false })
.limit(20)
if (error) throw error
// SELECT with related data (join)
const { data: posts } = await supabase
.from('posts')
.select(`
id,
title,
content,
created_at,
author:users (id, name, avatar_url),
comments (id, body, user_id)
`)
.eq('published', true)
// INSERT
const { data: newPost, error } = await supabase
.from('posts')
.insert({
title: 'Hello World',
content: 'My first post',
author_id: userId,
})
.select()
.single()
// UPDATE
const { error } = await supabase
.from('posts')
.update({ title: 'Updated Title' })
.eq('id', postId)
.eq('author_id', userId) // extra safety - only update own posts
// UPSERT
const { data } = await supabase
.from('user_settings')
.upsert({ user_id: userId, theme: 'dark' }, { onConflict: 'user_id' })
.select()
// DELETE
await supabase.from('posts').delete().eq('id', postId)
// Raw SQL (complex queries)
const { data } = await supabase.rpc('get_popular_posts', { limit_count: 10 })// Sign up
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'password123',
options: {
data: { full_name: 'Alice Smith' } // stored in user_metadata
}
})
// Sign in
const { data: { session }, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password123',
})
// Sign out
await supabase.auth.signOut()
// Get current user
const { data: { user } } = await supabase.auth.getUser()await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
scopes: 'email profile',
},
})await supabase.auth.signInWithOtp({
email: '[email protected]',
options: { emailRedirectTo: `${window.location.origin}/auth/callback` }
})// app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabase } from '@/lib/supabase'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
if (code) {
const supabase = await createServerSupabase()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(new URL('/dashboard', request.url))
}const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
if (event === 'SIGNED_IN') router.push('/dashboard')
if (event === 'SIGNED_OUT') router.push('/login')
}
)
// Cleanup
return () => subscription.unsubscribe()Always enable RLS on every table. Without it, all data is publicly accessible.
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can only read their own data
CREATE POLICY "Users can read own profile"
ON profiles FOR SELECT
USING (auth.uid() = user_id);
-- Users can read published posts (public read)
CREATE POLICY "Anyone can read published posts"
ON posts FOR SELECT
USING (published = true);
-- Users can only insert their own posts
CREATE POLICY "Users can create own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Users can only update their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Service role bypasses RLS (use for admin/backend)
-- Use SUPABASE_SERVICE_ROLE_KEY (not anon key) for admin operations// Subscribe to table changes
const channel = supabase
.channel('messages')
.on(
'postgres_changes',
{
event: '*', // INSERT | UPDATE | DELETE | *
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`,
},
(payload) => {
if (payload.eventType === 'INSERT') {
setMessages(prev => [...prev, payload.new as Message])
}
if (payload.eventType === 'DELETE') {
setMessages(prev => prev.filter(m => m.id !== payload.old.id))
}
}
)
.subscribe()
// Cleanup
return () => { supabase.removeChannel(channel) }// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.jpg`, file, {
cacheControl: '3600',
upsert: true,
contentType: 'image/jpeg',
})
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.jpg`)
// Download
const { data: blob } = await supabase.storage
.from('documents')
.download('report.pdf')
// Delete
await supabase.storage.from('avatars').remove([`${userId}/avatar.jpg`])Storage RLS policy:
-- Users can manage their own avatar
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
serve(async (req) => {
const { to, subject, body } = await req.json()
// Call external service (Resend, SendGrid, etc.)
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ from: '[email protected]', to, subject, html: body }),
})
return new Response(JSON.stringify({ sent: response.ok }), {
headers: { 'Content-Type': 'application/json' },
})
})supabase functions deploy send-email
supabase secrets set RESEND_API_KEY=re_xxxxxSUPABASE_SERVICE_ROLE_KEY bypasses RLS; never expose it in the browser{ data, error } and check errorsupabase gen types after every schema changesupabase.removeChannel() on component unmountprofiles table with RLS for private fields~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.