| name | grey-haven-security-practices |
| description | Grey Haven's security best practices - input validation, output sanitization, multi-tenant RLS, secret management with Doppler, rate limiting, OWASP Top 10 for TanStack/FastAPI stack. Use when implementing security-critical features. |
| skills | ["grey-haven-code-style","grey-haven-authentication-patterns","grey-haven-api-design-standards"] |
| allowed-tools | ["Read","Write","MultiEdit","Bash","Grep","Glob","TodoWrite"] |
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
brew install dopplerhq/cli/doppler
doppler login
cd /path/to/project
doppler setup
doppler run -- npm run dev
doppler run -- python app/main.py
Required Secrets (Doppler)
BETTER_AUTH_SECRET=<random-32-bytes>
JWT_SECRET_KEY=<random-32-bytes>
DATABASE_URL_ADMIN=postgresql://...
DATABASE_URL_AUTHENTICATED=postgresql://...
RESEND_API_KEY=re_...
STRIPE_SECRET_KEY=sk_...
OPENAI_API_KEY=sk-...
GOOGLE_CLIENT_SECRET=GOCSPX-...
GITHUB_CLIENT_SECRET=...
Accessing Secrets in Code
const apiKey = process.env.OPENAI_API_KEY!;
const apiKey = "sk-...";
import os
api_key = os.getenv("OPENAI_API_KEY")
api_key = "sk-..."
Input Validation
TypeScript (Zod Validation)
import { z } from "zod";
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) => {
const validated = UserCreateSchema.parse(data);
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):
pass
Output Sanitization
HTML Escaping (XSS Prevention)
function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>;
}
function UserProfile({ user }: { user: User }) {
return <div dangerouslySetInnerHTML={{ __html: user.bio }} />;
}
import DOMPurify from "isomorphic-dompurify";
function UserProfile({ user }: { user: User }) {
const sanitized = DOMPurify.sanitize(user.bio);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
SQL Injection Prevention
const user = await db.query.users.findFirst({
where: eq(users.email_address, email),
});
const user = await db.execute(
`SELECT * FROM users WHERE email = '${email}'`
);
user = await session.execute(
select(User).where(User.email_address == email)
)
user = await session.execute(
f"SELECT * FROM users WHERE email = '{email}'"
)
Multi-Tenant Security (RLS)
Enable RLS on All 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
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
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)
),
});
});
export const getUser = createServerFn("GET", async (userId: string) => {
return await db.query.users.findFirst({
where: eq(users.id, userId),
});
});
Rate Limiting
Redis-Based Rate Limiting
import { Redis } from "@upstash/redis";
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();
await rateLimit(, , );
});
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
export const auth = betterAuth({
session: {
expiresIn: 7 * 24 * 60 * 60,
updateAge: 24 * 60 * 60,
cookieOptions: {
httpOnly: true,
secure: true,
sameSite: "lax",
},
},
});
CORS Configuration
import { cors } from "@elysiajs/cors";
app.use(cors({
origin: [
"https://app.greyhaven.studio",
"https://admin.greyhaven.studio",
],
credentials: true,
maxAge: 86400,
}));
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
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
export const uploadFile = createServerFn("POST", async (file: File) => {
if (file.size > MAX_FILE_SIZE) {
throw new Error("File too large");
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error("Invalid file type");
}
const buffer = await file.arrayBuffer();
const header = new Uint8Array(buffer.slice(0, 4));
ext = file..().();
filename = ;
});
Environment-Specific Security
Development
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
DEBUG=true
Production
BETTER_AUTH_URL=https://app.greyhaven.studio
CORS_ORIGINS=https://app.greyhaven.studio
DEBUG=false
FORCE_HTTPS=true
Testing Security
import { describe, it, expect } from "vitest";
describe("Security", () => {
it("prevents tenant data leakage", async () => {
const userA = await createUser({ email: "a@example.com", tenantId: "A" });
const sessionB = await loginAs({ tenantId: "B" });
const result = await getUserById(userA.id, sessionB);
expect(result).toBeNull();
});
it("enforces rate limiting", async () => {
for (let i = 0; i < 11; i++) {
if (i < 10) {
await sendEmail({ to: "test@example.com" });
} else {
await expect(
sendEmail({ : })
)..();
}
}
});
});
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