Files
nick-doc/DEPLOYMENT.md

15 KiB

Amanat Assist Mini App - Deployment Guide

Codename: amanat-assist
Version: 1.0
Last Updated: 2026-06-05
Owner: Deployment


🎯 Overview

This document describes the deployment architecture for the Amanat Assist Telegram Mini App, using:

  • Frontend: Vite + React (static hosting)
  • LLM Edge Function: Cloudflare Workers (server-side LLM calls)
  • Backend: Amanat API (dev.amn.gg or amn.gg)

📁 Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        Telegram Client                             │
└─────────────────┬─────────────────────────────────┬───────────────┘
                  │                                     │
                  ▼                                     ▼
┌─────────────────────────────┐     ┌─────────────────────────────┐
│   Mini App (Static)          │     │   LLM Edge Function          │
│   - React + Vite             │     │   - Cloudflare Workers       │
│   - Hosted on CF Pages       │     │   - Route: /api/llm          │
│   - URL: assist.amn.gg       │     │   - Handles auth + LLM calls │
└─────────────────────────────┘     └──────────┬────────────────┘
                                              │
                                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                        LLM Providers                               │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│  │  Mistral   │ │   Kimi      │ │  DeepSeek   │ │  OpenCode   │ │
│  │  (Primary) │ │  (Fallback) │ │  (Fallback) │ │  (Local)    │ │
│  └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Amanat Backend                                │
│  - POST /api/auth/telegram      (Telegram SSO)                   │
│  - GET /api/marketplace/categories                                  │
│  - POST /api/files/upload      (File upload)                      │
│  - POST /api/marketplace/purchase-requests  (Submit w/ aiGenerated) │
└─────────────────────────────────────────────────────────────────┘

🚀 Quick Start

Prerequisites

  1. Cloudflare Account with Workers + Pages enabled
  2. Amanat Backend running at dev.amn.gg (already deployed )
  3. LLM API Keys (at least one):
    • Mistral: sk_...
    • Kimi: sk_...
    • DeepSeek: sk_...
  4. Domain configured in Cloudflare: assist.amn.gg (or subdomain)

📦 Step 1: Deploy Static Frontend (Cloudflare Pages)

1.1 Create Cloudflare Pages Project

# Navigate to project
cd /Users/manwe/CascadeProjects/escrow/amanat-assist

# Install dependencies
npm install

# Build production bundle
npm run build

1.2 Cloudflare Dashboard Setup

  1. Go to: https://dash.cloudflare.com
  2. Select your account → Workers & PagesCreate applicationPages
  3. Connect Git repository (if using Git) OR Upload files
  4. Project name: amanat-assist
  5. Production branch: main (or your deployment branch)
  6. Build command: npm run build
  7. Build output directory: dist
  8. Environment variables: (see Section 3)

1.3 Configure Custom Domain

  1. In Pages project → Custom domainsSet up custom domain
  2. Enter: assist.amn.gg
  3. Cloudflare will issue SSL certificate automatically
  4. Wait for DNS propagation (~5-10 minutes)

☁️ Step 2: Deploy LLM Edge Function (Cloudflare Workers)

2.1 Create Worker

  1. Go to: https://dash.cloudflare.com
  2. Select your account → Workers & PagesCreate serviceWorker
  3. Service name: amanat-assist-llm
  4. Starter: Fetch handler

2.2 Worker Code

Create index.ts:

// src/index.ts for Cloudflare Worker

interface LLMRequest {
  messages: Array<{ role: 'user' | 'assistant'; content: string }>;
  provider?: 'mistral' | 'kimi' | 'deepseek' | 'opencode';
  model?: string;
}

interface ProviderConfig {
  baseUrl: string;
  apiKeyEnv: string;
  chatEndpoint: string;
  model: string;
}

const PROVIDERS: Record<string, ProviderConfig> = {
  mistral: {
    baseUrl: 'https://api.mistral.ai',
    apiKeyEnv: 'MISTRAL_API_KEY',
    chatEndpoint: '/v1/chat/completions',
    model: 'mistral-large-latest',
  },
  kimi: {
    baseUrl: 'https://api.moonshot.cn',
    apiKeyEnv: 'KIMI_API_KEY',
    chatEndpoint: '/v1/chat/completions',
    model: 'moonshot-v1-8k',
  },
  deepseek: {
    baseUrl: 'https://api.deepseek.com',
    apiKeyEnv: 'DEEPSEEK_API_KEY',
    chatEndpoint: '/chat/completions',
    model: 'deepseek-chat',
  },
  opencode: {
    baseUrl: 'http://127.0.0.1:3456',
    apiKeyEnv: '',
    chatEndpoint: '/v1/messages',
    model: 'claude-3-sonnet',
  },
};

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Only allow POST
    if (request.method !== 'POST') {
      return new Response(JSON.stringify({ error: 'Method not allowed' }), {
        status: 405,
        headers: { 'Content-Type': 'application/json', 'Allow': 'POST' },
      });
    }

    // Validate origin (optional - for production)
    const origin = request.headers.get('origin');
    const allowedOrigins = [
      'https://assist.amn.gg',
      'https://dev.amn.gg',
      'https://amn.gg',
    ];
    
    if (origin && !allowedOrigins.some(o => origin.startsWith(o))) {
      return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
        status: 403,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    try {
      const body: LLMRequest = await request.json();
      const provider = body.provider || 'mistral';
      const config = PROVIDERS[provider];
      
      if (!config) {
        return new Response(JSON.stringify({ error: `Unknown provider: ${provider}` }), {
          status: 400,
          headers: { 'Content-Type': 'application/json' },
        });
      }

      // Get API key from environment
      const apiKey = config.apiKeyEnv ? env[config.apiKeyEnv] : '';
      
      if (config.apiKeyEnv && !apiKey) {
        return new Response(JSON.stringify({ error: `API key for ${provider} not configured` }), {
          status: 400,
          headers: { 'Content-Type': 'application/json' },
        });
      }

      // Build request for the provider
      const headers: Record<string, string> = { 'Content-Type': 'application/json' };
      if (apiKey) {
        headers['Authorization'] = `Bearer ${apiKey}`;
      }

      const model = body.model || config.model;
      const messages = body.messages;
      
      // Format request based on provider
      let providerBody: any;
      if (provider === 'opencode') {
        providerBody = {
          model,
          messages: messages.map(m => ({
            role: m.role,
            content: m.content,
          })),
          max_tokens: 1024,
        };
      } else {
        providerBody = {
          model,
          messages,
          temperature: 0.7,
        };
      }

      // Call the LLM provider
      const providerResponse = await fetch(`${config.baseUrl}${config.chatEndpoint}`, {
        method: 'POST',
        headers,
        body: JSON.stringify(providerBody),
      });

      if (!providerResponse.ok) {
        const error = await providerResponse.text();
        return new Response(JSON.stringify({ 
          error: `LLM error: ${providerResponse.status} - ${error}` 
        }), {
          status: providerResponse.status,
          headers: { 'Content-Type': 'application/json' },
        });
      }

      const data = await providerResponse.json();
      
      // Extract content based on provider
      let content: string;
      if (provider === 'opencode') {
        content = data.content?.[0]?.text || data.content || JSON.stringify(data);
      } else {
        content = data.choices?.[0]?.message?.content || JSON.stringify(data);
      }

      return new Response(JSON.stringify({ content, model: data.model || model }), {
        status: 200,
        headers: { 
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': origin || '*',
          'Access-Control-Allow-Methods': 'POST',
        },
      });
    } catch (err) {
      return new Response(JSON.stringify({ 
        error: `Internal error: ${err instanceof Error ? err.message : String(err)}` 
      }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      });
    }
  },
};

// Type for Cloudflare environment variables
export interface Env {
  MISTRAL_API_KEY?: string;
  KIMI_API_KEY?: string;
  DEEPSEEK_API_KEY?: string;
}

2.3 Configure Worker Settings

  1. Routes: assist.amn.gg/api/llm/* (or assist.amn.gg/api/llm)
  2. Environment variables: Add your LLM API keys
  3. Enable CORS: Handled in code

⚙️ Step 3: Environment Variables

3.1 Frontend (Cloudflare Pages)

Variable Value Required Notes
VITE_AMANAT_API_BASE https://dev.amn.gg Amanat backend
VITE_LLM_PROVIDER mistral Primary LLM provider
VITE_LLM_API_URL https://assist.amn.gg/api/llm Edge function URL

Optional:

  • VITE_OPENCODE_PROXY_URL - If using local proxy

3.2 Edge Function (Cloudflare Worker)

Variable Value Required
MISTRAL_API_KEY Your Mistral key (if using Mistral)
KIMI_API_KEY Your Kimi key
DEEPSEEK_API_KEY Your DeepSeek key

🤖 Step 4: Configure Telegram Mini App

4.1 Create Telegram Bot

  1. Open @BotFather in Telegram
  2. Send /newbot
  3. Follow prompts to create bot
  4. Save the bot token (needed for backend Telegram webhook)

4.2 Enable Mini App

  1. In @BotFather, send /mybots
  2. Select your bot
  3. Go to Bot SettingsMini App
  4. Set URL: https://assist.amn.gg
  5. Enable Inline mode (optional)

4.3 Configure Bot Menu (Optional)

  1. In @BotFather, send /setcommands
  2. Set commands:
start - Open Amanat Assist
help - Show help
auth - Re-authenticate

🧪 Step 5: Test Deployment

5.1 Test Frontend

# Local test
npm run dev
# Open: http://localhost:3000

# Production test
# Open: https://assist.amn.gg

5.2 Test Edge Function

# Direct test
curl -X POST https://assist.amn.gg/api/llm \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{"role": "user", "content": "Hello"}],
    "provider": "mistral",
    "model": "mistral-large-latest"
  }'

5.3 Test via Telegram

  1. Open your bot in Telegram
  2. Click the Mini App button
  3. Verify:
    • Silent auth (no login prompt)
    • Greeting message appears
    • Chat works
    • File upload works
    • Submit creates request in Amanat
    • AI badge appears in Amanat UI

📊 Monitoring & Logging

Cloudflare Workers

  1. Go to: https://dash.cloudflare.com
  2. Workers & Pages → Your Worker → Logs
  3. View real-time requests and errors

Cloudflare Pages

  1. Workers & Pages → Your Pages project → Deployments
  2. View build logs and deployment status

⚠️ Security Considerations

1. Origin Validation

The edge function validates the Origin header to prevent unauthorized access:

  • Allowed: https://assist.amn.gg, https://dev.amn.gg, https://amn.gg
  • Action: Update the allowedOrigins array if adding new domains

2. API Key Protection

  • Never expose LLM API keys in frontend code
  • All LLM calls go through the edge function
  • API keys stored only in Worker environment variables

Add to Worker wrangler.toml:

[triggers]
crons = ["*/5 * * * *"]  # Optional: cleanup

# Rate limiting via Cloudflare
# Configure in Cloudflare Dashboard → Workers → Rate Limiting

🔄 Update Process

Frontend Updates

# Make changes
npm run build
# Push to Git (if using Git integration)
git add . && git commit -m "..." && git push
# Cloudflare Pages auto-deploys

Worker Updates

# Make changes
# Deploy via Wrangler or Dashboard
npx wrangler deploy

📞 Support & Troubleshooting

Common Issues

Issue Solution
CORS errors Verify Access-Control-Allow-Origin in Worker
403 from LLM Check API key in Worker environment
404 on /api/llm Verify Worker route is configured
Telegram auth fails Verify backend /api/auth/telegram endpoint
No AI badge Verify backend schema changes (PRD §12)

Debug Mode

Add to frontend .env:

VITE_DEBUG=true

📚 References


Document version: 1.0 — 2026-06-05 Owner: Deployment Team