- 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
7.7 KiB
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):
- Validate - Check name format, reserved names, availability
- DNS - Create
{name}.ai.flexinit.nl→ 144.76.116.169 (or rely on wildcard) - Dokploy Project - Create project via API
- Dokploy App - Create application, add domain, deploy
- 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
- Hetzner API - Use
api.hetzner.cloud, NOTdns.hetzner.com(deprecated) - Dokploy domain - MUST create domain AFTER application exists (or 404)
- SSL delay - Let's Encrypt cert takes 30-60 seconds to provision
- ttyd WebSocket - Requires Traefik WebSocket support configured
- Container startup - OpenCode server takes ~10 seconds to be ready
- 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:
- OpenCode server (port 8080)
- 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
constoverlet - 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:
README.md- Full project specificationAGENTS.md- This file- 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