skills/cleanexpo/backend-agent/SKILL.md
Handles backend/API/database work for Unite-Hub. Implements Next.js API routes, Supabase database operations, RLS policies, authentication, and third-party integrations (Gmail, Stripe).
npx skillsauth add aiskillstore/marketplace backend-agentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Backend Agent is responsible for all server-side work in Unite-Hub:
User says: "Create new API endpoint", "Fix database query", "Update RLS policies", "Implement Gmail integration"
Questions to Ask:
Step A: Locate Files
# Find API routes
find src/app/api -name "route.ts" | grep -i "contacts"
# Find database utilities
find src/lib -name "*.ts" | grep -i "db"
Step B: Read Current Code
// Use text_editor tool
text_editor.view("src/app/api/contacts/route.ts")
text_editor.view("src/lib/db.ts")
Step C: Identify Dependencies
Step A: Create API Route
All API routes in Unite-Hub follow this pattern:
// src/app/api/example/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase";
export async function POST(request: NextRequest) {
try {
// 1. Parse request body
const body = await request.json();
const { workspaceId, action, ...params } = body;
// 2. Validate input
if (!workspaceId) {
return NextResponse.json(
{ error: "workspaceId is required" },
{ status: 400 }
);
}
// 3. Get Supabase client
const supabase = createClient();
// 4. Check authentication (if needed)
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// 5. Perform database operation
const { data, error } = await supabase
.from("contacts")
.select("*")
.eq("workspace_id", workspaceId) // CRITICAL: Workspace filter
.eq("organization_id", user.organization_id); // CRITICAL: Org filter
if (error) {
console.error("Database error:", error);
return NextResponse.json(
{ error: "Database query failed" },
{ status: 500 }
);
}
// 6. Return success response
return NextResponse.json({
success: true,
data,
count: data.length
});
} catch (error) {
console.error("API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
// Support OPTIONS for CORS
export async function OPTIONS(request: NextRequest) {
return NextResponse.json({}, { status: 200 });
}
Step B: Database Operations
All database operations MUST use workspace filtering:
// ❌ BAD - No workspace filter
const { data } = await supabase
.from("contacts")
.select("*");
// ✅ GOOD - Workspace filtered
const { data } = await supabase
.from("contacts")
.select("*")
.eq("workspace_id", workspaceId)
.eq("organization_id", orgId);
Required filters for data isolation:
.eq("workspace_id", workspaceId) - Workspace scope.eq("organization_id", orgId) - Organization scope (top-level)Step C: Update src/lib/db.ts Wrapper
The db.ts wrapper provides consistent database access:
// src/lib/db.ts
import { createClient } from "@/lib/supabase";
export const db = {
contacts: {
async listByWorkspace(workspaceId: string) {
const supabase = createClient();
const { data, error } = await supabase
.from("contacts")
.select("*")
.eq("workspace_id", workspaceId)
.order("created_at", { ascending: false });
if (error) throw error;
return data || [];
},
async getById(contactId: string, workspaceId: string) {
const supabase = createClient();
const { data, error } = await supabase
.from("contacts")
.select("*")
.eq("id", contactId)
.eq("workspace_id", workspaceId)
.single();
if (error) throw error;
return data;
},
async create(contact: ContactInput, workspaceId: string) {
const supabase = createClient();
const { data, error } = await supabase
.from("contacts")
.insert([{ ...contact, workspace_id: workspaceId }])
.select()
.single();
if (error) throw error;
return data;
},
async update(contactId: string, updates: Partial<ContactInput>, workspaceId: string) {
const supabase = createClient();
const { data, error } = await supabase
.from("contacts")
.update(updates)
.eq("id", contactId)
.eq("workspace_id", workspaceId)
.select()
.single();
if (error) throw error;
return data;
}
},
// Similar patterns for campaigns, emails, etc.
};
CRITICAL FIX for V1: Add missing import in src/lib/db.ts:58
// Line 1 - Add import
import { createClient, getSupabaseServer } from "./supabase";
// Line 58 - Fix usage
const supabaseServer = getSupabaseServer();
const { data: workspace, error } = await supabaseServer
.from("workspaces")
.select("*")
.eq("id", workspaceId)
.single();
Pattern 1: Client-Side Auth (Browser)
import { createClient } from "@/lib/supabase";
export async function GET(request: NextRequest) {
const supabase = createClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// User is authenticated, proceed
}
Pattern 2: Server-Side Auth (API Routes)
import { getSupabaseServer } from "@/lib/supabase";
export async function POST(request: NextRequest) {
const supabase = getSupabaseServer();
const { data: { session }, error } = await supabase.auth.getSession();
if (error || !session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Session is valid, proceed
}
CRITICAL for V1: Re-enable authentication on all API routes
Many routes currently have:
// TODO: Re-enable authentication in production
// const { auth } = await import("@/lib/auth");
// const session = await auth();
Action Required: Remove TODO comments and re-enable auth checks.
All Supabase tables MUST have RLS policies:
-- Enable RLS on table
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see contacts in their workspace
CREATE POLICY "Users can view workspace contacts"
ON contacts
FOR SELECT
USING (
workspace_id IN (
SELECT w.id
FROM workspaces w
JOIN user_organizations uo ON uo.organization_id = w.organization_id
WHERE uo.user_id = auth.uid()
)
);
-- Policy: Users can insert contacts in their workspace
CREATE POLICY "Users can create workspace contacts"
ON contacts
FOR INSERT
WITH CHECK (
workspace_id IN (
SELECT w.id
FROM workspaces w
JOIN user_organizations uo ON uo.organization_id = w.organization_id
WHERE uo.user_id = auth.uid()
)
);
-- Policy: Users can update contacts in their workspace
CREATE POLICY "Users can update workspace contacts"
ON contacts
FOR UPDATE
USING (
workspace_id IN (
SELECT w.id
FROM workspaces w
JOIN user_organizations uo ON uo.organization_id = w.organization_id
WHERE uo.user_id = auth.uid()
)
);
CRITICAL for V1: Verify RLS policies exist for:
contactscampaignsdrip_campaignsemailsgenerated_contentcampaign_enrollmentsGmail API Integration
// src/lib/integrations/gmail.ts
import { google } from "googleapis";
export async function getGmailClient(accessToken: string) {
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_CALLBACK_URL
);
oauth2Client.setCredentials({ access_token: accessToken });
return google.gmail({ version: "v1", auth: oauth2Client });
}
export async function fetchEmails(gmail: any, maxResults = 50) {
const res = await gmail.users.messages.list({
userId: "me",
maxResults,
q: "is:unread", // Only unread emails
});
const messages = res.data.messages || [];
const emails = [];
for (const message of messages) {
const email = await gmail.users.messages.get({
userId: "me",
id: message.id,
});
emails.push(email.data);
}
return emails;
}
Claude AI Integration
// src/lib/integrations/claude.ts
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
export async function generateContent({
contactName,
contactCompany,
interactionHistory,
contentType,
}: {
contactName: string;
contactCompany: string;
interactionHistory: string;
contentType: "followup" | "proposal" | "case_study";
}) {
const message = await client.messages.create({
model: "claude-opus-4-5-20251101",
max_tokens: 2000,
thinking: {
type: "enabled",
budget_tokens: 7500,
},
messages: [
{
role: "user",
content: `Generate a personalized ${contentType} email for ${contactName} at ${contactCompany}.
Interaction history:
${interactionHistory}
Generate a professional, personalized email that references their previous interactions.`,
},
],
});
return message.content[0].type === "text" ? message.content[0].text : null;
}
Structured Error Responses:
// Error response format
return NextResponse.json(
{
error: "Error message for user",
code: "ERROR_CODE",
details: isDev ? error.message : undefined, // Only in development
},
{ status: 500 }
);
Audit Logging:
// Log all important actions
await supabase.from("auditLogs").insert({
organization_id: orgId,
user_id: userId,
action: "contact_created",
resource_type: "contact",
resource_id: contact.id,
context: {
contact_email: contact.email,
source: "api",
},
ip_address: request.headers.get("x-forwarded-for"),
user_agent: request.headers.get("user-agent"),
created_at: new Date().toISOString(),
});
Example: /api/agents/contact-intelligence missing workspace filter
Steps:
src/app/api/agents/contact-intelligence/route.ts.eq("workspace_id", workspaceId)Code:
// Before
const { data: contacts } = await supabase.from("contacts").select("*");
// After
if (!workspaceId) {
return NextResponse.json(
{ error: "workspaceId is required" },
{ status: 400 }
);
}
const { data: contacts, error } = await supabase
.from("contacts")
.select("*")
.eq("workspace_id", workspaceId);
if (error) {
console.error("Database error:", error);
return NextResponse.json(
{ error: "Failed to fetch contacts" },
{ status: 500 }
);
}
Example: Create /api/contacts/bulk-update endpoint
Steps:
src/app/api/contacts/bulk-update/route.tsCode:
// src/app/api/contacts/bulk-update/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { workspaceId, contactIds, updates } = body;
// Validate input
if (!workspaceId || !contactIds || !Array.isArray(contactIds)) {
return NextResponse.json(
{ error: "Invalid input" },
{ status: 400 }
);
}
// Get authenticated user
const supabase = createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Perform bulk update
const { data, error } = await supabase
.from("contacts")
.update(updates)
.in("id", contactIds)
.eq("workspace_id", workspaceId) // CRITICAL: Workspace filter
.select();
if (error) {
console.error("Bulk update error:", error);
return NextResponse.json(
{ error: "Bulk update failed" },
{ status: 500 }
);
}
// Log audit event
await supabase.from("auditLogs").insert({
organization_id: user.organization_id,
user_id: user.id,
action: "contacts_bulk_updated",
resource_type: "contact",
context: {
updated_count: data.length,
contact_ids: contactIds,
updates,
},
});
return NextResponse.json({
success: true,
updated: data.length,
contacts: data,
});
} catch (error) {
console.error("API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Example: Add RLS policy for new table
Steps:
Code:
-- Enable RLS
ALTER TABLE new_table ENABLE ROW LEVEL SECURITY;
-- SELECT policy
CREATE POLICY "Users can view workspace records"
ON new_table
FOR SELECT
USING (
workspace_id IN (
SELECT w.id
FROM workspaces w
JOIN user_organizations uo ON uo.organization_id = w.organization_id
WHERE uo.user_id = auth.uid()
)
);
-- INSERT policy
CREATE POLICY "Users can create workspace records"
ON new_table
FOR INSERT
WITH CHECK (
workspace_id IN (
SELECT w.id
FROM workspaces w
JOIN user_organizations uo ON uo.organization_id = w.organization_id
WHERE uo.user_id = auth.uid()
)
);
-- UPDATE policy
CREATE POLICY "Users can update workspace records"
ON new_table
FOR UPDATE
USING (
workspace_id IN (
SELECT w.id
FROM workspaces w
JOIN user_organizations uo ON uo.organization_id = w.organization_id
WHERE uo.user_id = auth.uid()
)
);
-- Test policy
SELECT * FROM new_table; -- Should only return user's workspace records
// ❌ BAD - N+1 query problem
const contacts = await db.contacts.listByWorkspace(workspaceId);
for (const contact of contacts) {
const emails = await db.emails.listByContact(contact.id); // N queries!
}
// ✅ GOOD - Single query with join
const { data } = await supabase
.from("contacts")
.select(`
*,
emails (*)
`)
.eq("workspace_id", workspaceId);
-- Create indexes for frequently queried columns
CREATE INDEX idx_contacts_workspace_id ON contacts(workspace_id);
CREATE INDEX idx_contacts_ai_score ON contacts(ai_score DESC);
CREATE INDEX idx_emails_contact_id ON emails(contact_id);
CREATE INDEX idx_campaign_enrollments_contact ON campaign_enrollments(contact_id);
const PAGE_SIZE = 20;
const { data, error, count } = await supabase
.from("contacts")
.select("*", { count: "exact" })
.eq("workspace_id", workspaceId)
.order("created_at", { ascending: false })
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1);
return {
contacts: data,
totalCount: count,
page,
pageSize: PAGE_SIZE,
totalPages: Math.ceil(count / PAGE_SIZE),
};
✅ Authentication:
✅ Authorization:
✅ Input Validation:
✅ Data Isolation:
✅ Error Handling:
✅ Audit Logging:
What We Fix for V1:
src/lib/db.ts missing importWhat We Do NOT Build for V1:
The Backend Agent works with:
development
Apple Human Interface Guidelines for content display components. Use this skill when the user asks about charts component, collection view, image view, web view, color well, image well, activity view, lockup, data visualization, content display, displaying images, rendering web content, color pickers, or presenting collections of items in Apple apps. Also use when the user says how should I display charts, what's the best way to show images, should I use a web view, how do I build a grid of items, what component shows media, or how do I present a share sheet. Cross-references: hig-foundations for color/typography/accessibility, hig-patterns for data visualization patterns, hig-components-layout for structural containers, hig-platforms for platform-specific component behavior.
tools
Automate HelpDesk tasks via Rube MCP (Composio): list tickets, manage views, use canned responses, and configure custom fields. Always search tools first for current schemas.
testing
Expert Haskell engineer specializing in advanced type systems, pure functional design, and high-reliability software. Use PROACTIVELY for type-level programming, concurrency, and architecture guidance.
tools
GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.