# Grey Haven Security Practices

> 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.

- Skill: `majiayu000/grey-haven-security-practices` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/grey-haven-security-practices`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/grey-haven-security-practices/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/grey-haven-security-practices

---


# 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

```bash
# 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)

```bash
# 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

```typescript
// [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!
```

```python
# [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)

```typescript
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)

```python
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)

```typescript
// [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

```typescript
// [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!
);
```

```python
# [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

```sql
-- 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

```sql
-- 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

```typescript
// [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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
// TanStack Start
import { cors } from "@elysiajs/cors";

app.use(cors({
  origin: [
    "https://app.greyhaven.studio",
    "https://admin.greyhaven.studio",
  ],
  credentials: true,
  maxAge: 86400,
}));
```

```python
# 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

```typescript
// 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

```bash
# Doppler: dev config
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
DEBUG=true
```

### Production

```bash
# Doppler: production config
BETTER_AUTH_URL=https://app.greyhaven.studio
CORS_ORIGINS=https://app.greyhaven.studio
DEBUG=false
FORCE_HTTPS=true
```

## Testing Security

```typescript
// 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: "a@example.com", 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: "test@example.com" });
      } else {
        await expect(
          sendEmail({ to: "test@example.com" })
        ).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

1. **Doppler**: ALWAYS use for secrets (never commit to git)
2. **Input validation**: Validate ALL user input (Zod/Pydantic)
3. **RLS**: Enable on all multi-tenant tables
4. **tenant_id**: ALWAYS filter by tenant_id
5. **Rate limiting**: Implement on expensive operations
6. **HTTPS only**: Force HTTPS in production
7. **SQL injection**: Use ORM, never concatenate SQL
8. **XSS**: React auto-escapes, sanitize dangerouslySetInnerHTML
9. **CORS**: Whitelist specific origins
10. **Sessions**: httpOnly, secure, sameSite cookies

