Anti-Patterns from Frappe & Odoo
Purpose: This document catalogs critical mistakes from Frappe and Odoo that Framework M must not repeat.
Structure:
- ✅ Tackled = Already designed into Framework M (kept for reference)
- ⚠️ Remaining = Still needs explicit implementation
✅ TACKLED ISSUES
These issues have design decisions in place. Listed for reference only.
| # | Issue | Solution | Where |
|---|---|---|---|
| 1 | name as Primary Key | id (UUID) as PK, name as unique index | Phase 01, 02 |
| 2 | Global State (frappe.session.user) | Explicit user_context parameter | Phase 02 |
| 3 | Metadata in Database | Code-first Pydantic models | Phase 01, 02 |
| 4 | Synchronous/Blocking | Async native (Litestar + SQLAlchemy Async) | Phase 01 |
| 5 | Monkey Patching | Entrypoint-based overrides | Phase 01 |
| 6 | No Dependency Injection | dependency-injector library | Phase 01 |
| 7 | Weak/No Type Hints | 100% type hints, mypy --strict | Phase 01 |
| 8 | Database-Specific Features | Portable SQLAlchemy types only | Phase 02 |
| 9 | Implicit Transactions | Explicit transaction context managers | Phase 02 |
| 10 | No Event Sourcing / Audit Trail | Every state change emits event | Phase 02, 04 |
| 11 | Hard-coded Hooks | Event bus with multiple subscribers | Phase 04 |
| 12 | Permission Checks in Business Logic | Repository-level enforcement | Phase 03 |
| 13 | Session-Only Auth | Stateless JWT/header-based auth | Phase 03 |
| 14 | eval()/exec() in User Input | JMESPath/SimpleEval only | Phase 10 |
| 15 | Hard Delete Only | Soft delete with deleted_at | Phase 02 |
| 16 | Opt-out API Exposure | Opt-in api_resource = True | Phase 03 |
| 17 | Timezone-Naive Datetimes | DateTime(timezone=True) in SchemaMapper | Phase 02 |
| 18 | N+1 Query Problem | load_children_for_parents() (select_in_loading) | Phase 02 |
| 19 | Unbounded Queries | DEFAULT_LIMIT=20, MAX_LIMIT=1000 enforced | Phase 02 |
| 22 | Unrestricted File Uploads | File DocType with extension whitelist, MIME validation | Phase 06 |
| 25 | No Circuit Breaker (Email) | EmailQueue with async processing, not blocking | Phase 06 |
| 26 | No API Versioning | URL-based versioning /api/v1/ prefix | Phase 03 |
| 29 | Health Check Endpoints Missing | /studio/api/health endpoint implemented | Phase 07 |
| 30 | Caching Layer Missing | RedisCacheAdapter + GenericRepository integration | Phase 02, 04 |
| 32 | Over-Customization Upgrade Trap | Override mechanism + event-based extension | Phase 08 |
⚠️ REMAINING ISSUES TO TACKLE
These issues need explicit implementation. Each includes problem, impact, and required solution.
20. Missing Rate Limiting
❌ The Problem (Frappe)
# No built-in rate limiting
# Attackers brute-force login, spam API, exhaust resources
🔥 Why It's Bad
- Brute Force Attacks: Unlimited login attempts
- Resource Exhaustion: API spam causes denial of service
- Cost Explosion: Cloud bills skyrocket from abuse
- Real-world: Security advisories list this as vulnerability
✅ Required Solution
# Multi-level rate limiting
@rate_limit(max_requests=5, window_seconds=60, key="ip") # Login
async def login(credentials: LoginRequest) -> Token:
...
@rate_limit(max_requests=100, window_seconds=60, key="user_id") # API
async def create_document(data: CreateRequest) -> Document:
...
Phase: 03/10 (Middleware) Priority: HIGH
21. Secrets in Logs
❌ The Problem (Frappe)
frappe.log(f"Login attempt: {username}, {password}") # Oops!
🔥 Why It's Bad
- Credential Leak: Passwords visible in log files
- Compliance Violation: PII in logs breaks GDPR
- Breach Amplification: Attacker reads logs, gets more access
✅ Required Solution
import structlog
REDACTED_FIELDS = {"password", "token", "secret", "api_key", "credit_card"}
def redact_processor(logger, method_name, event_dict):
for key in list(event_dict.keys()):
if key.lower() in REDACTED_FIELDS:
event_dict[key] = "[REDACTED]"
return event_dict
logger = structlog.wrap_logger(
structlog.get_logger(),
processors=[redact_processor, ...]
)
Phase: 10 (Logging) Priority: HIGH
23. Missing Request ID Tracing
❌ The Problem (Frappe)
# "Error in Job" - which request triggered it? Unknown.
# Can't correlate logs across services
🔥 Why It's Bad
- Debugging Nightmare: Can't trace request → error
- Distributed Tracing: Microservices can't correlate
- Support Tickets: User says "it failed" - can't find the log
✅ Required Solution
class RequestIdMiddleware:
async def __call__(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
# Add to logging context
structlog.contextvars.bind_contextvars(request_id=request_id)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
# Every log includes request_id automatically
logger.info("Processing invoice", invoice_id=inv.id)
# {"request_id": "abc-123", "message": "Processing invoice", ...}
Phase: 03 (Middleware) Priority: MEDIUM
24. No Idempotency Keys for Mutations
❌ The Problem (Frappe)
# Network timeout → client retries → two invoices created!
🔥 Why It's Bad
- Duplicate Records: Retries create multiple copies
- Double Charges: Payment processed twice
- Data Corruption: Inconsistent state
✅ Required Solution
@post("/api/v1/{doctype}")
async def create_document(
data: CreateRequest,
idempotency_key: str = Header(alias="Idempotency-Key", default=None),
cache: CacheProtocol = Depends(),
) -> Document:
if idempotency_key:
cached = await cache.get(f"idempotency:{idempotency_key}")
if cached:
return cached # Return same response
result = await repo.save(data)
if idempotency_key:
await cache.set(f"idempotency:{idempotency_key}", result, ttl=86400)
return result
Phase: 03 (API Layer) Priority: MEDIUM
27. Unsafe Deserialization
❌ The Problem (Frappe/Odoo)
# Odoo uses pickle for sessions (RCE!)
import pickle
data = pickle.loads(user_controlled_bytes) # Arbitrary code execution
# YAML !!python/object tag executes code
yaml.load(user_input, Loader=yaml.FullLoader) # Unsafe!
🔥 Why It's Bad
- Remote Code Execution: Pickle can execute arbitrary Python
- Complete Compromise: Attacker owns the server
- Real-world: CVE-2022-XXXX (Odoo)
✅ Required Solution
# ✅ Use JSON only for user data
import json
data = json.loads(user_input) # Safe
# ✅ If YAML needed, use safe_load
import yaml
data = yaml.safe_load(user_input) # No code execution
# ❌ NEVER in production code:
pickle.loads(...)
yaml.load(..., Loader=yaml.FullLoader)
Phase: ALL (Code Audit) Priority: HIGH
28. Missing CSRF Protection
❌ The Problem (Frappe)
# CSRF tokens inconsistently applied
# Attacker page can POST to /api/ on behalf of logged-in user
🔥 Why It's Bad
- Account Takeover: Attacker changes user's password
- Data Theft: Attacker exports user's data
- Financial Fraud: Attacker initiates transfers
- Real-world: Security advisory for Frappe
✅ Required Solution
# 1. SameSite cookies (primary defense)
response.set_cookie(
key="session",
value=token,
httponly=True,
secure=True,
samesite="lax", # Or "strict"
)
# 2. Verify Origin header for mutations
async def csrf_check(request: Request):
if request.method in ("POST", "PUT", "DELETE"):
origin = request.headers.get("Origin")
if origin and origin not in ALLOWED_ORIGINS:
raise CSRFError("Origin not allowed")
Phase: 03 (Middleware) Priority: HIGH
31. MariaDB-Specific Issues (From Production)
❌ The Problem (Frappe)
# Frappe is tightly coupled to MariaDB
# Many production issues stem from MariaDB-specific problems:
# - InnoDB buffer pool misconfiguration
# - Slow queries due to missing indexes
# - Connection pool exhaustion
🔥 Why It's Bad
- Real-world Incidents: Frappe Cloud downtime traced to MariaDB bugs
- Scaling Limit: Vertical scaling only for single-node MariaDB
- Recovery Time: Database issues require manual intervention
✅ Required Solution
# Framework M: Database agnostic + proper pool config
# 1. SQLAlchemy handles dialect differences
# 2. Proper connection pooling
engine = create_async_engine(
DATABASE_URL,
pool_size=20,
max_overflow=10,
pool_timeout=30,
pool_recycle=3600,
pool_pre_ping=True, # Health check connections
)
# 3. Read replicas for scaling
# Configured via Phase 10 multi-bind support
Phase: 02, 10 (Database) Priority: MEDIUM
Summary: Remaining Work
| Priority | Issues |
|---|---|
| HIGH | |
| MEDIUM | #23 Request ID, #24 Idempotency, |
Checklist for Every Feature
Before implementing any feature, verify:
- Datetimes are timezone-aware (UTC)?
- Queries are paginated with max limit?
- No N+1 queries (use eager loading)?
- Rate limiting applied (if user-facing)?
- Secrets redacted from logs?
- File uploads validated (extension + magic bytes)?
- Request ID propagated through logs?
- No pickle/unsafe YAML?
- CSRF protection on mutations?
- Circuit breaker on external calls?