| name | supabase-artifact-connection |
| description | Connect Supabase databases to Claude Desktop artifacts with authentication and read-only queries using native fetch API. |
Supabase Artifact Connection
What's New (Updated Based on Engineering Feedback)
CRITICAL UPDATE: This skill now includes proper authentication flow.
Previous Version Issue:
- Only used URL + anon key
- Attempted to query data immediately
- Failed with RLS permission errors
Current Version Fix:
- Two-phase authentication: Project config (URL + anon key) + User auth (email/password)
- Login form automatically generated in artifacts
- Session management with persistence
- Works correctly with Row Level Security policies
User Experience:
- User provides URL + anon key in prompt
- Claude generates artifact with login form
- User opens artifact, sees login screen
- User enters Supabase Auth email/password at runtime
- Artifact authenticates and displays data
Core Principle
Enable live database connections in Claude Desktop artifacts using Supabase REST API with native fetch(), proper authentication, and enforced read-only access.
Critical Issue: JS Client Library Doesn't Work in Artifacts
PROBLEM: The Supabase JavaScript client library (@supabase/supabase-js@2) causes DataCloneError in Claude Desktop artifacts due to iframe sandbox restrictions.
SOLUTION: Use Supabase REST API with native fetch() instead.
DO NOT use this script:
<!-- ❌ NEVER USE - Causes DataCloneError in Claude Desktop -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
When to Activate
This skill activates when:
- User mentions "Supabase" + "artifact" or "dashboard"
- User provides Supabase credentials (URL + anon key)
- User wants to query database data in an interactive artifact
- User asks to "connect to Supabase" or "load data from Supabase"
Authentication Requirement
CRITICAL: Supabase queries require user authentication via email/password in addition to project configuration (URL + anon key).
Two-Phase Connection Pattern
Phase 1: Project Configuration (what user provides in prompt)
- Supabase project URL (e.g.,
https://xyz.supabase.co) - Anon key (starts with
eyJhbGc...)
Phase 2: User Authentication (handled via login form in artifact)
- User email (Supabase Auth user)
- User password
- Session stored in browser after successful login
Engineering Note: You cannot query data with just URL + anon key. User must authenticate with signInWithPassword() first.
Supabase REST API Wrapper
Always include this lightweight wrapper in artifacts - it mimics the Supabase client API but uses fetch():
class SupabaseRestClient {
constructor(url, key) {
this.url = url;
this.key = key;
this.authToken = key; // Initially use anon key, will be replaced with session token
}
from(table) {
return new QueryBuilder(this.url, this.authToken, table);
}
// Authentication methods
get auth() {
return {
signInWithPassword: async ({ email, password }) => {
try {
const response = await fetch(`${this.url}/auth/v1/token?grant_type=password`, {
method: 'POST',
headers: {
'apikey': this.key,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const errorData = await response.json();
return { data: null, error: errorData };
}
const data = await response.json();
// Update auth token for subsequent queries
this.authToken = data.access_token;
return { data, error: null };
} catch (error) {
return { data: null, error: { message: error.message } };
}
},
getSession: async () => {
try {
const response = await fetch(`${this.url}/auth/v1/user`, {
headers: {
'apikey': this.key,
'Authorization': `Bearer ${this.authToken}`
}
});
if (!response.ok) {
return { data: { session: null }, error: null };
}
const user = await response.json();
return { data: { session: { user } }, error: null };
} catch (error) {
return { data: { session: null }, error: null };
}
},
signOut: async () => {
this.authToken = this.key; // Reset to anon key
return { error: null };
}
};
}
}
class QueryBuilder {
constructor(url, authToken, table) {
this.url = url;
this.authToken = authToken;
this.table = table;
this.selectCols = '*';
this.filters = [];
this.orderBy = null;
this.limitCount = null;
}
select(columns = '*') {
this.selectCols = columns;
return this;
}
eq(column, value) {
this.filters.push(`${column}=eq.${encodeURIComponent(value)}`);
return this;
}
neq(column, value) {
this.filters.push(`${column}=neq.${encodeURIComponent(value)}`);
return this;
}
gt(column, value) {
this.filters.push(`${column}=gt.${encodeURIComponent(value)}`);
return this;
}
gte(column, value) {
this.filters.push(`${column}=gte.${encodeURIComponent(value)}`);
return this;
}
lt(column, value) {
this.filters.push(`${column}=lt.${encodeURIComponent(value)}`);
return this;
}
lte(column, value) {
this.filters.push(`${column}=lte.${encodeURIComponent(value)}`);
return this;
}
like(column, pattern) {
this.filters.push(`${column}=like.${encodeURIComponent(pattern)}`);
return this;
}
ilike(column, pattern) {
this.filters.push(`${column}=ilike.${encodeURIComponent(pattern)}`);
return this;
}
in(column, values) {
this.filters.push(`${column}=in.(${values.map(v => encodeURIComponent(v)).join(',')})`);
return this;
}
is(column, value) {
this.filters.push(`${column}=is.${value === null ? 'null' : encodeURIComponent(value)}`);
return this;
}
order(column, { ascending = true } = {}) {
this.orderBy = `${column}.${ascending ? 'asc' : 'desc'}`;
return this;
}
limit(count) {
this.limitCount = count;
return this;
}
async execute() {
try {
const params = [`select=${this.selectCols}`];
if (this.filters.length > 0) params.push(...this.filters);
if (this.orderBy) params.push(`order=${this.orderBy}`);
if (this.limitCount) params.push(`limit=${this.limitCount}`);
const url = `${this.url}/rest/v1/${this.table}?${params.join('&')}`;
const response = await fetch(url, {
headers: {
'apikey': this.authToken,
'Authorization': `Bearer ${this.authToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
return { data: null, error: errorData };
}
const data = await response.json();
return { data, error: null };
} catch (error) {
return { data: null, error: { message: error.message } };
}
}
}
function createSupabaseClient(url, key) {
return new SupabaseRestClient(url, key);
}
Standard Initialization Pattern
// Initialize Supabase client (REST API wrapper)
const SUPABASE_URL = 'https://[project-id].supabase.co';
const SUPABASE_ANON_KEY = 'eyJhbGc...';
const supabase = createSupabaseClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// Check if user is already authenticated
async function checkAuth() {
const { data: { session } } = await supabase.auth.getSession();
if (session) {
showDashboard(); // User is logged in
} else {
showLoginForm(); // Need to login
}
}
// Handle login
async function handleLogin(email, password) {
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
password: password
});
if (error) {
console.error('Login error:', error);
return false;
}
return true; // Login successful
}
// Query example (after authentication)
async function loadData() {
const { data, error } = await supabase
.from('table_name')
.select('*')
.limit(100)
.execute(); // Note: .execute() is required with REST wrapper
if (error) {
console.error('Query error:', error);
return null;
}
return data;
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', checkAuth);
Read-Only Access Rules
CRITICAL: NEVER generate code that writes to Supabase.
✅ ALLOWED Operations:
.select()- Query data.from()- Specify table- Filters:
.eq(),.neq(),.gt(),.gte(),.lt(),.lte(),.like(),.ilike(),.in(),.is() - Ordering:
.order() - Limits:
.limit()
❌ FORBIDDEN Operations:
.insert()- Create records.update()- Modify records.upsert()- Insert or update.delete()- Remove records- Any mutation operations
If user requests write operations, respond:
"This skill only supports read-only queries to protect database integrity. For write operations, use Supabase Dashboard or a dedicated backend service."
Query Patterns
Basic Query
const { data, error } = await supabase
.from('products')
.select('*')
.execute();
Filtered Query
const { data, error } = await supabase
.from('products')
.select('name, price, category')
.eq('category', 'Electronics')
.gt('price', 100)
.order('price', { ascending: false })
.limit(50)
.execute();
Search Query (Case-Insensitive)
const { data, error } = await supabase
.from('customers')
.select('*')
.ilike('name', `%${searchTerm}%`)
.execute();
Multiple Filters
const { data, error } = await supabase
.from('orders')
.select('*')
.eq('status', 'active')
.gte('created_at', '2025-01-01')
.lte('total', 1000)
.limit(20)
.execute();
IN Filter
const { data, error } = await supabase
.from('products')
.select('*')
.in('category', ['Electronics', 'Books', 'Toys'])
.execute();
Complete Artifact Template
Important: Include the REST API wrapper classes AND authentication UI.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Supabase Data Viewer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
h1 {
color: #1a1a1a;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
/* Login form styles */
.login-container {
max-width: 400px;
margin: 60px auto;
}
.login-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.form-group label {
font-weight: 500;
font-size: 14px;
color: #333;
}
.form-group input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus {
outline: none;
border-color: #3b82f6;
}
.login-button {
padding: 12px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-top: 10px;
}
.login-button:hover {
background: #2563eb;
}
.login-button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.logout-button {
padding: 8px 16px;
background: #dc2626;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
margin-bottom: 20px;
}
.logout-button:hover {
background: #b91c1c;
}
.hidden {
display: none;
}
.loading {
padding: 40px;
text-align: center;
color: #666;
}
.error {
background: #ffebee;
border: 1px solid #e57373;
padding: 15px;
border-radius: 6px;
color: #c62828;
margin: 20px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
text-align: left;
padding: 12px;
border-bottom: 1px solid #ddd;
}
th {
background: #f8f9fa;
font-weight: 600;
font-size: 13px;
}
tbody tr:hover {
background: #fafafa;
}
</style>
</head>
<body>
<!-- Login Screen -->
<div id="loginScreen" class="hidden">
<div class="login-container">
<h1>Login to Supabase</h1>
<div class="subtitle">Enter your credentials to access data</div>
<form id="loginForm" class="login-form">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" required placeholder="Your password">
</div>
<div id="loginError" class="error hidden"></div>
<button type="submit" class="login-button">Login</button>
</form>
</div>
</div>
<!-- Dashboard (shown after login) -->
<div id="dashboard" class="hidden">
<div class="container">
<button id="logoutButton" class="logout-button">Logout</button>
<h1>Supabase Data</h1>
<div class="subtitle">Connected to: [PROJECT_URL]</div>
<div id="content">
<div class="loading">Loading data...</div>
</div>
</div>
</div>
<script>
// ============================================
// SUPABASE REST API WRAPPER WITH AUTH (Required for Claude Desktop)
// ============================================
class SupabaseRestClient {
constructor(url, key) {
this.url = url;
this.key = key;
this.authToken = key;
}
from(table) {
return new QueryBuilder(this.url, this.authToken, table);
}
get auth() {
return {
signInWithPassword: async ({ email, password }) => {
try {
const response = await fetch(`${this.url}/auth/v1/token?grant_type=password`, {
method: 'POST',
headers: {
'apikey': this.key,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const errorData = await response.json();
return { data: null, error: errorData };
}
const data = await response.json();
this.authToken = data.access_token;
return { data, error: null };
} catch (error) {
return { data: null, error: { message: error.message } };
}
},
getSession: async () => {
try {
const response = await fetch(`${this.url}/auth/v1/user`, {
headers: {
'apikey': this.key,
'Authorization': `Bearer ${this.authToken}`
}
});
if (!response.ok) {
return { data: { session: null }, error: null };
}
const user = await response.json();
return { data: { session: { user } }, error: null };
} catch (error) {
return { data: { session: null }, error: null };
}
},
signOut: async () => {
this.authToken = this.key;
return { error: null };
}
};
}
}
class QueryBuilder {
constructor(url, authToken, table) {
this.url = url;
this.authToken = authToken;
this.table = table;
this.selectCols = '*';
this.filters = [];
this.orderBy = null;
this.limitCount = null;
}
select(columns = '*') { this.selectCols = columns; return this; }
eq(column, value) { this.filters.push(`${column}=eq.${encodeURIComponent(value)}`); return this; }
neq(column, value) { this.filters.push(`${column}=neq.${encodeURIComponent(value)}`); return this; }
gt(column, value) { this.filters.push(`${column}=gt.${encodeURIComponent(value)}`); return this; }
gte(column, value) { this.filters.push(`${column}=gte.${encodeURIComponent(value)}`); return this; }
lt(column, value) { this.filters.push(`${column}=lt.${encodeURIComponent(value)}`); return this; }
lte(column, value) { this.filters.push(`${column}=lte.${encodeURIComponent(value)}`); return this; }
like(column, pattern) { this.filters.push(`${column}=like.${encodeURIComponent(pattern)}`); return this; }
ilike(column, pattern) { this.filters.push(`${column}=ilike.${encodeURIComponent(pattern)}`); return this; }
in(column, values) { this.filters.push(`${column}=in.(${values.map(v => encodeURIComponent(v)).join(',')})`); return this; }
is(column, value) { this.filters.push(`${column}=is.${value === null ? 'null' : encodeURIComponent(value)}`); return this; }
order(column, { ascending = true } = {}) { this.orderBy = `${column}.${ascending ? 'asc' : 'desc'}`; return this; }
limit(count) { this.limitCount = count; return this; }
async execute() {
try {
const params = [`select=${this.selectCols}`];
if (this.filters.length > 0) params.push(...this.filters);
if (this.orderBy) params.push(`order=${this.orderBy}`);
if (this.limitCount) params.push(`limit=${this.limitCount}`);
const url = `${this.url}/rest/v1/${this.table}?${params.join('&')}`;
const response = await fetch(url, {
headers: {
'apikey': this.authToken,
'Authorization': `Bearer ${this.authToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
return { data: null, error: errorData };
}
const data = await response.json();
return { data, error: null };
} catch (error) {
return { data: null, error: { message: error.message } };
}
}
}
function createSupabaseClient(url, key) {
return new SupabaseRestClient(url, key);
}
// ============================================
// CONFIGURATION
// ============================================
const SUPABASE_URL = 'https://[PROJECT_ID].supabase.co';
const SUPABASE_ANON_KEY = 'eyJhbGc...';
const supabase = createSupabaseClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// ============================================
// UI ELEMENTS
// ============================================
const loginScreen = document.getElementById('loginScreen');
const dashboard = document.getElementById('dashboard');
const loginForm = document.getElementById('loginForm');
const loginError = document.getElementById('loginError');
const logoutButton = document.getElementById('logoutButton');
// ============================================
// AUTHENTICATION HANDLERS
// ============================================
async function checkAuth() {
const { data: { session } } = await supabase.auth.getSession();
if (session) {
showDashboard();
} else {
showLogin();
}
}
function showLogin() {
loginScreen.classList.remove('hidden');
dashboard.classList.add('hidden');
}
function showDashboard() {
loginScreen.classList.add('hidden');
dashboard.classList.remove('hidden');
loadData();
}
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
loginError.classList.add('hidden');
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
password: password
});
if (error) {
loginError.textContent = error.message || 'Login failed. Please check your credentials.';
loginError.classList.remove('hidden');
} else {
showDashboard();
}
});
logoutButton.addEventListener('click', async () => {
await supabase.auth.signOut();
showLogin();
});
// ============================================
// LOAD AND DISPLAY DATA
// ============================================
async function loadData() {
const contentDiv = document.getElementById('content');
try {
const { data, error } = await supabase
.from('table_name')
.select('*')
.limit(100)
.execute();
if (error) throw new Error(error.message || JSON.stringify(error));
if (!data || data.length === 0) {
contentDiv.innerHTML = '<p>No data found.</p>';
return;
}
// Create table
const columns = Object.keys(data[0]);
let html = '<table><thead><tr>';
columns.forEach(col => html += `<th>${col}</th>`);
html += '</tr></thead><tbody>';
data.forEach(row => {
html += '<tr>';
columns.forEach(col => html += `<td>${row[col] ?? ''}</td>`);
html += '</tr>';
});
html += '</tbody></table>';
contentDiv.innerHTML = html;
} catch (error) {
console.error('Error:', error);
contentDiv.innerHTML = `<div class="error">Error loading data: ${error.message}</div>`;
}
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', checkAuth);
</script>
</body>
</html>
Error Handling
Always include comprehensive error handling:
try {
const { data, error } = await supabase
.from('table_name')
.select('*')
.execute();
if (error) {
throw new Error(error.message || JSON.stringify(error));
}
// Process data
console.log('Data loaded:', data);
} catch (error) {
console.error('Supabase error:', error);
// Show user-friendly error message
document.getElementById('content').innerHTML =
`<div class="error">Failed to load data: ${error.message}</div>`;
}
Common Errors
Error: "Invalid login credentials" or authentication failed
- User email/password are incorrect
- User does not exist in Supabase Auth
- Check credentials in Supabase Dashboard → Authentication → Users
Error: "Invalid API key" or 401
- Check SUPABASE_ANON_KEY is correct
- Ensure key starts with
eyJhbGc... - Verify key matches project in Supabase Dashboard → Settings → API
Error: "Table not found" or 404
- Verify table name spelling
- Check table exists in Supabase Dashboard → Table Editor
- Ensure anon key has read permissions
Error: "Row Level Security policy violation" or 403
- Most common error: User authenticated but RLS policy blocks access
- Table has RLS enabled but no policy for authenticated users
- Fix: Add RLS policy in Supabase Dashboard → Authentication → Policies
- Example policy for authenticated read access:
CREATE POLICY "Allow authenticated users to read" ON table_name FOR SELECT TO authenticated USING (true);
Error: "CORS error"
- Should not occur with official Supabase hosting
- If using self-hosted, check CORS configuration
Error: Login form not showing
- Check browser console for JavaScript errors
- Verify
.hiddenclass is defined in CSS - Ensure
checkAuth()is called on page load
Success Checklist
Before sharing artifact with user:
- ✅ REST API wrapper WITH AUTH included in artifact (NOT CDN script)
- ✅ Client initialized with user's project URL and anon key
- ✅ Login form implemented with email/password inputs
- ✅ Session checking on page load (
checkAuth()) - ✅ Logout button included
- ✅ Query uses read-only operations only
- ✅
.execute()called at end of query chain - ✅ Error handling included (both auth and data errors)
- ✅ Loading state shown to user
- ✅ Data displayed in readable format
Important User Instructions: When artifact is opened, user needs to:
- Enter their Supabase Auth email (created in Supabase Dashboard)
- Enter their password
- Login will authenticate and display data
- Session persists on refresh (no re-login needed until logout)
Example Use Cases
Simple Data Table:
const { data, error } = await supabase
.from('customers')
.select('*')
.limit(50)
.execute();
Filtered Dashboard:
const { data, error } = await supabase
.from('users')
.select('name, email, created_at')
.eq('status', 'active')
.order('created_at', { ascending: false })
.execute();
Search Interface:
const { data, error } = await supabase
.from('products')
.select('*')
.ilike('name', `%${searchTerm}%`)
.execute();
Next Steps After Connection
Once basic connection works:
- Add interactive filters (dropdowns, search)
- Implement data visualization (charts, graphs)
- Add export functionality (CSV, JSON)
- Create multi-table views
- Build custom UI components
Remember: Always start with basic connection, then enhance incrementally based on user needs.