1. System Architecture
1.1 System Context (C4 Level 1)
┌─────────────────┐
│ End User │
│ (Browser) │
└────────┬─────────┘
│ HTTPS
▼
┌─────────────────┐
│ Swiss AI App │
│ Builder Platform│
└───┬────┬────┬────┘
│ │ │
┌─────────────┘ │ └──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Stripe │ │ OAuth │ │ External LLM │
│ (Payments) │ │ Providers │ │ APIs (BYO) │
└──────────────┘ │ (Google, │ └──────────────┘
│ GitHub) │
└──────────────┘- End User — Developer/non-technical user building apps via natural language
- Stripe — Payment processing (CHF billing)
- OAuth Providers — Google, GitHub for social login
- External LLM APIs — Optional user-provided API keys (OpenAI, Anthropic) proxied through CH
1.2 Container Diagram (C4 Level 2)
┌─────────────────────────────────────────────────────────────────────────┐
│ CLOUDFLARE (CDN + WAF + DNS) │
└─────────────────────────────┬───────────────────────────────────────────┘
│
┌─────────────────────┼──────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ Next.js App │ │ Deployed User │ │ Hetzner GPU Server │
│ (Vercel) │ │ Apps (Vercel) │ │ (Zürich) │
│ │ │ │ │ │
│ ┌───────────┐ │ └─────────────────┘ │ ┌───────────────┐ │
│ │ App Router│ │ │ │ Ollama │ │
│ │ API Routes│ │◄─────── Redis ──────────►│ │ (LLM Inference│ │
│ │ SSE Stream│ │ (Upstash/Hetzner) │ │ Server) │ │
│ └───────────┘ │ │ └───────────────┘ │
│ │ │ ┌───────────────┐ │
│ ┌───────────┐ │ │ │ BullMQ Worker │ │
│ │ WebSocket │ │ │ │ (Inference │ │
│ │ (Preview) │ │ │ │ Queue) │ │
│ └───────────┘ │ │ └───────────────┘ │
└───────┬───────┘ │ ┌───────────────┐ │
│ │ │ Sandbox Runner│ │
▼ │ │ (Preview │ │
┌───────────────┐ │ │ Containers) │ │
│ Neon Postgres│ │ └───────────────┘ │
│ (Platform DB)│ └─────────────────────┘
│ ┌───────────┐ │
│ │ Tenant DBs│ │
│ │ (Schemas) │ │
│ └───────────┘ │
└───────────────┘1.3 Containers Overview
| Container | Technology | Purpose |
|---|---|---|
| Web Application | Next.js 14 (App Router) on Vercel | UI + API routes + SSE streaming |
| Platform Database | Neon Postgres (Serverless) | Users, apps, billing, conversations |
| Tenant Databases | Neon Postgres (schema-per-tenant) | User app data (each app gets a schema) |
| Cache / Queue Broker | Redis (Upstash or self-hosted) | Session cache, BullMQ job queue, rate limiting |
| LLM Inference | Ollama on Hetzner GPU (RTX 4090) | Code generation via Llama 3.1 8B / DeepSeek Coder V2 16B |
| Inference Worker | Node.js BullMQ worker on Hetzner | Processes AI generation jobs from queue |
| Sandbox Runner | WebContainers (client-side) | Safe preview of generated apps |
| CDN / WAF | Cloudflare | DDoS protection, SSL termination, DNS |
| Object Storage | Cloudflare R2 or Hetzner S3 | App source code, assets, backups |
1.4 Component Diagram (C4 Level 3 — Next.js App)
┌─────────────────────────────────────────────────────────────┐ │ Next.js Application │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ Auth Module │ │ App Builder │ │ Admin Dashboard │ │ │ │ (NextAuth.js)│ │ UI (React) │ │ (React) │ │ │ └──────┬──────┘ └──────┬───────┘ └────────┬──────────┘ │ │ │ │ │ │ │ ┌──────┴────────────────┴────────────────────┴──────────┐ │ │ │ API Layer (Route Handlers) │ │ │ │ /api/auth/* /api/apps/* /api/chat (SSE) │ │ │ │ /api/billing/* /api/templates /api/admin/* │ │ │ └──────┬────────────────┬────────────────┬──────────────┘ │ │ │ │ │ │ │ ┌──────┴──────┐ ┌──────┴──────┐ ┌─────┴───────────┐ │ │ │ Auth Service│ │ App Service │ │ AI Service │ │ │ │ (NextAuth) │ │ (CRUD, │ │ (Prompt assembly,│ │ │ │ │ │ deploy, │ │ queue dispatch, │ │ │ │ │ │ files) │ │ stream relay) │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ Billing │ │ Sandbox │ │ Deployment │ │ │ │ Service │ │ Service │ │ Service │ │ │ │ (Stripe) │ │ (Preview) │ │ (Vercel API) │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Data Access Layer (Prisma ORM) │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
1.5 Data Flow: User Creates App via AI Chat
User Next.js Redis/BullMQ Ollama Worker Neon DB │ │ │ │ │ │──POST /chat──►│ │ │ │ │ │──save msg─────►│ │ │ │ │──enqueue job──►│ │ │ │◄──SSE open────│ │ │ │ │ │ │──dequeue────────►│ │ │ │ │ │──inference──► │ │ │ │◄──stream tokens──│ │ │◄──SSE tokens──│◄──pub/sub──────│ │ │ │ │ │◄──complete────────│ │ │ │──save response─┼──────────────────┼──────────────►│ │ │──parse code────│ │ │ │◄──SSE done────│ │ │ │ │──preview()───►│ (WebContainer │ │ │ │◄──live app────│ in browser) │ │ │
1.6 Technology Choices & Justification
| Component | Choice | Justification |
|---|---|---|
| Frontend | Next.js 14 (App Router) | SSR + API routes in one deploy; Vercel-native |
| Hosting (Phase 1) | Vercel | Zero-config deploys, edge functions, fast iteration |
| Database | Neon Postgres | Serverless, scales to zero, branching for dev |
| ORM | Prisma | Type-safe queries, migrations, schema-as-code |
| Auth | NextAuth.js v5 | Built for Next.js; email + OAuth; JWT sessions |
| LLM Inference | Ollama (self-hosted) | Swiss data sovereignty; no API costs per token |
| GPU Server | Hetzner Dedicated (Zürich) | Swiss DC; ~€150-200/mo for RTX 4090 |
| Job Queue | BullMQ + Redis | Battle-tested; priority queues; retries |
| Payments | Stripe | CHF support; subscription + usage-based billing |
| CDN / DNS | Cloudflare | Free tier; wildcard DNS; SSL; WAF |
| Code Editor | Monaco Editor | VSCode engine; syntax highlighting |
| Object Storage | Cloudflare R2 | S3-compatible; no egress fees |
1.7 Architecture Decision Records
ADR-001: Next.js Monolith over Microservices
Decision: Single Next.js application handling UI + API + SSE streaming.
Rationale: Solo developer building MVP. Microservices add deployment complexity and operational overhead.
Accepted for Phase 1
ADR-002: Ollama on Dedicated GPU vs. Cloud GPU API
Decision: Self-hosted Ollama on Hetzner dedicated GPU server.
Rationale: Swiss data sovereignty is core. Cloud GPU APIs are mostly US-based. Hetzner has Zürich DC.
Accepted
ADR-003: Neon Postgres over Supabase/PlanetScale
Decision: Neon Postgres for platform and tenant databases.
Rationale: True Postgres with serverless scaling. Schema-per-tenant is native. No unnecessary BaaS features.
Accepted for Phase 1
1.8 Communication Patterns
| Flow | Pattern | Protocol |
|---|---|---|
| Browser ↔ API | Sync request/response | HTTPS REST |
| AI chat streaming | Server push | SSE (Server-Sent Events) |
| Preview hot reload | Bidirectional | WebSocket (client-side) |
| Job dispatch | Async queue | BullMQ over Redis |
| Token streaming | Pub/sub | Redis Pub/Sub |
| Webhooks (Stripe) | Async push | HTTPS POST |
1.9 SSE Architecture Detail
Browser Vercel Edge Redis Hetzner Worker
│ │ │ │
│──GET /api/chat?stream───►│ │ │
│◄──Content-Type: text/ │ │ │
│ event-stream │ │ │
│ │──SUBSCRIBE job:{id}─►│ │
│ │ │◄──PUBLISH tokens──│
│◄──data: {token}──────────│◄──message────────────│ │
│◄──data: {token}──────────│◄──message────────────│ │
│◄──data: [DONE]───────────│◄──message {done}─────│ │
│ │──UNSUBSCRIBE────────►│ │2. Data Model
2.1 Entity Relationship Diagram
┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Users │ │ Organizations │ │ OrgMemberships │
├──────────────┤ ├──────────────────┤ ├─────────────────┤
│ id (uuid) PK │◄────│ id (uuid) PK │◄────│ id (uuid) PK │
│ email │ │ name │ │ userId FK │
│ passwordHash │ │ slug │ │ orgId FK │
│ name │ │ plan │ │ role (enum) │
│ avatarUrl │ │ stripeCustomerId │ └─────────────────┘
│ provider │ └──────────────────┘
│ emailVerified│
└──────┬───────┘ ┌───────────────────┐
│ │ Apps │
└────────────────►│ id (uuid) PK │
│ userId FK │
│ name, slug │
│ status (enum) │◄── draft|preview|deployed|paused|archived
│ dbSchema (text) │◄── Neon schema name for this app
│ settings (jsonb) │
└───────┬───────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌───────────────┐ ┌─────────────────┐
│ AppVersions │ │ Conversations │ │ Deployments │
├─────────────────┤ ├───────────────┤ ├─────────────────┤
│ version (int) │ │ appId FK │ │ appId FK │
│ files (jsonb) │ │ title │ │ provider (enum) │◄── vercel|k8s
│ parentVersionId │ └───────┬───────┘ │ url, subdomain │
└─────────────────┘ ▼ │ status (enum) │◄── building|live|failed
┌───────────────┐ └─────────────────┘
│ Messages │
├───────────────┤
│ role (enum) │◄── user|assistant|system
│ content (text)│
│ metadata(jsonb│◄── model, tokens, duration
└───────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Subscriptions │ │ Credits │ │ AuditLogs │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ plan (enum) │ │ balance (int) │ │ action (text) │
│ stripeSubId │ │ type (enum) │ │ resource (text) │
│ status (enum) │ │ expiresAt │ │ metadata (jsonb)│
└─────────────────┘ └─────────────────┘ └─────────────────┘2.2 Prisma Schema (Key Models)
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
passwordHash String?
name String?
avatarUrl String?
provider String? // google, github, email
providerId String?
emailVerified DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
apps App[]
memberships OrgMembership[]
subscription Subscription?
credits Credit[]
auditLogs AuditLog[]
usageLogs UsageLog[]
@@index([email])
@@index([provider, providerId])
}
model App {
id String @id @default(uuid()) @db.Uuid
userId String @db.Uuid
orgId String? @db.Uuid
name String
slug String
description String?
status AppStatus @default(DRAFT)
currentVersionId String? @db.Uuid
dbSchema String? // Neon schema name: "app_{id_short}"
settings Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
versions AppVersion[]
conversations Conversation[]
deployments Deployment[]
domains Domain[]
@@unique([userId, slug])
@@index([userId])
@@index([status])
}
model AppVersion {
id String @id @default(uuid()) @db.Uuid
appId String @db.Uuid
version Int
files Json // { "path": "content", ... }
parentVersionId String? @db.Uuid
createdAt DateTime @default(now())
@@unique([appId, version])
}
model Message {
id String @id @default(uuid()) @db.Uuid
conversationId String @db.Uuid
role MessageRole // USER | ASSISTANT | SYSTEM
content String
metadata Json? // { model, tokensIn, tokensOut, durationMs }
createdAt DateTime @default(now())
@@index([conversationId, createdAt])
}2.3 Enums
enum Plan { FREE STARTER PRO ENTERPRISE }
enum AppStatus { DRAFT PREVIEW DEPLOYED PAUSED ARCHIVED }
enum MessageRole { USER ASSISTANT SYSTEM }
enum CreditType { PURCHASED GRANTED EARNED }
enum DeployProvider { VERCEL K8S }
enum DeploymentStatus { BUILDING LIVE FAILED STOPPED }
enum UsageType { AI_GENERATION DEPLOYMENT DB_QUERY }2.4 Tenant Database Schema Pattern
Each user app that uses a database gets a Postgres schema within the same Neon database.
-- Created when user enables "database" for their app
CREATE SCHEMA IF NOT EXISTS app_a1b2c3d4;
-- Example: user's app has a "todos" table
CREATE TABLE app_a1b2c3d4.todos (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Isolation enforced via connection string with search_path set
-- ?options=-c search_path=app_{id}2.5 Multi-Tenancy Strategy
┌──────────────────────────────────────────────┐ │ Neon Postgres Instance │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ public │ │app_a1b2 │ │app_c3d4 │ │ │ │ (platform│ │(user app │ │(user app │ │ │ │ tables) │ │ tables) │ │ tables) │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ Each app schema isolated by search_path │ │ + dedicated Postgres role + RLS │ └──────────────────────────────────────────────┘
Isolation Guarantees
- Each app gets a dedicated Postgres role with access only to its schema
- Connection string includes
?options=-c search_path=app_{id} - RLS policies as defense-in-depth
- SQL validation before execution (block cross-schema queries)
2.6 Backup & Recovery
| Component | Strategy | Frequency | Retention |
|---|---|---|---|
| Platform DB | Neon automatic PITR | Continuous WAL | 7-30 days |
| Tenant schemas | Same Neon PITR | Continuous | Same as platform |
| App source code | AppVersion.files (JSONB) + R2 | Per version | Indefinite |
| Object storage (R2) | Cloudflare redundancy | Automatic | Indefinite |
3. AI Pipeline
3.1 Pipeline Overview
┌────────────┐ ┌──────────────┐ ┌─────────┐ ┌────────────┐ ┌──────────┐
│ User Input │───►│ Prompt │───►│ BullMQ │───►│ Ollama │───►│ Response │
│ (Chat msg) │ │ Assembly │ │ Queue │ │ Inference │ │ Parser │
└────────────┘ └──────────────┘ └─────────┘ └────────────┘ └────┬─────┘
│
┌──────────────┐ ┌─────────┐ ┌────────────┐ │
│ Preview │◄───│ File │◄───│ Validator │◄───────┘
│ Trigger │ │ Writer │ │ (lint/tsc) │
└──────────────┘ └─────────┘ └────────────┘3.2 Code Generation Pipeline Steps
8-Step Pipeline
1. Input Processing └─► Validate user message → Check credit balance → Load context (last 20 msgs) 2. Prompt Assembly └─► Select system prompt → Inject app template → Inject existing code + DB schema 3. Model Selection └─► Simple request → Llama 3.1 8B (fast, ~15 tok/s) └─► Complex request → DeepSeek Coder V2 16B (quality, ~8 tok/s) └─► User BYO API key → proxy to external provider 4. Inference (Ollama) └─► Stream tokens via HTTP → Relay via SSE + Redis pub/sub 5. Response Parsing └─► Extract code blocks (```filename.ext ... ```) → Parse file operations 6. Validation └─► TypeScript compilation check → ESLint pass → Security scan 7. File System Write └─► Write files to app virtual FS → Update app version 8. Preview Trigger └─► Push files to client WebContainer → Hot reload
3.3 System Prompts
// Base System Prompt (all requests):
You are an expert full-stack web developer. You generate complete, working
Next.js 14 applications using TypeScript, Tailwind CSS, and Prisma.
Rules:
- Output ONLY code files in the format: ```path/to/file.ext\n...code...\n```
- Use Next.js App Router (app/ directory)
- Use TypeScript strictly (no `any`)
- Use Tailwind CSS for styling (no CSS modules)
- Use Prisma for database access when DB is needed
- Include error handling and loading states
- Make UI responsive and accessible
- Never use external APIs unless explicitly asked3.4 Context Window Management
| Allocation | Budget % | ~Tokens (8K model) |
|---|---|---|
| System prompt | 10% | ~800 |
| Existing code | 40% | ~3,200 |
| Conversation history | 25% | ~2,000 |
| Current message | 10% | ~800 |
| Output reserve | 15% | ~1,200 |
3.5 Model Selection Logic
function selectModel(input: ProcessedInput): string {
// User has BYO API key → route externally
if (input.userApiKey) return 'external';
const hasExistingCode = Object.keys(input.currentFiles).length > 0;
const complexity = estimateComplexity(input);
// Simple iterations (small changes, bug fixes) → fast model
if (hasExistingCode && complexity === 'low') {
return 'llama3.1:8b'; // ~15 tok/s on RTX 4090
}
// New apps or complex changes → quality model
return 'deepseek-coder-v2:16b'; // ~8 tok/s on RTX 4090
}
function estimateComplexity(input: ProcessedInput): 'low' | 'medium' | 'high' {
const msg = input.userMessage.toLowerCase();
if (msg.length < 100 && /change|fix|update|rename|color|text|typo/i.test(msg))
return 'low';
if (/add.*page|new.*feature|database|auth|api.*route|integration/i.test(msg))
return 'high';
return 'medium';
}3.6 Hybrid AI Routing (BYO API Key)
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
│ User │────►│ AI Service │────►│ Route Decision │
│ Request │ │ (Next.js) │ │ │
└──────────┘ └──────────────┘ └────────┬─────────┘
│
┌─────────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Ollama │ │ CH Proxy → │ │ CH Proxy → │
│ (Default) │ │ OpenAI API │ │ Anthropic API│
│ Hetzner ZH │ │ (via Hetzner)│ │ (via Hetzner)│
└──────────────┘ └──────────────┘ └──────────────┘🔑 Key Point
User data transits through CH infrastructure even when using external providers. API keys encrypted with AES-256, per-app key derivation via HKDF.
3.7 SSE Event Format
// SSE events sent to browser:
event: token
data: {"t": "partial token text"}
event: file
data: {"path": "app/page.tsx", "content": "...full file..."}
event: done
data: {"usage": {"tokensIn": 1200, "tokensOut": 3400,
"model": "deepseek-coder-v2", "durationMs": 12340, "creditsUsed": 5}}
event: error
data: {"code": "GENERATION_FAILED", "message": "Model timed out"}3.8 Error Handling & Retry Strategy
| Error | Handling | Retry |
|---|---|---|
| Ollama unreachable | Return error; fallback to BYO key | 2 retries, exponential (5s, 15s) |
| Model OOM (VRAM) | Kill inference, retry smaller model | 1 retry with llama3.1:8b |
| Generation timeout (>3min) | Return partial result + error | No retry; suggest simplifying |
| Invalid code output | Show raw output + error | 1 auto-retry with "fix errors" prompt |
| Rate limit (queue full) | Return 429; show queue position | Client polls for slot |
3.9 Credit Cost Calculation
function calculateCredits(usage: { model: string; tokensIn: number; tokensOut: number }): number {
const rates = {
'llama3.1:8b': { input: 0.001, output: 0.002 }, // ~1 credit per simple request
'deepseek-coder-v2:16b': { input: 0.003, output: 0.006 }, // ~3-5 credits per complex request
'external:gpt-4': { input: 0, output: 0 }, // BYO key = 0 platform credits
};
const rate = rates[usage.model] ?? rates['deepseek-coder-v2:16b'];
return Math.ceil((usage.tokensIn * rate.input) + (usage.tokensOut * rate.output));
}4. Sandbox Architecture
4.1 Approach Comparison
| Criteria | WebContainers (Client) | Docker (Server) | Hybrid |
|---|---|---|---|
| Latency | Instant (runs in browser) | 2-5s container spin-up | Instant preview, server for build |
| Server cost | Zero | High (GPU server runs containers) | Low |
| Security | Browser sandbox (strong) | Container isolation (strong) | Both |
| Node.js support | Yes (WASM-based) | Full native | Both |
| Database access | Limited (PGlite in-browser) | Full Postgres access | Both |
| Max app complexity | Medium (browser memory) | High | High |
| Solo dev feasibility | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
ADR-004: WebContainers as Primary Sandbox
Decision: Use WebContainers (StackBlitz API) for app preview. PGlite for database preview. Server-side containers only for production builds.
Accepted — zero server infra for preview, instant hot reload, strong browser sandbox security
4.2 WebContainers + PGlite Architecture
┌─────────────────────────────────────────────────────────────┐ │ User's Browser │ │ │ │ ┌──────────────┐ ┌──────────────────────────────────┐ │ │ │ App Builder │ │ Preview Panel │ │ │ │ UI (React) │ │ ┌────────────────────────────┐ │ │ │ │ │ │ │ WebContainer Instance │ │ │ │ │ ┌──────────┐ │ │ │ ┌──────┐ ┌───────────┐ │ │ │ │ │ │ Monaco │─┼────┼─►│ │Node.js│ │ Next.js │ │ │ │ │ │ │ Editor │ │ │ │ │(WASM) │ │ Dev Server│ │ │ │ │ │ └──────────┘ │ │ │ └──────┘ └─────┬─────┘ │ │ │ │ │ ┌──────────┐ │ │ │ ┌──────────┐ │ │ │ │ │ │ │ AI Chat │ │ │ │ │ PGlite │◄───┘ │ │ │ │ │ │ Panel │ │ │ │ │(in-browser│ │ │ │ │ │ └──────────┘ │ │ │ │ Postgres) │ │ │ │ │ └──────────────┘ │ │ └──────────┘ │ │ │ │ │ └────────────────────────────┘ │ │ │ │ ┌────────────────────────────┐ │ │ │ │ │ Preview iframe │ │ │ │ │ │ (localhost:3000 proxy) │ │ │ │ │ └────────────────────────────┘ │ │ │ └──────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
4.3 Real-Time Preview Flow
SSE Stream App Builder UI WebContainer Preview iframe
│ │ │ │
│──file event───────►│ │ │
│ {path, content} │──writeFile()───────►│ │
│ │ │──HMR trigger────────►│
│ │ │ │──re-render
│──file event───────►│ │ │
│ {path, content} │──writeFile()───────►│ │
│ │ │──HMR trigger────────►│
│ │ │ │──re-render4.4 Security & Resource Limits
| Threat | Mitigation |
|---|---|
| File system access | WebContainer has virtual FS only — no access to user's real FS |
| Network access | Blocked by default; only localhost dev server allowed |
| XSS from preview | iframe sandbox attribute + separate origin |
| Memory bomb | Browser kills tab if memory exceeds ~512MB |
| Cookie/storage theft | Preview runs on different origin (null or blob:) |
4.5 Preview vs. Production
| Aspect | Preview (WebContainer) | Production (Deployed) |
|---|---|---|
| Runtime | WASM Node.js in browser | Real Node.js on Vercel/K8s |
| Database | PGlite (in-browser) | Neon Postgres |
| File system | Virtual (in-memory) | Real FS / serverless |
| Environment vars | Mocked / empty | Real secrets injected |
| Network | localhost only | Full internet access |
| Auth | Mocked (always logged in) | Real auth flow |
5. Deployment Pipeline
5.1 App Lifecycle
┌───────┐ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐
│ DRAFT │───►│ PREVIEW │───►│ DEPLOYED │───►│ PAUSED │───►│ ARCHIVED │
└───────┘ └─────────┘ └──────────┘ └────────┘ └──────────┘
│ │ │ │
│ │ │ └──► DEPLOYED (resume)
│ │ └──► DRAFT (unpublish, edit)
│ └──► DRAFT (back to editing)
└──► (delete)5.2 Phase 1: Vercel Deployment Flow
User clicks Platform API Vercel API Cloudflare
"Deploy" │ │ │
│──POST /deploy─────►│ │ │
│ │── 1. Validate ───►│ │
│ │── 2. Bundle code ►│ │
│ │── 3. Create proj ►│ │
│ │── 4. Upload files►│ │
│ │── 5. Create deploy│──build──► │
│ │── 6. Set env vars►│ │
│◄── started ────────│ │ │
│ │◄── 7. Ready ──────│ │
│ │── 8. CNAME ───────┼──────────────────►│
│◄── deployed! ──────│ myapp.crea.ch │ │5.3 Subdomain Management
┌─────────────────────────────────────────────────┐ │ Cloudflare DNS │ │ │ │ *.crea.ch → CNAME → cname.vercel-dns.com │ │ crea.ch → A → 76.76.21.21 (Vercel) │ │ │ │ SSL: Cloudflare Universal SSL (wildcard) │ │ + Vercel automatic SSL per deployment │ └─────────────────────────────────────────────────┘
- Auto-generated from app slug:
my-todo-app→my-todo-app.crea.ch - Reserved subdomains:
www, api, app, admin, docs, blog, mail, status - Validation: lowercase alphanumeric + hyphens, 3-63 chars
5.4 Environment Variables per App
| Variable | Source | Description |
|---|---|---|
| DATABASE_URL | Generated by platform | Neon Postgres URL scoped to app schema |
| NEXT_PUBLIC_APP_URL | Computed from subdomain | Public URL of deployed app |
| NODE_ENV | production | Standard Node env |
| APP_SECRET | Generated (random 32 bytes) | For signing cookies/tokens |
| User-defined | Encrypted in platform DB | User adds via settings panel |
5.5 Phase 2: K8s-Based Deployment
┌──────────────────────────────────────────────────────────┐ │ K8s Cluster (Swiss DC) │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Namespace: user-apps │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ app-abc │ │ app-def │ │ app-ghi │ ... │ │ │ │ │ Pod │ │ Pod │ │ Pod │ │ │ │ │ │ Next.js │ │ Next.js │ │ Next.js │ │ │ │ │ │ 256MB │ │ 256MB │ │ 512MB │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ ┌────┴──────────────┴──────────────┴──────────┐ │ │ │ │ │ Traefik Ingress Controller │ │ │ │ │ │ *.crea.ch → route by subdomain │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ │ ┌─────────────────────┐ ┌──────────────────────────┐ │ │ │ Container Registry │ │ Cert-Manager │ │ │ │ (Harbor or GHCR) │ │ (Let's Encrypt wildcard)│ │ │ └─────────────────────┘ └──────────────────────────┘ │ └──────────────────────────────────────────────────────────┘
5.6 Resource Allocation per Deployed App
| Plan | CPU Req/Limit | Memory Req/Limit | Scale |
|---|---|---|---|
| Free | 50m / 200m | 128MB / 256MB | 1 pod |
| Starter | 100m / 500m | 256MB / 512MB | 1-2 pods |
| Pro | 250m / 1000m | 512MB / 1GB | 1-5 pods |
| Enterprise | Custom | Custom | Auto |
5.7 Cost (Phase 1)
Phase 1 per-app cost: effectively $0
Vercel serverless model — pay only for compute time. Neon schemas are lightweight. Cloudflare DNS + SSL is free. R2 storage ~$0.015/GB/mo.
6. Infrastructure
6.1 Phase 1 Infrastructure
┌─────────────────────────────────────────────────────────────────────────────┐
│ INTERNET │
└───────────────────────────────────┬─────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ Cloudflare │
│ • CDN • WAF • DNS • R2 │
└───┬───────────┬───────────┬────┘
│ │ │
┌───────────┘ │ └───────────┐
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
│ Vercel │ │ Neon Postgres │ │ Hetzner Dedicated │
│ (Frankfurt/ZRH) │ │ (EU/Frankfurt) │ │ (Zürich DC) │
│ │ │ │ │ │
│ • Next.js App │ │ • Platform DB │ │ • Ollama (RTX 4090) │
│ • API Routes │ │ • Tenant Schemas │ │ • BullMQ Worker │
│ • Deployed Apps │ │ │ │ • Redis │
│ │ │ │ │ • Nginx Proxy │
└───────────────────┘ └───────────────────┘ └───────────────────────┘6.2 Phase 1 Cost Breakdown
| Service | Plan | Monthly Cost (CHF) |
|---|---|---|
| Vercel | Pro | ~22 |
| Neon Postgres | Scale (Launch) | ~19 |
| Hetzner GPU (RTX 4090) | EX44 or similar | ~200-350 |
| Cloudflare | Free (+ R2 usage) | ~5 |
| Upstash Redis | Pay-as-you-go | ~10-20 |
| Sentry | Developer | Free |
| Domain | Annual | ~5 |
| Total | ~260-420 | |
6.3 Hetzner GPU Server — Docker Compose
version: '3.8'
services:
ollama:
image: ollama/ollama:latest
ports: ["127.0.0.1:11434:11434"]
volumes: [ollama_data:/root/.ollama]
deploy:
resources:
reservations:
devices: [{ driver: nvidia, count: 1, capabilities: [gpu] }]
restart: unless-stopped
redis:
image: redis:7-alpine
ports: ["127.0.0.1:6379:6379"]
volumes: [redis_data:/data]
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
restart: unless-stopped
worker:
build: ./worker
environment:
- OLLAMA_URL=http://ollama:11434
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
- WORKER_CONCURRENCY=3
depends_on: [ollama, redis]
restart: unless-stopped
nginx:
image: nginx:alpine
ports: ["443:443"]
volumes: [./nginx.conf:/etc/nginx/nginx.conf, ./certs:/etc/nginx/certs]
restart: unless-stopped6.4 Phase 2: K8s Cluster Design
┌──────────────────────────────────────────────────────────────────┐ │ K8s Cluster (Swiss DC — Exoscale/Infomaniak) │ │ │ │ Node Pools: │ │ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │ │ platform-pool │ │ app-worker-pool │ │ gpu-pool │ │ │ │ 2x 4vCPU/8GB │ │ 2-10x 2vCPU/4GB │ │ 1x GPU node │ │ │ │ │ │ (autoscale) │ │ (RTX 4090) │ │ │ │ • Platform API │ │ • User app pods │ │ • Ollama │ │ │ │ • Redis │ │ • Build jobs │ │ • BullMQ worker │ │ │ │ • Monitoring │ │ │ │ │ │ │ └─────────────────┘ └──────────────────┘ └──────────────────┘ │ │ │ │ Namespaces: │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ platform │ │user-apps │ │monitoring│ │ ingress-system │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │ └──────────────────────────────────────────────────────────────────┘
6.5 CI/CD Pipeline
┌────────┐ ┌──────┐ ┌──────────┐ ┌─────────┐ ┌────────┐
│ Lint │──►│ Test │──►│ Type │──►│ Build │──►│ Deploy │
│ │ │ │ │ Check │ │ │ │ │
└────────┘ └──────┘ └──────────┘ └─────────┘ └────────┘
│
┌──────────────┼──────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌────────┐
│ Vercel │ │ Prisma │ │ Hetzner│
│ (app) │ │ Migrate │ │(worker)│
└─────────┘ └──────────┘ └────────┘6.6 Monitoring & Observability
Phase 1 (Lightweight)
- Sentry (errors) — free tier
- Vercel Analytics — included in Pro
- UptimeRobot — free tier
- Vercel Logs +
docker logs nvidia-smi+ simple script
Phase 2 (Full Stack on K8s)
- Prometheus (metrics)
- Loki (logs)
- Tempo (traces)
- Grafana (dashboards + alerting)
- PagerDuty/Slack alerts
6.7 Key Metrics & Alert Thresholds
| Metric | Source | Alert Threshold |
|---|---|---|
| API response time (p95) | Prometheus | > 500ms |
| AI generation time (p95) | Custom metric | > 120s |
| Queue depth | BullMQ / Redis | > 20 jobs |
| GPU VRAM usage | nvidia-smi exporter | > 90% |
| Error rate (5xx) | Prometheus | > 5% |
| Deployment success rate | Custom metric | < 90% |
6.8 Disaster Recovery
| Scenario | RTO | RPO | Recovery |
|---|---|---|---|
| Vercel outage | 0 | 0 | CDN cached; wait for recovery |
| Neon outage | 1h | ~0 | Wait for recovery; read-only mode |
| Hetzner GPU down | 15min | 0 | Disable AI; enable BYO API fallback |
| Data corruption | 30min | Minutes | Neon PITR restore |
| Security breach | 1h | 0 | Rotate secrets; revoke sessions; audit |
7. Security
7.1 Authentication Architecture
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Browser │────▶│ NextAuth.js │────▶│ PostgreSQL │
│ │◀────│ /api/auth/* │◀────│ users table │
└─────────────┘ └──────┬───────┘ └─────────────┘
│
┌──────┴───────┐
│ OAuth 2.0 │
│ • Google │
│ • GitHub │
└──────────────┘- Passwords hashed with bcrypt (cost factor 12)
- JWT strategy —
httpOnly,Secure,SameSite=Lax - Access token: 24h expiry, Refresh token: 30-day (rotated on use)
- Account lockout after 5 failed attempts (15-min cooldown)
7.2 Authorization — Platform RBAC
| Role | Apps (own) | Apps (team) | Team Mgmt | Billing | Admin |
|---|---|---|---|---|---|
| Owner | CRUD | CRUD | ✅ | ✅ | ✅ |
| Admin | CRUD | CRUD | ✅ | ❌ | ❌ |
| Member | CRUD | Read+Edit | ❌ | ❌ | ❌ |
| Viewer | — | Read | ❌ | ❌ | ❌ |
7.3 Row-Level Security (Tenant Isolation)
-- Enable RLS on apps table
ALTER TABLE apps ENABLE ROW LEVEL SECURITY;
-- Users can only see their own tenant's apps
CREATE POLICY tenant_isolation ON apps
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- Set tenant context on every request
CREATE OR REPLACE FUNCTION set_tenant_context(p_tenant_id uuid)
RETURNS void AS $$
BEGIN
PERFORM set_config('app.current_tenant_id', p_tenant_id::text, true);
END;
$$ LANGUAGE plpgsql;7.4 API Security
Rate Limiting
| Plan | Requests/min | Burst | AI Requests/min |
|---|---|---|---|
| Free | 10 | 15 | 3 |
| Starter | 30 | 45 | 10 |
| Pro | 100 | 150 | 30 |
| Business | 300 | 450 | 100 |
| Enterprise | Custom | Custom | Custom |
Generated App Security (Auto-Included)
- CSRF: Double-submit cookie pattern
- XSS: React JSX escaping + DOMPurify for any dangerouslySetInnerHTML
- SQL Injection: Prisma ORM with parameterized queries (never raw SQL)
- Secure Headers: X-Content-Type-Options, X-Frame-Options, HSTS, Permissions-Policy
- Input Validation: Zod schemas generated for all API routes
7.5 Network Security
┌─────────────┐ ┌───────────────────────┐
│ Vercel │ │ Hetzner GPU Server │
│ Edge │◀════════▶│ │
│ │ WireGuard│ ┌─────────────┐ │
│ Next.js │ Tunnel │ │ Ollama │ │
│ App │ (TLS) │ │ :11434 │ │
│ │ │ └─────────────┘ │
└─────────────┘ │ │
│ Firewall: │
│ • SSH: key-only │
│ • WireGuard: 51820 │
│ • All other: DENY │
└───────────────────────┘7.6 FADP/nDSG Compliance
Data Inventory
| Data Category | Storage Location | Retention | Encryption |
|---|---|---|---|
| User accounts | PostgreSQL (CH) | Until deletion | AES-256 |
| App source code | PostgreSQL (CH) | Until deletion | AES-256 |
| Conversation history | PostgreSQL (CH) | 90 days | AES-256 |
| Audit logs | PostgreSQL (CH) | 90 days | AES-256 |
| Payment data | Stripe (EU) | Per Stripe | Stripe-managed |
| AI inference | RAM only (CH) | None (transient) | N/A |
Breach Notification Workflow
Incident Detected
│
▼
Assess Severity ──▶ Not a breach ──▶ Log & Close
│
▼ (personal data affected)
Notify FDPIC within 72h
│
▼ (high risk to individuals)
Notify affected users "as soon as possible"
│
▼
Post-mortem & remediation7.7 Security Checklist by Phase
Phase 1 (MVP) ✅
- NextAuth.js with bcrypt + JWT
- RLS on all tenant tables
- Rate limiting (Redis)
- Zod input validation
- CORS + CSP headers
- WireGuard tunnel to GPU server
- Audit logging
- Dependabot + npm audit
- FADP processing register
Phase 2 (Scale) ⬜
- HashiCorp Vault for secrets
- K8s network policies
- Trivy container scanning
- Snyk integration
- Annual pen test
- WAF (ModSecurity / Cloudflare)
- SOC 2 Type I preparation
8. API Design
8.1 Common Types
// Standard error response
interface ErrorResponse {
error: {
code: string; // e.g., 'VALIDATION_ERROR', 'NOT_FOUND'
message: string; // Human-readable
details?: unknown; // Validation errors, etc.
};
requestId: string; // UUID for support/debugging
}
// Paginated response wrapper
interface PaginatedResponse<T> {
data: T[];
nextCursor: string | null;
hasMore: boolean;
}
// Timestamp fields (all models)
interface Timestamps {
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}8.2 API Endpoints Overview
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register | — | Create new account |
| POST | /api/auth/login | — | Authenticate, get JWT |
| GET | /api/auth/me | ✅ | Current user profile |
| GET | /api/apps | ✅ | List user apps (paginated) |
| POST | /api/apps | ✅ | Create app (with optional AI prompt) |
| GET | /api/apps/:id | ✅ | App detail |
| PATCH | /api/apps/:id | ✅ | Update app metadata/env vars |
| DELETE | /api/apps/:id | ✅ | Soft delete (30-day recovery) |
| POST | /api/apps/:id/chat | ✅ | AI chat → SSE stream |
| GET | /api/apps/:id/chat/history | ✅ | Conversation history |
| GET | /api/apps/:id/files | ✅ | File tree |
| GET | /api/apps/:id/files/:path | ✅ | File content |
| PUT | /api/apps/:id/files/:path | ✅ | Update file |
| POST | /api/apps/:id/deploy | ✅ | Deploy app |
| GET | /api/apps/:id/deployments | ✅ | Deployment history |
| POST | /api/apps/:id/domains | ✅ | Add custom domain |
| GET | /api/billing/subscription | ✅ | Current plan & usage |
| POST | /api/billing/checkout | ✅ | Stripe checkout redirect |
| POST | /api/billing/webhook | Stripe sig | Stripe webhook handler |
| GET | /api/templates | — | Browse templates |
| GET | /api/admin/stats | Owner | Platform admin stats |
8.3 SSE Chat Streaming
// POST /api/apps/:id/chat — Request
interface ChatRequest {
message: string; // 1-50000 chars
attachments?: { type: 'image'; url: string }[];
}
// Response: text/event-stream (SSE)
type SSEEvent =
| { event: 'token'; data: { content: string } }
| { event: 'file_start'; data: { path: string; action: 'create' | 'update' | 'delete' } }
| { event: 'file_content'; data: { path: string; content: string } }
| { event: 'file_end'; data: { path: string } }
| { event: 'error'; data: { code: string; message: string } }
| { event: 'done'; data: { messageId: string; version: number; tokensUsed: number } };Example SSE stream:
event: token
data: {"content":"I'll create a todo app with authentication."}
event: file_start
data: {"path":"src/app/page.tsx","action":"create"}
event: file_content
data: {"path":"src/app/page.tsx","content":"'use client';\nimport { useState } from 'react';..."}
event: file_end
data: {"path":"src/app/page.tsx"}
event: done
data: {"messageId":"msg_abc123","version":2,"tokensUsed":1847}8.4 Error Codes
| Code | HTTP | Description |
|---|---|---|
| VALIDATION_ERROR | 422 | Request body failed schema validation |
| NOT_FOUND | 404 | Resource does not exist |
| UNAUTHORIZED | 401 | Missing or invalid auth token |
| FORBIDDEN | 403 | Insufficient permissions |
| EMAIL_EXISTS | 409 | Email already registered |
| PLAN_LIMIT_REACHED | 402 | Plan quota exceeded |
| RATE_LIMITED | 429 | Too many requests |
| ACCOUNT_LOCKED | 423 | Too many failed login attempts |
| INTERNAL_ERROR | 500 | Unexpected server error |
8.5 Webhook Design
| Event | Trigger |
|---|---|
| deployment.completed | Deployment goes live |
| deployment.failed | Deployment build fails |
| app.created | New app created |
| app.deleted | App deleted |
// Webhook payload
interface WebhookPayload {
id: string; // Webhook delivery ID
event: string; // e.g., 'deployment.completed'
timestamp: string; // ISO 8601
data: Record<string, unknown>;
}
// Signature verification (HMAC-SHA256)
// Header: X-Webhook-Signature: sha256=<hex_digest>
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expected = `sha256=${createHmac('sha256', secret).update(payload).digest('hex')}`;
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Delivery: 3 retries (10s, 60s, 300s), 10s timeout, expect 2xx8.6 API Versioning
- Phase 1 (MVP): No version prefix —
/api/auth/login - Phase 2: Version prefix —
/api/v1/auth/login - Old unversioned routes redirect to v1
- Deprecation via
Sunsetheader (6-month window)