grey-haven-security-practices
About
This Claude Skill provides Grey Haven's security best practices for TanStack Start and FastAPI applications. It covers critical security measures including input validation, output sanitization, multi-tenant RLS, secret management with Doppler, rate limiting, and OWASP Top 10 compliance. Use this skill when implementing security-critical features to ensure robust application security.
Quick Install
Claude Code
Recommended/plugin add https://github.com/greyhaven-ai/claude-code-configgit clone https://github.com/greyhaven-ai/claude-code-config.git ~/.claude/skills/grey-haven-security-practicesCopy and paste this command in Claude Code to install this skill
Documentation
Grey Haven Security Practices
Follow Grey Haven Studio's security best practices for TanStack Start and FastAPI applications.
Secret Management with Doppler
CRITICAL: NEVER commit secrets to git. Always use Doppler.
Doppler Setup
# Install Doppler CLI
brew install dopplerhq/cli/doppler
# Authenticate
doppler login
# Setup project
cd /path/to/project
doppler setup
# Access secrets
doppler run -- npm run dev # TypeScript
doppler run -- python app/main.py # Python
Required Secrets (Doppler)
# Auth
BETTER_AUTH_SECRET=<random-32-bytes>
JWT_SECRET_KEY=<random-32-bytes>
# Database
DATABASE_URL_ADMIN=postgresql://...
DATABASE_URL_AUTHENTICATED=postgresql://...
# APIs
RESEND_API_KEY=re_...
STRIPE_SECRET_KEY=sk_...
OPENAI_API_KEY=sk-...
# OAuth
GOOGLE_CLIENT_SECRET=GOCSPX-...
GITHUB_CLIENT_SECRET=...
Accessing Secrets in Code
// [OK] Correct - Use process.env (Doppler provides at runtime)
const apiKey = process.env.OPENAI_API_KEY!;
// [X] Wrong - Hardcoded secrets
const apiKey = "sk-..."; // NEVER DO THIS!
# [OK] Correct - Use os.getenv (Doppler provides at runtime)
import os
api_key = os.getenv("OPENAI_API_KEY")
# [X] Wrong - Hardcoded secrets
api_key = "sk-..." # NEVER DO THIS!
Input Validation
TypeScript (Zod Validation)
import { z } from "zod";
// [OK] Validate all user input
const UserCreateSchema = z.object({
email_address: z.string().email().max(255),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150),
});
export const createUser = createServerFn("POST", async (data: unknown) => {
// Validate input
const validated = UserCreateSchema.parse(data);
// Now safe to use
await db.insert(users).values(validated);
});
Python (Pydantic Validation)
from pydantic import BaseModel, EmailStr, Field, validator
class UserCreate(BaseModel):
"""User creation schema with validation."""
email_address: EmailStr
name: str = Field(min_length=1, max_length=100)
age: int = Field(ge=0, le=150)
@validator("name")
def name_must_not_contain_special_chars(cls, v):
if not v.replace(" ", "").isalnum():
raise ValueError("Name must be alphanumeric")
return v
@router.post("/users", response_model=UserResponse)
async def create_user(data: UserCreate):
# Pydantic validates automatically
# data is now safe to use
pass
Output Sanitization
HTML Escaping (XSS Prevention)
// [OK] React automatically escapes in JSX
function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>; // Safe, auto-escaped
}
// [X] Dangerous - Raw HTML
function UserProfile({ user }: { user: User }) {
return <div dangerouslySetInnerHTML={{ __html: user.bio }} />; // UNSAFE!
}
// [OK] If HTML needed, sanitize first
import DOMPurify from "isomorphic-dompurify";
function UserProfile({ user }: { user: User }) {
const sanitized = DOMPurify.sanitize(user.bio);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
SQL Injection Prevention
// [OK] Use parameterized queries (Drizzle)
const user = await db.query.users.findFirst({
where: eq(users.email_address, email), // Safe, parameterized
});
// [X] Never concatenate SQL
const user = await db.execute(
`SELECT * FROM users WHERE email = '${email}'` // SQL INJECTION!
);
# [OK] Use ORM (SQLModel/SQLAlchemy)
user = await session.execute(
select(User).where(User.email_address == email) # Safe, parameterized
)
# [X] Never concatenate SQL
user = await session.execute(
f"SELECT * FROM users WHERE email = '{email}'" # SQL INJECTION!
)
Multi-Tenant Security (RLS)
Enable RLS on All Tables
-- ALWAYS enable RLS for multi-tenant tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE teams ENABLE ROW LEVEL SECURITY;
Tenant Isolation Policies
-- Authenticated users see only their tenant's data
CREATE POLICY "Tenant isolation for users"
ON users FOR ALL TO authenticated
USING (tenant_id = (current_setting('request.jwt.claims')::json->>'tenant_id')::uuid);
Always Include tenant_id in Queries
// [OK] Correct - Tenant isolation enforced
export const getUser = createServerFn("GET", async (userId: string) => {
const session = await getSession();
const tenantId = session.user.tenant_id;
return await db.query.users.findFirst({
where: and(
eq(users.id, userId),
eq(users.tenant_id, tenantId) // REQUIRED!
),
});
});
// [X] Wrong - Missing tenant check (security vulnerability!)
export const getUser = createServerFn("GET", async (userId: string) => {
return await db.query.users.findFirst({
where: eq(users.id, userId), // Can access ANY tenant's users!
});
});
Rate Limiting
Redis-Based Rate Limiting
import { Redis } from "@upstash/redis";
// Doppler provides REDIS_URL
const redis = new Redis({ url: process.env.REDIS_URL! });
async function rateLimit(identifier: string, limit: number, window: number) {
const key = `rate-limit:${identifier}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, window);
}
if (count > limit) {
throw new Error("Rate limit exceeded");
}
return { success: true, remaining: limit - count };
}
export const sendEmail = createServerFn("POST", async (data) => {
const session = await getSession();
// Rate limit: 10 emails per hour per user
await rateLimit(`email:${session.user.id}`, 10, 3600);
// Send email...
});
Authentication Security
Password Requirements
const PasswordSchema = z.string()
.min(12, "Password must be at least 12 characters")
.regex(/[A-Z]/, "Must contain uppercase letter")
.regex(/[a-z]/, "Must contain lowercase letter")
.regex(/[0-9]/, "Must contain number")
.regex(/[^A-Za-z0-9]/, "Must contain special character");
Session Security
// lib/server/auth.ts
export const auth = betterAuth({
session: {
expiresIn: 7 * 24 * 60 * 60, // 7 days
updateAge: 24 * 60 * 60, // Refresh daily
cookieOptions: {
httpOnly: true, // Prevent XSS
secure: true, // HTTPS only
sameSite: "lax", // CSRF protection
},
},
});
CORS Configuration
// TanStack Start
import { cors } from "@elysiajs/cors";
app.use(cors({
origin: [
"https://app.greyhaven.studio",
"https://admin.greyhaven.studio",
],
credentials: true,
maxAge: 86400,
}));
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.greyhaven.studio",
"https://admin.greyhaven.studio",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["*"],
max_age=86400,
)
File Upload Security
// Validate file type and size
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
export const uploadFile = createServerFn("POST", async (file: File) => {
// Validate size
if (file.size > MAX_FILE_SIZE) {
throw new Error("File too large");
}
// Validate type
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error("Invalid file type");
}
// Validate content (check file header, not just extension)
const buffer = await file.arrayBuffer();
const header = new Uint8Array(buffer.slice(0, 4));
// Check for valid image headers
// JPEG: FF D8 FF
// PNG: 89 50 4E 47
// etc.
// Generate safe filename (prevent path traversal)
const ext = file.name.split(".").pop();
const filename = `${crypto.randomUUID()}.${ext}`;
// Upload to secure storage...
});
Environment-Specific Security
Development
# Doppler: dev config
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
DEBUG=true
Production
# Doppler: production config
BETTER_AUTH_URL=https://app.greyhaven.studio
CORS_ORIGINS=https://app.greyhaven.studio
DEBUG=false
FORCE_HTTPS=true
Testing Security
// tests/integration/security.test.ts
import { describe, it, expect } from "vitest";
describe("Security", () => {
it("prevents tenant data leakage", async () => {
// Create user in tenant A
const userA = await createUser({ email: "[email protected]", tenantId: "A" });
// Try to access as tenant B user
const sessionB = await loginAs({ tenantId: "B" });
const result = await getUserById(userA.id, sessionB);
// Should return null or 404, not tenant A's user
expect(result).toBeNull();
});
it("enforces rate limiting", async () => {
// Make 11 requests (limit is 10)
for (let i = 0; i < 11; i++) {
if (i < 10) {
await sendEmail({ to: "[email protected]" });
} else {
await expect(
sendEmail({ to: "[email protected]" })
).rejects.toThrow("Rate limit exceeded");
}
}
});
});
When to Apply This Skill
Use this skill when:
- Handling user input
- Implementing authentication
- Working with sensitive data
- Configuring API endpoints
- Writing database queries
- Implementing file uploads
- Setting up CORS
- Managing secrets with Doppler
Critical Reminders
- Doppler: ALWAYS use for secrets (never commit to git)
- Input validation: Validate ALL user input (Zod/Pydantic)
- RLS: Enable on all multi-tenant tables
- tenant_id: ALWAYS filter by tenant_id
- Rate limiting: Implement on expensive operations
- HTTPS only: Force HTTPS in production
- SQL injection: Use ORM, never concatenate SQL
- XSS: React auto-escapes, sanitize dangerouslySetInnerHTML
- CORS: Whitelist specific origins
- Sessions: httpOnly, secure, sameSite cookies
GitHub Repository
Related Skills
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
creating-opencode-plugins
MetaThis skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.
nestjs
MetaThis skill provides NestJS development standards and architectural patterns for building domain-centric applications. It covers modular design, dependency injection, decorator patterns, and key framework features like controllers, services, middleware, and interceptors. Use it when developing NestJS applications, implementing APIs, configuring microservices, or integrating with databases.
