Team Documentation | Created: January 30, 2026
A comprehensive guide for securely connecting the Angularize backend (deployed on Google Cloud Run) to a frontend application.
This document outlines the approach to securely expose our Angularize Multi-Agent API to a frontend application. We'll use API Key authentication combined with CORS policies to ensure secure, controlled access—similar to how platforms like Lovable, v0.dev, and Bolt work.
sequenceDiagram
participant User
participant Frontend
participant "Cloud Run<br/>Backend" as Backend
participant "AI Agents" as Agents
User->>Frontend: Enter prompt & click "Generate"
Frontend->>Backend: POST /api/chat<br/>(with X-API-Key header)
Backend->>Backend: Validate API Key
Backend->>Agents: Start generation job
Backend-->>Frontend: Return job_id + status_url
loop Poll for status
Frontend->>Backend: GET /api/status/{job_id}
Backend-->>Frontend: Return progress/logs
end
Backend-->>Frontend: Final result with URLs
Frontend-->>User: Show preview + GitHub/Vercel links
Our backend is a FastAPI application with these key endpoints:
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/api/chat |
POST | Start a new generation or update job |
/api/status/{job_id} |
GET | Check job status and progress |
/api/cancel/{job_id} |
POST | Cancel a running job |
Note
Currently deployed on Google Cloud Run at a URL like:
https://angularize-generator-xxxxx.run.app
We'll implement a simple but effective API key system:
flowchart LR
A[Frontend Request] --> B{Has X-API-Key?}
B -->|No| C[401 Unauthorized]
B -->|Yes| D{Key Matches?}
D -->|No| C
D -->|Yes| E[Process Request]
File: api/main.py
from fastapi import FastAPI, BackgroundTasks, HTTPException, Depends, Security
from fastapi.security import APIKeyHeader
from fastapi.middleware.cors import CORSMiddleware
import os
# =========================================
# API KEY CONFIGURATION
# =========================================
API_KEY = os.getenv("API_KEY")
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
async def verify_api_key(api_key: str = Security(API_KEY_HEADER)):
"""
Verify the API key from the request header.
If API_KEY env var is not set, authentication is skipped (dev mode).
"""
if not API_KEY:
# No key configured = open access (for local development)
return None
if api_key != API_KEY:
raise HTTPException(
status_code=401,
detail="Invalid or missing API key"
)
return api_key
# =========================================
# CORS CONFIGURATION
# =========================================
ALLOWED_ORIGINS = [
"https://your-production-frontend.com",
"https://your-frontend.vercel.app",
"http://localhost:3000", # Local Next.js dev
"http://localhost:5173", # Local Vite dev
"http://127.0.0.1:5500", # VS Code Live Server
]
app = FastAPI(title="Multi-Agent Playground API")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# =========================================
# PROTECTED ENDPOINTS
# =========================================
@app.post("/api/chat", response_model=ChatResponse)
async def start_chat(
request: ChatRequest,
background_tasks: BackgroundTasks,
api_key: str = Depends(verify_api_key) # 👈 ADD THIS
):
# ... existing implementation ...
@app.get("/api/status/{job_id}", response_model=JobStatus)
async def get_status(
job_id: str,
api_key: str = Depends(verify_api_key) # 👈 ADD THIS
):
# ... existing implementation ...
@app.post("/api/cancel/{job_id}")
async def cancel_job(
job_id: str,
api_key: str = Depends(verify_api_key) # 👈 ADD THIS
):
# ... existing implementation ...# API Security
API_KEY="dev-testing-key-12345"
# Existing configuration
GOOGLE_API_KEY="AIzaSy..."
GITHUB_TOKEN="ghp_..."
VERCEL_TOKEN="..."Set the API key as an environment variable in Cloud Run:
# Option 1: Using gcloud CLI
gcloud run services update angularize-generator \
--region=us-central1 \
--set-env-vars="API_KEY=prod-secure-key-$(openssl rand -hex 16)"
# Option 2: Via Cloud Console
# Navigate to: Cloud Run → Service → Edit & Deploy New Revision → Variables & SecretsImportant
Generating a Secure API Key
Use a cryptographically random key for production:
# Python
python -c "import secrets; print(secrets.token_urlsafe(32))"
# PowerShell
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))flowchart TB
subgraph Frontend["Frontend Application"]
UI["User Interface"]
API["API Service Layer"]
State["State Management"]
end
subgraph Backend["Cloud Run Backend"]
Auth["API Key Middleware"]
Routes["FastAPI Routes"]
Agents["AI Agents"]
end
UI -->|User Prompt| API
API -->|POST with X-API-Key| Auth
Auth --> Routes
Routes --> Agents
Routes -->|job_id| API
API -->|Poll status| Routes
API --> State
State --> UI
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://your-cloud-run-url.run.app';
const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
interface ChatRequest {
message: string;
user_id: string;
project_id?: string; // For updates
session_id?: string; // For conversation continuity
}
interface ChatResponse {
job_id: string;
project_id: string;
status_url: string;
session_id?: string;
preview_url?: string;
warning?: string;
}
interface JobStatus {
job_id: string;
status: 'in_progress' | 'success' | 'failed' | 'partial' | 'cancelled';
progress: number;
logs: string[];
github_url?: string;
vercel_url?: string;
result?: any;
}
// Start a new generation or update
export async function startGeneration(request: ChatRequest): Promise<ChatResponse> {
const response = await fetch(`${API_BASE_URL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY || '',
},
body: JSON.stringify(request),
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Invalid API key');
}
throw new Error(`API error: ${response.statusText}`);
}
return response.json();
}
// Poll job status
export async function getJobStatus(jobId: string): Promise<JobStatus> {
const response = await fetch(`${API_BASE_URL}/api/status/${jobId}`, {
headers: {
'X-API-Key': API_KEY || '',
},
});
if (!response.ok) {
throw new Error(`Failed to get status: ${response.statusText}`);
}
return response.json();
}
// Cancel a running job
export async function cancelJob(jobId: string): Promise<void> {
await fetch(`${API_BASE_URL}/api/cancel/${jobId}`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY || '',
},
});
}'use client';
import { useState, useEffect } from 'react';
import { startGeneration, getJobStatus, JobStatus } from '@/lib/api';
export default function Generator() {
const [prompt, setPrompt] = useState('');
const [jobId, setJobId] = useState<string | null>(null);
const [status, setStatus] = useState<JobStatus | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
// Start generation
const handleGenerate = async () => {
if (!prompt.trim()) return;
setIsGenerating(true);
try {
const response = await startGeneration({
message: prompt,
user_id: 'user-' + Date.now(), // Or use actual auth
});
setJobId(response.job_id);
} catch (error) {
console.error('Generation failed:', error);
setIsGenerating(false);
}
};
// Poll for status updates
useEffect(() => {
if (!jobId) return;
const pollStatus = async () => {
try {
const currentStatus = await getJobStatus(jobId);
setStatus(currentStatus);
if (['success', 'failed', 'partial', 'cancelled'].includes(currentStatus.status)) {
setIsGenerating(false);
}
} catch (error) {
console.error('Status poll failed:', error);
}
};
// Poll every 2 seconds
const interval = setInterval(pollStatus, 2000);
pollStatus(); // Initial fetch
return () => clearInterval(interval);
}, [jobId]);
return (
<div className="generator">
{/* Input Section */}
<div className="input-section">
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe your Angular application..."
disabled={isGenerating}
/>
<button
onClick={handleGenerate}
disabled={isGenerating || !prompt.trim()}
>
{isGenerating ? 'Generating...' : '✨ Generate'}
</button>
</div>
{/* Status & Logs */}
{status && (
<div className="status-section">
<div className="progress-bar">
<div style={{ width: `${status.progress}%` }} />
</div>
<p>Status: {status.status}</p>
{/* Live Logs */}
<div className="logs">
{status.logs.slice(-10).map((log, i) => (
<div key={i} className="log-entry">{log}</div>
))}
</div>
{/* Result Links */}
{status.status === 'success' && (
<div className="result-links">
{status.github_url && (
<a href={status.github_url} target="_blank">
📁 View on GitHub
</a>
)}
{status.vercel_url && (
<a href={status.vercel_url} target="_blank">
🌐 Live Preview
</a>
)}
</div>
)}
</div>
)}
</div>
);
}- Update
api/main.pywith API key middleware - Update
api/main.pywith CORS configuration - Add
API_KEYto.env.example - Build and deploy updated Docker image
- Set
API_KEYenvironment variable in Cloud Run - Test with Postman/curl
- Create API service layer with authentication
- Add
NEXT_PUBLIC_API_URLto environment - Add
NEXT_PUBLIC_API_KEYto environment (⚠️ See security note below) - Implement polling for job status
- Add error handling for 401 responses
- Test end-to-end flow
Caution
Frontend API Key Exposure
Any API key in frontend code is visible to users via browser DevTools. This is acceptable if:
- The key only grants access to start jobs (no admin access)
- You implement rate limiting on the backend
- You use separate keys per user/tenant (future enhancement)
For higher security, consider implementing user authentication (Firebase/Auth0) where the backend trusts authenticated users instead of a shared API key.
# Test health endpoint (no auth required)
curl https://your-cloud-run-url.run.app/health
# Test with valid API key
curl -X POST https://your-cloud-run-url.run.app/api/chat \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"message": "Create a portfolio website", "user_id": "test-user"}'
# Test with invalid key (should return 401)
curl -X POST https://your-cloud-run-url.run.app/api/chat \
-H "Content-Type: application/json" \
-H "X-API-Key: wrong-key" \
-d '{"message": "test", "user_id": "test"}'- Import the endpoint:
POST {{base_url}}/api/chat - Add header:
X-API-Key: {{api_key}} - Set body (JSON):
{ "message": "Create a modern e-commerce Angular app with dark theme", "user_id": "postman-test" } - Send and note the
job_idin response - Poll:
GET {{base_url}}/api/status/{{job_id}}
Caution
Critical Security Requirement: Users and attackers must NEVER be able to see the API key. The architecture below ensures the API key stays on the server only.
This is the recommended production approach. The API key is stored ONLY on your frontend's server (Next.js API Routes / Express server), never in the browser.
flowchart LR
subgraph Browser["🌐 Browser (User's Device)"]
UI["React UI"]
end
subgraph FrontendServer["🖥️ Frontend Server (Vercel/Your Server)"]
API["API Route<br/>/api/generate"]
KEY["🔑 API_KEY<br/>(env variable)"]
end
subgraph CloudRun["☁️ Cloud Run Backend"]
Auth["Auth Middleware"]
Agents["AI Agents"]
end
UI -->|"POST /api/generate<br/>(no API key)"| API
API -->|"Read from env"| KEY
API -->|"POST with X-API-Key<br/>(key added server-side)"| Auth
Auth --> Agents
Agents -->|Response| API
API -->|Response| UI
style KEY fill:#ff6b6b,stroke:#c0392b,color:#fff
style Browser fill:#3498db,stroke:#2980b9,color:#fff
File: app/api/generate/route.ts (Server-side only - key never reaches browser)
import { NextRequest, NextResponse } from 'next/server';
// ✅ This runs on the SERVER - users cannot see this code
const ANGULARIZE_API_URL = process.env.ANGULARIZE_API_URL!; // Cloud Run URL
const ANGULARIZE_API_KEY = process.env.ANGULARIZE_API_KEY!; // SECRET - never exposed
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Forward request to Cloud Run with API key (added server-side)
const response = await fetch(`${ANGULARIZE_API_URL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': ANGULARIZE_API_KEY, // 🔒 Added on server, invisible to browser
},
body: JSON.stringify({
message: body.message,
user_id: body.user_id || 'anonymous',
project_id: body.project_id,
}),
});
if (!response.ok) {
return NextResponse.json(
{ error: 'Backend error' },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('API route error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}File: app/api/status/[jobId]/route.ts
import { NextRequest, NextResponse } from 'next/server';
const ANGULARIZE_API_URL = process.env.ANGULARIZE_API_URL!;
const ANGULARIZE_API_KEY = process.env.ANGULARIZE_API_KEY!;
export async function GET(
request: NextRequest,
{ params }: { params: { jobId: string } }
) {
const response = await fetch(
`${ANGULARIZE_API_URL}/api/status/${params.jobId}`,
{
headers: {
'X-API-Key': ANGULARIZE_API_KEY, // 🔒 Server-side only
},
}
);
const data = await response.json();
return NextResponse.json(data);
}Frontend Component (Browser - no API key visible)
// ✅ Browser code - calls YOUR server, not Cloud Run directly
async function generateProject(prompt: string) {
const response = await fetch('/api/generate', { // Your Next.js route
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: prompt }),
// ❌ NO API KEY HERE - it's added by the server
});
return response.json();
}Frontend Server (Vercel/Your Hosting)
# These are SERVER-ONLY variables (no NEXT_PUBLIC_ prefix)
ANGULARIZE_API_URL=https://angularize-generator-xxxxx.run.app
ANGULARIZE_API_KEY=your-super-secret-api-key
# ⚠️ NEVER use NEXT_PUBLIC_ for secrets - those are exposed to browser!| Check | Status | How to Verify |
|---|---|---|
| API key not in browser code | ✅ | Open DevTools → Sources → search for key |
| API key not in network requests | ✅ | DevTools → Network → inspect request headers |
| Direct Cloud Run access blocked | ✅ | Only accepts requests with valid API key |
| Environment variables correct | ✅ | No NEXT_PUBLIC_ prefix on secrets |
Tip
Testing for Leaks
- Open browser DevTools (F12)
- Go to Network tab
- Trigger a generation
- Click on the request to
/api/generate - Check Headers - should NOT contain
X-API-Key - The key is added by your server, not the browser!
| Priority | Enhancement | Description |
|---|---|---|
| 🔴 High | Rate Limiting | Add rate limiting per IP/session |
| 🔴 High | User Authentication | Login with Google/GitHub via Firebase |
| 🟡 Medium | Per-User Keys | Each user gets their own API key |
| 🟡 Medium | Usage Quotas | Track and limit usage per user |
| 🟢 Low | Webhooks | Push notifications instead of polling |
| 🟢 Low | WebSocket | Real-time log streaming |
This approach provides:
✅ Simple implementation - Just add middleware and headers
✅ Secure communication - HTTPS + API key validation
✅ CORS protection - Only allowed origins can call the API
✅ Easy testing - Works with Postman, cURL, and any frontend
✅ Scalable - Can evolve to per-user keys or OAuth later
For any questions, refer to the FastAPI security documentation or contact the backend team.