# AI Stack Deployer - Agent Instructions **MANDATORY: Read before implementing** --- ## PROJECT IDENTITY Self-service portal: Users enter name → Get deployed AI assistant at `{name}.ai.flexinit.nl` **Stack:** - Backend: Bun + Hono - Frontend: Vanilla HTML/CSS/JS (NO frameworks) - Deploy: Docker + Dokploy - Network: Hetzner DNS + Traefik --- ## MANDATORY: DEPLOYMENT WORKFLOW **User submits name → Deploy AI stack (5 steps):** 1. **Validate** - Check name format, reserved names, availability 2. **DNS** - Create `{name}.ai.flexinit.nl` → 144.76.116.169 (or rely on wildcard) 3. **Dokploy Project** - Create project via API 4. **Dokploy App** - Create application, add domain, deploy 5. **Verify** - Wait for SSL, test OpenCode + ttyd endpoints **CRITICAL:** Each step MUST succeed before proceeding. No parallel execution. --- ## MANDATORY: SECRETS (Get FIRST) ```bash DOKPLOY_TOKEN=$(bws-wrapper get 6b3618fc-ba02-49bc-bdc8-b3c9004087bc) HETZNER_TOKEN=$(bws-wrapper get ) # Ask user or search BWS ``` --- ## MANDATORY: API COMMANDS (COPY EXACTLY) ### Hetzner DNS API (MODIFY ONLY: name, comment) **CRITICAL:** Use `api.hetzner.cloud/v1`, NOT `dns.hetzner.com` (deprecated) ```bash # List existing DNS records curl -s "https://api.hetzner.cloud/v1/zones/343733/rrsets" \ -H "Authorization: Bearer $HETZNER_TOKEN" # Create A record (OPTIONAL - wildcard *.ai.flexinit.nl already exists) curl -X POST "https://api.hetzner.cloud/v1/zones/343733/rrsets" \ -H "Authorization: Bearer $HETZNER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "{name}.ai", "type": "A", "ttl": 300, "records": [{"value": "144.76.116.169", "comment": "AI Stack for {name}"}] }' ``` **Constants:** - Zone ID: `343733` (flexinit.nl) - Traefik IP: `144.76.116.169` - Wildcard: `*.ai.flexinit.nl` → 144.76.116.169 (pre-configured) ### Dokploy API (MODIFY ONLY: projectId, applicationId, name, domain) **Base:** `http://10.100.0.20:3000/api` ```bash # Create project curl -X POST "http://10.100.0.20:3000/api/project.create" \ -H "Authorization: Bearer $DOKPLOY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "{username}-ai-stack"}' # Create application curl -X POST "http://10.100.0.20:3000/api/application.create" \ -H "Authorization: Bearer $DOKPLOY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"projectId": "...", "name": "{username}-opencode"}' # Add domain curl -X POST "http://10.100.0.20:3000/api/domain.create" \ -H "Authorization: Bearer $DOKPLOY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"applicationId": "...", "host": "{name}.ai.flexinit.nl"}' # Deploy application curl -X POST "http://10.100.0.20:3000/api/application.deploy" \ -H "Authorization: Bearer $DOKPLOY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"applicationId": "..."}' # Check status curl -s "http://10.100.0.20:3000/api/application.one?applicationId=..." \ -H "Authorization: Bearer $DOKPLOY_TOKEN" ``` --- ## CRITICAL: MANDATORY RULES **NEVER:** - Use frontend frameworks (React, Vue, Svelte, etc.) - Use old Hetzner API (`dns.hetzner.com`) - Create Dokploy domain BEFORE application exists - Skip error handling on ANY API call - Store secrets in code - Proceed to next step if current step fails **ALWAYS:** - Validate name (alphanumeric, 3-20 chars, not reserved) - Get tokens from BWS first - Wait for SSL certificate (30-60 seconds) - Verify each deployment step - Handle errors gracefully with user feedback - Mobile-responsive design --- ## CRITICAL: GOTCHAS 1. **Hetzner API** - Use `api.hetzner.cloud`, NOT `dns.hetzner.com` (deprecated) 2. **Dokploy domain** - MUST create domain AFTER application exists (or 404) 3. **SSL delay** - Let's Encrypt cert takes 30-60 seconds to provision 4. **ttyd WebSocket** - Requires Traefik WebSocket support configured 5. **Container startup** - OpenCode server takes ~10 seconds to be ready 6. **Reserved names** - Block: admin, api, www, root, test, staging, prod, etc. --- ## MANDATORY: VALIDATION **Before deployment, check:** ```javascript // Name validation regex const isValid = /^[a-z0-9]{3,20}$/.test(name); // Reserved names const reserved = ['admin', 'api', 'www', 'root', 'test', 'staging', 'prod']; const isReserved = reserved.includes(name); ``` --- ## MANDATORY: VERIFICATION **After deployment, verify:** ```bash # 1. SSL certificate provisioned curl -I "https://{name}.ai.flexinit.nl" # Should return 200 # 2. OpenCode server responding curl "https://{name}.ai.flexinit.nl" # Should show OpenCode UI # 3. ttyd terminal accessible curl -I "https://{name}.ai.flexinit.nl:7681" # Should return 200 ``` --- ## Implementation Patterns ### Backend (Bun + Hono) ```typescript // Use Hono for routing import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { serveStatic } from 'hono/bun'; const app = new Hono(); // Serve frontend app.use('/*', serveStatic({ root: './src/frontend' })); // API routes app.post('/api/deploy', deployHandler); app.get('/api/status/:id', statusHandler); app.get('/api/check/:name', checkHandler); ``` ### SSE Implementation ```typescript // Server-Sent Events for progress updates app.get('/api/status/:id', (c) => { return streamSSE(c, async (stream) => { // Send progress updates await stream.writeSSE({ event: 'progress', data: JSON.stringify({ step: 'dns', status: 'completed' }) }); // ... more updates await stream.writeSSE({ event: 'complete', data: JSON.stringify({ url: 'https://...' }) }); }); }); ``` ### Frontend (Vanilla JS State Machine) ```javascript // State: 'idle' | 'deploying' | 'success' | 'error' let state = 'idle'; function setState(newState, data = {}) { state = newState; render(state, data); } // SSE connection const eventSource = new EventSource(`/api/status/${deploymentId}`); eventSource.addEventListener('progress', (e) => { const data = JSON.parse(e.data); updateProgress(data); }); eventSource.addEventListener('complete', (e) => { setState('success', JSON.parse(e.data)); }); ``` ### Docker Stack Template The user stack needs: 1. OpenCode server (port 8080) 2. ttyd web terminal (port 7681) ```dockerfile FROM git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest # Install ttyd RUN apt-get update && apt-get install -y ttyd # Expose ports EXPOSE 8080 7681 # Start both services CMD ["sh", "-c", "opencode serve --host 0.0.0.0 --port 8080 & ttyd -W -p 7681 opencode attach http://localhost:8080"] ``` --- ## Code Style ### TypeScript - Use strict mode - Prefer `const` over `let` - Use async/await over callbacks - Handle all errors explicitly - Type all function parameters and returns ### CSS - Use CSS variables for theming - Mobile-first responsive design - BEM-like naming: `.component__element--modifier` - Dark theme as default ### JavaScript (Frontend) - No frameworks, vanilla only - Module pattern for organization - Event delegation where possible - Graceful degradation --- ## MANDATORY: TESTING CHECKLIST **Before marking complete, ALL must pass:** - [ ] Name validation works (alphanumeric, 3-20 chars) - [ ] Reserved names blocked (admin, api, www, root, etc.) - [ ] DNS record created successfully - [ ] Dokploy project created - [ ] Application deployed and healthy - [ ] SSL certificate provisioned - [ ] ttyd accessible in browser - [ ] Error states handled gracefully - [ ] Mobile responsive - [ ] Loading states smooth (no flicker) ## File Reading Order When starting implementation, read in this order: 1. `README.md` - Full project specification 2. `AGENTS.md` - This file 3. Check existing oh-my-opencode-free for reference patterns --- ## Reference Projects - `~/locale-projects/oh-my-opencode-free` - The stack being deployed - `~/projecten/infrastructure` - Infrastructure patterns and docs