Copied to clipboard!

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:

  1. Navigate to Project SettingsAPI Keys
  2. Click Create New Key
  3. Enter key name (e.g., "Astro Build", "Mobile App")
  4. Optional: Set expiration date
  5. Click Create
  6. 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

  1. Never commit keys to version control

    # .gitignore
    .env.local
    .env.*.local
    
  2. Use environment-specific keys

    Development:   vib_sk_dev_...
    Staging:       vib_sk_staging_...
    Production:    vib_sk_prod_...
    
  3. Set expiration dates

    expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000)  // 90 days
    
  4. Rotate regularly

    • Every 90 days minimum
    • Immediately if compromised
  5. 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:

  1. Visit admin dashboard
  2. Enter email and password
  3. Click "Sign In"
  4. JWT token issued automatically
  5. Token stored in browser (secure cookies)

Via OAuth Providers:

  • Google
  • 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 .env files
  • 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

  1. 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
  2. Authentication

    • Always use HTTPS in production
    • Implement session timeout (15-30 minutes)
    • Use secure cookies for web apps
    • Log authentication failures
  3. Content Security

    • Publish only when ready
    • Keep drafts private
    • Archive sensitive old content
    • Use status-based queries
  4. File Security

    • Use signed URLs for private files
    • Set appropriate expiration times
    • Validate file types on upload
    • Scan uploads for malware
  5. 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.