Chapter 11: Security & Permissions
Vibe CMS is built with enterprise-grade security. This chapter covers API keys, authentication, permissions, and best practices for securing your content management system.
API Keys
API Key Types
Vibe CMS uses project-scoped API keys for authentication:
Key Features:
- Scoped to specific projects
- Can be rotated without affecting other keys
- Support expiration dates
- Track usage and last access time
- Prefix-based identification for easy management
Creating API Keys
Via Admin Dashboard:
- Navigate to Project Settings → API Keys
- Click Create New Key
- Enter key name (e.g., "Astro Build", "Mobile App")
- Optional: Set expiration date
- Click Create
- Copy and save immediately - you won't see it again
Via API:
const newKey = await cms.projects.createApiKey('project-id', {
name: 'Frontend Build Key',
description: 'Used for Astro static site generation',
expires_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year
});
console.log(newKey.api_key); // vib_sk_xxx... (shown only once)
console.log(newKey.prefix); // vib_sk_xxx (for identification)
API Key Format
vib_sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
├─ vib: Product identifier (Vibe CMS)
├─ sk: Key type (secret key)
└─ XXXX: 32 random alphanumeric characters
Using API Keys
In SDK:
const cms = createVibeCMS({
projectId: 'your-project-id',
apiKey: 'vib_sk_...' // Optional for authenticated calls
});
In HTTP Headers:
curl -H "Authorization: Bearer vib_sk_..." \
https://api.vibe-cms.com/collections/blog-posts
In Environment Variables:
# .env.local (never commit)
CMS_API_KEY=vib_sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Key Rotation
Create new key:
const newKey = await cms.projects.createApiKey('project-id', {
name: 'Frontend Build Key v2'
});
Update code to use new key:
# Update .env or secrets manager
CMS_API_KEY=vib_sk_newkey...
Deactivate old key:
await cms.projects.deactivateApiKey('project-id', 'old-key-id');
Delete old key:
await cms.projects.deleteApiKey('project-id', 'old-key-id');
Key Best Practices
Never commit keys to version control
# .gitignore .env.local .env.*.localUse environment-specific keys
Development: vib_sk_dev_... Staging: vib_sk_staging_... Production: vib_sk_prod_...Set expiration dates
expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 daysRotate regularly
- Every 90 days minimum
- Immediately if compromised
Monitor usage
const keys = await cms.projects.listApiKeys('project-id'); keys.forEach(key => { console.log(`${key.name}: last used ${key.last_used_at}`); });
Authentication
Public API (No Authentication)
Public API calls don't require authentication:
const cms = createVibeCMS({
projectId: 'your-project-id'
// No apiKey needed for public queries
});
// Fetch published content (public)
const posts = await cms.collection('blog-posts').all();
What's public:
- Published content only
- No draft or archived items
- Read-only access
- No admin features
Authenticated API (With API Key)
Authenticated calls require an API key:
const cms = createVibeCMS({
projectId: 'your-project-id',
apiKey: 'vib_sk_...' // Required for authenticated calls
});
// Create content (requires authentication)
await cms.collection('blog-posts').create({
title: 'New Post',
content: '...'
});
// Update content
await cms.collection('blog-posts').id('item-id').update({
title: 'Updated Title'
});
// Delete content
await cms.collection('blog-posts').id('item-id').delete();
// Access draft content
const draftPosts = await cms.collection('blog-posts')
.status('draft')
.all();
What requires authentication:
- Creating/updating/deleting content
- Accessing draft or archived content
- File uploads
- Admin operations
JWT Authentication (Supabase)
Vibe CMS uses Supabase Auth for user authentication:
Admin Dashboard Login
Via Email/Password:
- Visit admin dashboard
- Enter email and password
- Click "Sign In"
- JWT token issued automatically
- Token stored in browser (secure cookies)
Via OAuth Providers:
- GitHub
- Microsoft
- Apple
- Discord (coming soon)
Backend Authentication
# FastAPI authentication middleware
from app.dependencies.auth import get_current_user
@app.post("/api/posts")
async def create_post(
data: ContentItemCreate,
user: JWTUser = Depends(get_current_user)
):
# User is authenticated
print(f"User: {user.email}")
# Create post with user context
post = await db.create_content_item(
collection_id=data.collection_id,
created_by=user.user_id
)
return post
Token Verification
async def verify_jwt_token(token: str) -> dict:
"""
Verify JWT token from request headers.
"""
try:
payload = jwt.get_unverified_claims(token)
# Check required fields
if "sub" not in payload or "email" not in payload:
raise HTTPException(status_code=401, detail="Invalid token")
# Check expiration
if "exp" in payload and payload["exp"] < time.time():
raise HTTPException(status_code=401, detail="Token expired")
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
Content Permissions
Project Access Control
Every user can access only their own projects:
@app.get("/projects")
async def list_projects(user: JWTUser = Depends(get_current_user)):
# Returns only projects owned by this user
projects = db.query(Project).filter(
Project.account_id == user.user_id
).all()
return projects
Content Item Status
Content items have three status levels:
Draft (draft)
- Only visible to authenticated users
- Can only be accessed with API key
- Used for work-in-progress content
- Requires authentication to access
Published (published)
- Publicly visible (no authentication needed)
- Served from optimized published table
- Can be accessed by unauthenticated users
- Best for static site generation
Archived (archived)
- Hidden from most queries
- Only visible to authenticated users
- Maintains version history
- Can be restored if needed
Query Authorization
// Public query - returns only published items
const publicPosts = await cms.collection('blog-posts').all();
// ✓ Works without API key
// Only includes published posts
// Authenticated query - returns all items
const allPosts = await cms.collection('blog-posts')
.apiKey('vib_sk_...')
.all();
// ✓ Includes draft and archived items
// Requires API key
Content Filtering by Status
// Get only published
const published = await cms.collection('blog-posts')
.status('published')
.all();
// Get only draft
const draft = await cms.collection('blog-posts')
.status('draft')
.apiKey('vib_sk_...') // Requires auth
.all();
// Get multiple statuses
const editable = await cms.collection('blog-posts')
.status(['draft', 'archived'])
.all();
Database Security
Row-Level Security (RLS)
Vibe CMS uses Supabase RLS policies:
-- Only allow users to see their own projects
CREATE POLICY "Users can only see their own projects"
ON public.projects
FOR SELECT
USING (auth.uid()::text = account_id);
-- Only allow users to create content in their projects
CREATE POLICY "Users can only create in own projects"
ON public.cms_content_items
FOR INSERT
WITH CHECK (EXISTS(
SELECT 1 FROM public.projects p
WHERE p.id = projects.project_id
AND p.account_id = auth.uid()::text
));
SECURITY DEFINER Functions
Database functions use SECURITY DEFINER to safely bypass RLS:
CREATE FUNCTION get_published_content(
p_project_id text,
p_collection_slug text
) RETURNS TABLE (...) AS $$
BEGIN
-- Function runs with database role permissions
-- Can access data user might not have direct access to
-- But only returns publicly published content
RETURN QUERY
SELECT * FROM cms_content_items_published
WHERE project_id = p_project_id
AND collection_slug = p_collection_slug;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
File Security
File Access Control
Public Files:
// Anyone can access
const publicUrl = cms.assetUrl('asset-id');
// Returns permanent public URL
Private Files:
// Requires authentication
const privateUrl = cms.assetUrl('asset-id', {
private: true
});
// Returns signed URL valid for 24 hours
File Upload Security
Generate upload token:
const token = await cms.generateUploadToken({
filename: 'document.pdf',
mime_type: 'application/pdf',
file_size: 1024000, // 1MB
expires_in: 3600 // 1 hour
});
// Token is single-use and expires
Upload using token:
const response = await fetch(
`https://api.vibe-cms.com/upload?token=${token.token}`,
{
method: 'PUT',
body: fileData,
headers: {
'Content-Type': 'application/pdf'
}
}
);
Rate Limiting
API Rate Limits
Public API:
- 100 requests/minute per IP address
- Soft limit (returns 429 after exceeded)
Authenticated API:
- 1,000 requests/minute per API key
- Tracked per key, not IP address
Rate Limit Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1699564800
Handling Rate Limits
try {
const response = await fetch(apiUrl, { headers });
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
console.log(`Rate limited, retry after ${retryAfter}s`);
// Wait and retry
await new Promise(resolve =>
setTimeout(resolve, parseInt(retryAfter) * 1000)
);
// Retry request
return fetch(apiUrl, { headers });
}
return response.json();
} catch (error) {
console.error('Request failed:', error);
}
Data Protection
GDPR Compliance
Right to be Forgotten:
// Securely delete user data
await cms.projects.deleteUserData('user-id', {
include_content: true,
include_files: true,
include_api_keys: true
});
Data Export:
const userData = await cms.projects.exportUserData('user-id');
console.log(userData);
// Returns all user-associated data
Data Encryption
In Transit:
- All API calls use HTTPS/TLS 1.3
- Certificate pinning available
- HSTS headers enforced
At Rest:
- Database data encrypted with AES-256
- File uploads encrypted in Supabase Storage
- Automatic key rotation
Security Checklist
Before Launch
- Generate API keys in production environment
- Never commit
.envfiles - Set API key expiration (90 days max)
- Use environment variables for all secrets
- Enable HTTPS everywhere
- Implement rate limiting on your API layer
- Configure CORS properly
- Use signed URLs for private files
- Rotate API keys regularly
- Monitor API key usage
- Set up audit logging
- Test authentication flows
Ongoing
- Review API key usage monthly
- Rotate keys every 90 days
- Monitor failed authentication attempts
- Update SDK versions regularly
- Review firewall/WAF rules
- Backup database regularly
- Test disaster recovery
- Review audit logs for suspicious activity
Troubleshooting
"Unauthorized" Error
Problem: 401 Unauthorized response
Causes:
- Missing API key for authenticated operation
- Expired API key
- Invalid API key format
- API key from different project
Solution:
// Verify API key is set
console.log(cms.config.apiKey); // Should show key
// Check key expiration
const keys = await cms.projects.listApiKeys('project-id');
const key = keys.find(k => k.prefix === 'vib_sk_xxx');
console.log(key.expires_at); // Check expiration date
// Verify key belongs to correct project
console.log(key.project_id); // Should match your projectId
Rate Limit Error
Problem: 429 Too Many Requests
Solution:
// Implement exponential backoff
async function queryWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.status === 429 && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s...
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
const posts = await queryWithRetry(() =>
cms.collection('blog-posts').all()
);
CORS Errors
Problem: Cross-origin requests blocked
Solution:
// The SDK handles CORS automatically for same-origin requests
// For cross-origin, configure CORS in Vibe CMS settings:
await cms.projects.updateSettings('project-id', {
cors_origins: [
'https://yourdomain.com',
'https://www.yourdomain.com',
'https://staging.yourdomain.com'
]
});
Best Practices
API Key Management
- Store in secure secrets manager (GitHub Secrets, AWS Secrets Manager, etc.)
- Never log or expose keys
- Rotate regularly (90 days max)
- Use descriptive names for key identification
Authentication
- Always use HTTPS in production
- Implement session timeout (15-30 minutes)
- Use secure cookies for web apps
- Log authentication failures
Content Security
- Publish only when ready
- Keep drafts private
- Archive sensitive old content
- Use status-based queries
File Security
- Use signed URLs for private files
- Set appropriate expiration times
- Validate file types on upload
- Scan uploads for malware
Monitoring
- Track API key usage
- Monitor failed authentications
- Alert on unusual patterns
- Review audit logs regularly
Next Steps
Now that you understand security:
- Generate API keys for your projects
- Set up secure environment variables
- Implement authentication in your apps
- Configure file access controls
- Set up monitoring and alerts
Need help? Join our Discord community or check the Security API Reference.