.claude/skills/rbac-security/SKILL.md
Role-based access control (RBAC) patterns, authentication wrappers, authorization checks, input validation with Zod schemas, security boundaries, server action security, real-time message validation, preventing common vulnerabilities like XSS and SQL injection, and security best practices for ree-board project
npx skillsauth add DW225/ree-board rbac-securityInstall 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.
CRITICAL: This skill is MANDATORY for all server actions and database operations.
Activate this skill when:
Rule: ALL server actions MUST use actionWithAuth or rbacWithAuth
Use when: Only user authentication is needed (no board-specific permissions)
// lib/actions/user/updateProfile.ts
"use server";
import { actionWithAuth } from "@/lib/actions/actionWithAuth";
export const updateProfile = async (name: string) =>
actionWithAuth(async (userId) => {
// userId is guaranteed to exist
await db.update(userTable).set({ name }).where(eq(userTable.id, userId));
return { success: true };
});
Use when: Board-specific permissions are required
// lib/actions/post/deletePost.ts
"use server";
import { rbacWithAuth } from "@/lib/actions/actionWithAuth";
export const deletePost = async (postId: string, boardId: string) =>
rbacWithAuth(boardId, async (userId) => {
// User's role is checked against board
// Only owner/member can proceed
await db.delete(postTable).where(eq(postTable.id, postId));
return { success: true };
});
Three Roles with Decreasing Permissions:
enum Role {
owner = "owner", // Full control (delete board, manage members)
member = "member", // Create/edit/delete own posts, vote
guest = "guest", // Read-only access
}
Role Permissions:
| Action | Owner | Member | Guest | | --------------- | ----- | ------ | ----- | | View board | ✅ | ✅ | ✅ | | Create post | ✅ | ✅ | ❌ | | Edit own post | ✅ | ✅ | ❌ | | Delete own post | ✅ | ✅ | ❌ | | Vote | ✅ | ✅ | ❌ | | Manage members | ✅ | ❌ | ❌ | | Delete board | ✅ | ❌ | ❌ |
Implementation:
// lib/actions/actionWithAuth.ts
export const rbacWithAuth = async <T>(
boardId: string,
callback: (userId: string, role: Role) => Promise<T>
): Promise<T> => {
// Verify session using Supabase
const session = await verifySession();
if (!session?.userId) {
throw new Error("Unauthorized");
}
// Check user's role for this board
const member = await db.query.memberTable.findFirst({
where: and(
eq(memberTable.boardId, boardId),
eq(memberTable.userId, session.userId)
),
});
if (!member) {
throw new Error("Access denied");
}
// Guest role has read-only access
if (member.role === "guest") {
throw new Error("Insufficient permissions");
}
return callback(session.userId, member.role);
};
Use Zod for Complex Validation:
import { z } from "zod";
const CreatePostSchema = z.object({
boardId: z.string().min(1, "Board ID is required"),
content: z
.string()
.min(1, "Content cannot be empty")
.max(1000, "Content too long"),
type: z.enum(["went_well", "to_improve", "action_items"]),
});
export const createPost = async (data: unknown) => {
// ✅ Validate input
const validated = CreatePostSchema.parse(data);
return rbacWithAuth(validated.boardId, async (userId) => {
const post = await db
.insert(postTable)
.values({
id: nanoid(),
userId,
...validated,
createdAt: new Date(),
})
.returning();
return post[0];
});
};
Critical for Ably Messages:
// lib/realtime/messageProcessors.ts
import { z } from "zod";
const PostUpdateSchema = z.object({
postId: z.string(),
content: z.string().min(1).max(1000),
timestamp: z.number(),
});
export const processPostUpdate = (data: unknown) => {
try {
// ✅ Validate message data
const validated = PostUpdateSchema.parse(data);
// ✅ Check message staleness (30s threshold)
const now = Date.now();
if (now - validated.timestamp > 30000) {
console.warn("Stale message discarded", {
age: now - validated.timestamp,
});
return;
}
// Process validated message
updatePostSignal(validated.postId, validated.content);
} catch (error) {
console.error("Invalid message data", { error, data });
}
};
Never Trust Client Input:
// ❌ BAD - Trusts client completely
export const deletePost = async (postId: string) => {
await db.delete(postTable).where(eq(postTable.id, postId));
};
// ✅ GOOD - Validates ownership
export const deletePost = async (postId: string, boardId: string) =>
rbacWithAuth(boardId, async (userId) => {
const post = await db.query.postTable.findFirst({
where: eq(postTable.id, postId),
});
// Verify post exists and user owns it
if (!post) {
throw new Error("Post not found");
}
if (post.userId !== userId) {
throw new Error("Cannot delete another user's post");
}
await db.delete(postTable).where(eq(postTable.id, postId));
});
Already Handled by React: React escapes content by default
For Markdown Content:
import ReactMarkdown from "react-markdown";
import rehypeSanitize from "rehype-sanitize";
// ✅ Sanitize user-generated markdown
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>{userContent}</ReactMarkdown>;
Drizzle ORM Prevents This:
// ✅ Parameterized queries (safe)
await db.select().from(postTable).where(eq(postTable.id, postId));
// ❌ Raw SQL (avoid unless necessary)
await db.execute(sql`SELECT * FROM post WHERE id = ${postId}`);
CRITICAL VULNERABILITY:
"use server";
// ❌ NEVER DO THIS
export async function deleteBoard(id: string) {
await db.delete(boardTable).where(eq(boardTable.id, id));
}
Correct:
"use server";
// ✅ ALWAYS USE AUTHENTICATION
export const deleteBoard = async (id: string) =>
rbacWithAuth(id, async (userId, role) => {
if (role !== "owner") {
throw new Error("Only board owner can delete");
}
await db.delete(boardTable).where(eq(boardTable.id, id));
});
Bad:
"use client";
function DeleteButton({ userRole, boardId }) {
// ❌ Client-side check can be bypassed
if (userRole === "owner") {
return <button onClick={() => deleteBoard(boardId)}>Delete</button>;
}
}
Good:
"use client";
function DeleteButton({ boardId }) {
// ✅ UI check for UX, server validates
return <button onClick={() => deleteBoard(boardId)}>Delete</button>;
}
// Server action validates role
export const deleteBoard = async (id: string) =>
rbacWithAuth(id, async (userId, role) => {
if (role !== "owner") {
throw new Error("Unauthorized");
}
// ...
});
Bad:
// ❌ API keys in client component
"use client";
const API_KEY = "secret-key"; // Exposed in bundle!
Good:
// ✅ API keys in server actions/environment
"use server";
export async function callExternalAPI() {
const apiKey = process.env.API_KEY; // Server-side only
// ...
}
Bad:
// ❌ Trusts message data completely
channel.subscribe("post:update", (message) => {
updatePost(message.data.postId, message.data.content);
});
Good:
// ✅ Validates before processing
channel.subscribe("post:update", (message) => {
const validated = PostUpdateSchema.safeParse(message.data);
if (!validated.success) {
console.error("Invalid message", validated.error);
return;
}
updatePost(validated.data.postId, validated.data.content);
});
lib/actions/actionWithAuth.ts - Authentication wrapper implementationslib/realtime/messageProcessors.ts - Real-time message validationproxy.ts - Supabase authentication proxy (Next.js 16)CLAUDE.md (Security section) - Comprehensive security guidelinesWhen creating/modifying features:
actionWithAuth or rbacWithAuthverifySession()memberTableCheck Board Ownership:
const isOwner = await db.query.memberTable.findFirst({
where: and(
eq(memberTable.boardId, boardId),
eq(memberTable.userId, userId),
eq(memberTable.role, "owner")
),
});
if (!isOwner) throw new Error("Unauthorized");
Verify Post Ownership:
const post = await db.query.postTable.findFirst({
where: eq(postTable.id, postId),
});
if (!post) {
throw new Error("Post not found");
}
if (post.userId !== userId) {
throw new Error("Not your post");
}
Last Updated: 2026-01-10
development
Jest testing strategies, test organization, factory patterns for test data, mocking strategies for authentication and external services, real-time message processor testing, test-driven development workflow, unit vs integration testing, fake timer usage for time-dependent tests, and testing best practices for ree-board project
development
Preact Signals for reactive state management, signal vs computed signal usage, batch updates for performance, action creator patterns, signal integration with React components, state management by domain (boards posts members), reactive patterns, and signal best practices for ree-board project
tools
Next.js 16 App Router patterns including server components, client components, server actions, route handlers, layouts, metadata API, dynamic routes, file conventions, data fetching, caching strategies, and Next.js best practices for building modern React applications
data-ai
Drizzle ORM best practices including schema design with relationships, database migrations, prepared statements for performance, transactions, indexes, Turso SQLite database operations, type safety patterns, query optimization, and database workflow for ree-board project