一键导入
supabase-artifact-connection
Connect Supabase databases to Claude Desktop artifacts with authentication and read-only queries using native fetch API.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Connect Supabase databases to Claude Desktop artifacts with authentication and read-only queries using native fetch API.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Execute tasks efficiently with minimal explanations. Deliver results first, explanations only when needed. Optimized for action-focused workflows and ADHD-friendly responses.
Apply the complete CURV Tools design system to all visual outputs - dashboards, reports, HTML artifacts, and data visualizations. Includes exact colors from production tools (rgb(3, 12, 27) dark background, rgb(157, 78, 221) purple accent), glassmorphic effects, gradient borders, header/footer templates, and animation patterns used in GL Decoder and SQPR Analyser.
Automatically convert uploaded data (CSV, Excel, JSON) into complete interactive dashboards with zero user input required. Detects patterns in PPC reports, sales data, analytics exports, and business metrics - then generates insights, recommendations, and visualizations instantly. Works seamlessly with CURV design system for on-brand outputs with tabs, funnels, filters, and multi-view layouts.
Generate professional slide decks and presentations using Formula Botanica brand guidelines - green botanical theme with Roboto Light/Thin fonts, leaf logo motifs, and clean layouts for finance processes, team presentations, and internal documentation.
Connect Supabase databases to Claude Desktop artifacts with read-only queries and live data visualization.
| name | supabase-artifact-connection |
| description | Connect Supabase databases to Claude Desktop artifacts with authentication and read-only queries using native fetch API. |
CRITICAL UPDATE: This skill now includes proper authentication flow.
Previous Version Issue:
Current Version Fix:
User Experience:
Enable live database connections in Claude Desktop artifacts using Supabase REST API with native fetch(), proper authentication, and enforced read-only access.
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>
This skill activates when:
CRITICAL: Supabase queries require user authentication via email/password in addition to project configuration (URL + anon key).
Phase 1: Project Configuration (what user provides in prompt)
https://xyz.supabase.co)eyJhbGc...)Phase 2: User Authentication (handled via login form in artifact)
Engineering Note: You cannot query data with just URL + anon key. User must authenticate with signInWithPassword() first.
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);
}
// 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);
CRITICAL: NEVER generate code that writes to Supabase.
.select() - Query data.from() - Specify table.eq(), .neq(), .gt(), .gte(), .lt(), .lte(), .like(), .ilike(), .in(), .is().order().limit().insert() - Create records.update() - Modify records.upsert() - Insert or update.delete() - Remove recordsIf 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."
const { data, error } = await supabase
.from('products')
.select('*')
.execute();
const { data, error } = await supabase
.from('products')
.select('name, price, category')
.eq('category', 'Electronics')
.gt('price', 100)
.order('price', { ascending: false })
.limit(50)
.execute();
const { data, error } = await supabase
.from('customers')
.select('*')
.ilike('name', `%${searchTerm}%`)
.execute();
const { data, error } = await supabase
.from('orders')
.select('*')
.eq('status', 'active')
.gte('created_at', '2025-01-01')
.lte('total', 1000)
.limit(20)
.execute();
const { data, error } = await supabase
.from('products')
.select('*')
.in('category', ['Electronics', 'Books', 'Toys'])
.execute();
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>
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>`;
}
Error: "Invalid login credentials" or authentication failed
Error: "Invalid API key" or 401
eyJhbGc...Error: "Table not found" or 404
Error: "Row Level Security policy violation" or 403
CREATE POLICY "Allow authenticated users to read"
ON table_name
FOR SELECT
TO authenticated
USING (true);
Error: "CORS error"
Error: Login form not showing
.hidden class is defined in CSScheckAuth() is called on page loadBefore sharing artifact with user:
checkAuth()).execute() called at end of query chainImportant User Instructions: When artifact is opened, user needs to:
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();
Once basic connection works:
Remember: Always start with basic connection, then enhance incrementally based on user needs.