ContentFlow implements defense-in-depth security across authentication, data encryption, webhook integrity, and row-level access control.
| Asset | Sensitivity | Storage |
|---|---|---|
| OAuth tokens (access/refresh) | Critical | AES-256-GCM encrypted in Supabase |
| API keys | Critical | bcrypt hashed in Supabase |
| Stripe customer data | High | Stripe-managed, only IDs stored |
| User PII (email, name) | High | Supabase with RLS |
| Post content | Medium | Supabase with RLS |
| Webhook signing secrets | High | Supabase, per-webhook unique |
- External attacker — API brute-force, SSRF, injection
- Malicious user — IDOR, privilege escalation, payment bypass
- Compromised dependency — supply chain attack
- Insider threat — credential exposure, data exfiltration
Internet ──► API Gateway ──► FastAPI ──► Supabase (DB)
│ ├──► Redis (cache/queue)
│ ├──► Stripe (billing)
│ ├──► Resend (email)
│ └──► External platforms (OAuth)
│
└──► Stripe Webhooks (inbound)
┌─────────────────────────────────────────────────────┐
│ Client Request │
└─────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ RequestValidatorMiddleware │
│ • Body size limit (10MB / 100MB for uploads) │
│ • Scanner user-agent blocking │
└─────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ SecurityHeadersMiddleware │
│ • HSTS, CSP, X-Frame-Options, X-Content-Type │
│ • Permissions-Policy, Referrer-Policy │
└─────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ RequestIdMiddleware + LoggingMiddleware │
│ • UUID v4 per request │
│ • Structured logging (no PII) │
└─────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ Authentication (get_current_user / get_admin_user) │
│ • API key validation (bcrypt, timing-safe) │
│ • User lookup + workspace resolution │
│ • Admin: enterprise plan enforcement │
└─────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ Authorization (per-endpoint) │
│ • owner_id filtering on all queries │
│ • workspace_id scoping │
│ • Billing quota checks │
└─────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ Business Logic + Supabase (RLS enabled) │
└─────────────────────────────────────────────────────┘
Client API Server
│ │
│ X-API-Key: cf_live_xxxxx │
│ ─────────────────────────────► │
│ │ 1. Validate prefix (cf_live_ or cf_test_)
│ │ 2. Lookup key_prefix in api_keys table
│ │ 3. bcrypt.checkpw(raw_key, hashed_key)
│ │ 4. Resolve user_id → AuthenticatedUser
│ 200 OK │
│ ◄───────────────────────────── │
- Raw key format:
{prefix}_{token}(e.g.,cf_live_Ab3x...) - Token:
secrets.token_urlsafe(24)(192 bits of entropy) - Storage: Only the bcrypt hash is stored in the database
- The raw key is shown exactly once at creation time
| Prefix | Environment | Description |
|---|---|---|
cf_live_ |
Production | Full access to all endpoints |
cf_test_ |
Sandbox | Rate-limited, no real publishing |
cf_admin_ |
Admin | Elevated privileges, enterprise-only |
- bcrypt with automatic salt (cost factor 12)
- Raw keys never stored or logged
key_previewfield shows onlycf_live_...xxxx(last 4 chars)- Keys can be deactivated (
is_active = false) without deletion - Key rotation with 24-hour grace period
Connected social account tokens (access + refresh) are encrypted at rest using AES-256-GCM (authenticated encryption).
TOKEN_ENCRYPTION_KEY (env var, base64-encoded 32 bytes)
│
▼
┌──────────────────┐
│ AES-256-GCM │──► encrypted_access_token (stored in DB)
│ encrypt() │──► encrypted_refresh_token (stored in DB)
│ (random nonce) │
└──────────────────┘
- Generate a new 32-byte key:
python -c "import os,base64; print(base64.b64encode(os.urandom(32)).decode())" - Run migration script to re-encrypt all tokens with the new key
- Update
TOKEN_ENCRYPTION_KEYenvironment variable - Restart all services
- Celery Beat runs
refresh_oauth_tokensevery 10 minutes - Tokens are refreshed
TOKEN_REFRESH_LEEWAY_SECONDS(default: 300s) before expiry - Platform-specific rate limits are respected during refresh
Every webhook delivery is signed so recipients can verify authenticity.
Signing:
payload = f"{timestamp}.{json_body}"
signature = HMAC-SHA256(signing_secret, payload)
Headers:
X-ContentFlow-Signature: sha256={hex_signature}
X-ContentFlow-Timestamp: {unix_timestamp}
X-ContentFlow-Event: {event_type}
import hashlib, hmac, time
def verify_webhook(body: bytes, signature: str, timestamp: str, secret: str, tolerance: int = 300):
if abs(time.time() - int(timestamp)) > tolerance:
raise ValueError("Timestamp too old — possible replay attack")
signed_payload = f"{timestamp}.{body.decode()}"
expected = "sha256=" + hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("Invalid signature")- Per-webhook
signing_secret(unique per subscription) - Timestamp tolerance prevents replay attacks (default: 5 minutes)
hmac.compare_digestprevents timing attacks- Failed deliveries retry with exponential backoff (max 6 attempts)
- Dead letter queue for permanently failed deliveries
All external URLs (webhook targets, media URLs) are validated:
- Scheme whitelist — only
http://andhttps://allowed - Blocked hostnames —
localhost,*.local,*.internal - Private IP ranges — 10.x, 172.16-31.x, 192.168.x, 127.x, ::1, fe80::
- DNS re-resolution — resolves hostname and validates all returned IPs
- DNS rebinding defense — checks resolved IPs, not just hostname
All tables have RLS enabled. Policies ensure data isolation between users.
| Table | Policy | Rule |
|---|---|---|
users |
Self-select | id = auth.uid() |
users |
Self-update | id = auth.uid() |
api_keys |
Service role only | Blocked for authenticated role |
social_accounts |
Owner access | owner_id = auth.uid() |
posts |
Owner access | owner_id = auth.uid() |
post_deliveries |
Owner access | owner_id = auth.uid() |
video_jobs |
Owner access | owner_id = auth.uid() |
webhooks |
Owner access | owner_id = auth.uid() |
The API server uses SUPABASE_SERVICE_ROLE_KEY to bypass RLS for cross-user operations (e.g., Celery workers processing tasks). This key must never be exposed to clients.
| Plan | Requests/min | Posts/month | Videos/month | Social Sets |
|---|---|---|---|---|
| Free | 10 | 20 | 3 | 2 |
| Build | 60 | 200 | 20 | 5 |
| Scale | 300 | 999,999 | 100 | 20 |
| Enterprise | 1,000 | Unlimited | Unlimited | Unlimited |
- Redis-backed sliding window counter
- Rate limit headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset - HTTP 429 when exceeded
- Body size limit — 10MB default, 100MB for upload endpoints
- Scanner blocking — known vulnerability scanner user-agents rejected (403)
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Force HTTPS |
X-Content-Type-Options |
nosniff |
Prevent MIME sniffing |
X-Frame-Options |
DENY |
Prevent clickjacking |
X-XSS-Protection |
1; mode=block |
XSS filter |
Referrer-Policy |
strict-origin-when-cross-origin |
Limit referrer leakage |
Content-Security-Policy |
default-src 'self' |
Restrict resource loading |
Permissions-Policy |
geolocation=(), microphone=(), camera=() |
Disable browser APIs |
ErrorTrackingMiddlewarecatches unhandled exceptions- Error responses never leak stack traces or internal details
- Generic
{"detail": "Internal server error"}for 500 errors - Sentry integration captures full context (opt-in via
SENTRY_DSN)
All API inputs validated via Pydantic BaseModel:
- Type checking and coercion
max_lengthconstraints on all string inputsmin_length/max_lengthon list fields (e.g., platforms: 1-20)- Enum validation for platforms, statuses
- JSON schema validation for nested objects
- All database operations use Supabase client (parameterized queries)
- No raw SQL execution from user input
- Encrypt tokens at rest (AES-256-GCM)
- Hash API keys (bcrypt)
- Enforce data isolation (RLS + owner_id queries)
- Sign webhook deliveries (HMAC-SHA256)
- Validate and sanitize all inputs
- Maintain security headers on responses
- Monitor and alert on security events
- Regular dependency audits
- Keep API keys confidential
- Rotate API keys regularly
- Validate webhook signatures on receipt
- Use HTTPS for all API communication
- Report security vulnerabilities responsibly
| Level | Description | Response Time |
|---|---|---|
| P0 | Active breach, data exposure | Immediate (< 1 hour) |
| P1 | Vulnerability with exploit path | < 4 hours |
| P2 | Vulnerability without known exploit | < 24 hours |
| P3 | Security improvement | Next sprint |
- Detect — Sentry alerts, monitoring, user reports
- Contain — Disable affected endpoints, rotate compromised keys
- Investigate — Audit logs, request traces, git blame
- Remediate — Fix root cause, deploy patch
- Notify — Affected users within 72 hours (GDPR requirement)
- Review — Post-mortem, update threat model
- Security team: security@contentflow.dev
- Bug reports: https://github.com/contentflow/api/security/advisories
We welcome responsible disclosure of security vulnerabilities.
- Email: security@contentflow.dev
- Response within 48 hours
- API endpoints (
api.contentflow.dev) - OAuth token handling
- Webhook delivery security
- Admin panel access controls
- Social engineering attacks
- Physical attacks
- Denial of service attacks
- Third-party platform vulnerabilities
- Data processing agreement (DPA) template
- Right to erasure (
DELETE /users/{id}) - Data export (
GET /users/{id}/export) - Breach notification within 72 hours
- Privacy policy referencing data handling
- Access control (API keys, admin isolation)
- Encryption at rest (AES-256-GCM for tokens)
- Audit logging (audit_logs table)
- Change management (git, CI/CD)
- Formal security policies documentation
- Annual penetration testing
-
TOKEN_ENCRYPTION_KEYis set and securely generated (32 bytes) -
OAUTH_STATE_SECRETis set and unique per environment -
JWT_SECRETis set (NOT the defaultchange-me-in-production) -
STRIPE_WEBHOOK_SECRETis set -
SUPABASE_SERVICE_ROLE_KEYis set (never exposed to clients) - No hardcoded secrets in source code
- RLS policies applied (
02_rls.sql) - Sentry DSN configured for error tracking
- Security headers middleware enabled
- Rotate
TOKEN_ENCRYPTION_KEYquarterly - Rotate
JWT_SECRETquarterly - Audit API key usage (
last_used_atfield) - Review webhook delivery failures (dead letter queue)
- Run
pip-auditfor dependency vulnerabilities - Run
scripts/secret_scan.py --gitfor leaked secrets - Review audit_logs for suspicious activity
- Update OAuth client secrets per platform requirements