This document outlines security best practices for deploying and maintaining this Workers for Platforms application.
DO NOT commit these values:
- API tokens (dispatch namespace, custom hostnames)
- Account IDs (already in placeholders)
- Database IDs (already in placeholders)
- Zone IDs
- Domain names (if sensitive)
Use Wrangler secrets for production:
# Dispatch namespace token (required)
echo "your-token" | wrangler secret put DISPATCH_NAMESPACE_API_TOKEN
# Custom hostnames token (optional)
echo "your-token" | wrangler secret put CLOUDFLARE_API_TOKENCreate .dev.vars file (already in .gitignore):
DISPATCH_NAMESPACE_API_TOKEN=your-dispatch-token
CLOUDFLARE_API_TOKEN=your-cloudflare-tokenNever commit .dev.vars to git!
The /admin endpoint exposes sensitive information:
- Database contents
- All deployed projects
- Infrastructure details
Use Cloudflare Access to protect /admin:
-
Create Access Application:
- Application type: Self-hosted
- Application domain:
yourdomain.com - Path:
/admin*
-
Configure Access Policy:
- Policy name: Admin Access
- Action: Allow
- Include rule: Emails → Your authorized emails
-
Test Access:
- Visit
/admin- should redirect to authentication - After login, should access admin interface
- Visit
Always validate:
- Subdomain format:
^[a-z0-9-]{1,63}$ - Project names: Sanitize for XSS
- Script content: Consider sandboxing or content security policies
Implement file validation:
// File type restrictions
const allowedTypes = ['.html', '.css', '.js', '.jpg', '.png', '.svg', '.ico'];
const isValidType = asset.path.match(/\.(html|css|js|jpg|png|svg|ico)$/);
// File size limits
const maxFileSize = 10 * 1024 * 1024; // 10MB
const isValidSize = asset.size <= maxFileSize;
// Total upload size
const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
const maxTotalSize = 50 * 1024 * 1024; // 50MBConsider adding rate limiting for:
- Project creation endpoint (
/projects) - Asset uploads
- Admin access attempts
Example using Cloudflare Rate Limiting:
# In wrangler.toml
[[unsafe.bindings]]
name = "RATE_LIMITER"
type = "ratelimit"
namespace_id = "your-namespace-id"
# Then in code:
const { success } = await env.RATE_LIMITER.limit({ key: clientIP });
if (!success) return c.text('Rate limit exceeded', 429);- D1 database is bound only to this worker
- No direct external access
- All queries use parameterized statements (protection against SQL injection)
Consider what data is stored:
- Project scripts may contain sensitive logic
- Asset files may contain confidential information
- Limit who can access
/adminto view this data
For the builder interface, consider adding CSP headers:
app.use('*', async (c, next) => {
await next();
c.header('Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
);
});- All secrets stored via
wrangler secret put(not in wrangler.toml) -
.dev.varsin.gitignore - Admin endpoint protected with Cloudflare Access
- Input validation enabled
- Rate limiting configured (if needed)
- CSP headers configured (if needed)
- File upload restrictions in place
- Account ID and Database ID are placeholders in committed code
- Test admin endpoint requires authentication
- Verify secrets are not exposed in logs
- Monitor for unusual activity via logs
- Set up alerting for errors or abuse patterns
# Real-time logs
npx wrangler tail
# Filter for errors
npx wrangler tail --format json | grep ERROR
# Monitor specific endpoints
npx wrangler tail | grep "/projects"Consider setting up alerts for:
- High error rates
- Unusual deployment volumes
- Large asset uploads
- Failed authentication attempts (if using Access)
If secrets are compromised:
- Immediately rotate all API tokens
- Update secrets:
wrangler secret put DISPATCH_NAMESPACE_API_TOKEN - Review recent deployments for unauthorized access
- Check D1 database for suspicious projects
- Review admin access logs
Depending on your use case, consider:
- GDPR: If storing EU user data
- Data retention: Policy for how long to keep projects
- User consent: For tracking or analytics
- Terms of Service: For user-generated content