Skip to content

Madhan-mohan14/Project-Samarth

Repository files navigation

Angularize API Security & Frontend Integration Guide

Team Documentation | Created: January 30, 2026
A comprehensive guide for securely connecting the Angularize backend (deployed on Google Cloud Run) to a frontend application.


Executive Summary

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
Loading

Current Architecture

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


Security Implementation

1. API Key Authentication

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]
Loading

Backend Changes Required

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 ...

2. Environment Configuration

Local Development (.env)

# API Security
API_KEY="dev-testing-key-12345"

# Existing configuration
GOOGLE_API_KEY="AIzaSy..."
GITHUB_TOKEN="ghp_..."
VERCEL_TOKEN="..."

Google Cloud Run

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 & Secrets

Important

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 }))

Frontend Integration

Architecture Overview

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
Loading

Example: React/Next.js Implementation

1. API Service (lib/api.ts)

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 || '',
    },
  });
}

2. React Component (components/Generator.tsx)

'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>
  );
}

Deployment Checklist

Backend (Cloud Run)

  • Update api/main.py with API key middleware
  • Update api/main.py with CORS configuration
  • Add API_KEY to .env.example
  • Build and deploy updated Docker image
  • Set API_KEY environment variable in Cloud Run
  • Test with Postman/curl

Frontend

  • Create API service layer with authentication
  • Add NEXT_PUBLIC_API_URL to environment
  • Add NEXT_PUBLIC_API_KEY to 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.


Testing

Using cURL

# 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"}'

Using Postman

  1. Import the endpoint: POST {{base_url}}/api/chat
  2. Add header: X-API-Key: {{api_key}}
  3. Set body (JSON):
    {
      "message": "Create a modern e-commerce Angular app with dark theme",
      "user_id": "postman-test"
    }
  4. Send and note the job_id in response
  5. Poll: GET {{base_url}}/api/status/{{job_id}}

🔒 Secure Architecture: Hiding API Keys Completely

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.

Backend-for-Frontend (BFF) Pattern

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
Loading

Implementation: Next.js API Routes

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();
}

Environment Variables Setup

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!

Security Verification Checklist

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

  1. Open browser DevTools (F12)
  2. Go to Network tab
  3. Trigger a generation
  4. Click on the request to /api/generate
  5. Check Headers - should NOT contain X-API-Key
  6. The key is added by your server, not the browser!

Future Enhancements

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

Summary

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.

About

Project Samarth is an intelligent, data-driven chatbot that answers natural-language questions about Indian agriculture and climate data. It combines rainfall data (IMD 1901–2015) and crop production data (MoA 2009–2015) to enable interactive, explainable analysis — all through a Streamlit-powered interface.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages