Files
ai-stack-deployer/docs/AGENTS.md
Oussama Douhou 2f306f7d68 feat: production-ready deployment with multi-language UI
- Add multi-language support (NL, AR, EN) with RTL
- Improve health checks (SSL-tolerant, multi-endpoint)
- Add DELETE /api/stack/:name for cleanup
- Add persistent storage (portal-ai-workspace-{name})
- Improve rollback (delete domain, app, project)
- Increase SSE timeout to 255s
- Add deployment strategy documentation
2026-01-10 09:56:33 +01:00

7.7 KiB

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)

DOKPLOY_TOKEN=$(bws-wrapper get 6b3618fc-ba02-49bc-bdc8-b3c9004087bc)
HETZNER_TOKEN=$(bws-wrapper get <HETZNER_BWS_ID>)  # 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)

# 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

# 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:

// 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:

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

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

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

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