stapeln is a visual drag-and-drop container stack designer built with:
- Frontend: ReScript-TEA (The Elm Architecture) + Deno
- Backend: Elixir (Phoenix) + Ephapax + Idris2 + Rust
- Communication: REST API + WebSocket
βββββββββββ
β Model β Current application state
ββββββ¬βββββ
β
β
βββββββββββ
β View β Renders UI from Model
ββββββ¬βββββ
β
β (User Interaction)
βββββββββββ
β Msg β User events, API responses
ββββββ¬βββββ
β
β
βββββββββββ
β Update β Model β Msg β (Model, Cmd)
βββββββββββ
frontend/src/
βββ Main.res # Entry point, TEA initialization
βββ Model.res # State types (component, connection, dragState)
βββ Msg.res # Event types (AddComponent, DragMove, etc.)
βββ Update.res # State transitions
βββ View.res # Main view renderer
βββ Canvas.res # Drag-and-drop canvas
βββ Components.res # Component library (palette)
βββ Connections.res # Connection line rendering
βββ Config.res # Configuration panel
βββ Export.res # Export menu
βββ Router.res # cadre-tea-router integration
SVG-based canvas for:
- Infinite canvas (pan/zoom)
- Crisp rendering at any zoom level
- Easy connection line drawing (SVG paths)
- CSS styling
Drag-and-Drop:
- User clicks component in palette β
StartDragComponent - Mouse moves β
DragMove(position) - Mouse release β
DragEndβ Add component at position
Connection Lines:
- Click component β select (highlight)
- Click another component β create connection
- Connections drawn as SVG paths (bezier curves or orthogonal lines)
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Phoenix Web Layer (REST + WebSocket) β
β - StackController (CRUD) β
β - StackChannel (real-time updates) β
β - ExportController (generate compose files) β
ββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
β
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Business Logic (Elixir) β
β - Stack management β
β - Component orchestration β
β - Validation coordination β
ββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
ββββββββ΄βββββββ¬βββββββββββββββ
β β β
ββββββββββββ ββββββββββββ ββββββββββββ
β Validatorβ β Prover β β Codegen β
β (Ephapax)β β (Idris2) β β (Rust) β
ββββββββββββ ββββββββββββ ββββββββββββ
Ephapax (Linear Types):
defmodule Stackur.Validator.Ephapax do
# Validate linear resource usage
# - Each volume consumed exactly once
# - No dangling port references
# - Network resources properly scoped
def validate_linear_types(stack) do
# Call Ephapax via Port/NIF
# Returns: {:ok, proof} | {:error, violations}
end
endIdris2 (Formal Proofs):
-- validation/src/Stack.idr
data Stack = MkStack (List Component) (List Connection)
-- Proof: No circular dependencies
noCycles : (s : Stack) -> Dec (Acyclic s)
-- Proof: Dependencies form a DAG
dependencyDAG : (s : Stack) -> Either DAGError (TopologicalOrder s)
-- Proof: Port conflict detection
noPortConflicts : (s : Stack) -> Dec (UniquePortBindings s)Integration:
defmodule Stackur.Validator.Idris do
# Calls compiled Idris2 executable via Port
def prove_correctness(stack_json) do
Port.open({:spawn_executable, "/usr/local/bin/stapeln-prover"},
[:binary, :use_stdio, {:args, [stack_json]}])
end
endWhy Rust for Codegen?
- Type-safe TOML generation
- Fast compilation
- Elixir NIF integration (Rustler)
- Memory safety guarantees
// backend/native/stapeln_codegen/src/lib.rs
use rustler::{Encoder, Env, Term};
use toml::Value;
#[rustler::nif]
fn generate_compose_toml(stack_json: String) -> Result<String, String> {
let stack: Stack = serde_json::from_str(&stack_json)
.map_err(|e| format!("Parse error: {}", e))?;
let toml = compose_toml_from_stack(&stack)?;
Ok(toml::to_string(&toml).unwrap())
}
fn compose_toml_from_stack(stack: &Stack) -> Result<Value, String> {
// Build compose.toml structure
// Handle services, volumes, networks
// Validate against verified-container-spec
}Elixir NIF wrapper:
defmodule Stackur.Codegen do
use Rustler, otp_app: :stapeln, crate: "stapeln_codegen"
def generate_compose_toml(_stack_json), do: :erlang.nif_error(:nif_not_loaded)
def generate_docker_compose(_stack_json), do: :erlang.nif_error(:nif_not_loaded)
def generate_podman_compose(_stack_json), do: :erlang.nif_error(:nif_not_loaded)
endNote: AffineScript compiler may not be production-ready.
MVP Alternative: Use Rust's affine type system:
// Affine types: used at most once
struct Volume(String); // Can't be cloned
impl Volume {
fn mount_once(self, container: &mut Container) {
// Consumes self, can't be used again
container.volumes.push(self);
}
}
// Port conflict detection
fn check_port_conflicts(services: &[Service]) -> Result<(), String> {
let mut ports = HashSet::new();
for service in services {
for port in &service.ports {
if !ports.insert(port.host_port) {
return Err(format!("Port {} already bound", port.host_port));
}
}
}
Ok(())
}POST /api/stacks Create stack
GET /api/stacks/:id Get stack
PUT /api/stacks/:id Update stack
DELETE /api/stacks/:id Delete stack
POST /api/stacks/:id/validate Validate stack
POST /api/stacks/:id/export Export to compose file
defmodule StackurWeb.StackChannel do
use Phoenix.Channel
def join("stack:" <> stack_id, _payload, socket) do
{:ok, assign(socket, :stack_id, stack_id)}
end
def handle_in("update_component", payload, socket) do
# Update component, broadcast to all clients
broadcast(socket, "component_updated", payload)
{:noreply, socket}
end
def handle_in("validate", _payload, socket) do
stack_id = socket.assigns.stack_id
# Async validation
Task.start(fn ->
result = Stackur.Validator.validate(stack_id)
broadcast(socket, "validation_result", result)
end)
{:noreply, socket}
end
endFrontend (ReScript) Backend (Elixir)
β β
β Drag component to canvas β
β β AddComponent(Svalinn, {x,y}) β
β β
β WebSocket: "add_component" β
βββββββββββββββββββββββββββββββββββββ
β β Validate placement
β β Save to database
β β
β β "component_added" β
ββββββββββββββββββββββββββββββββββββ€
β β
Update Model Broadcast to other clients
Frontend Backend Validator
β β β
β Click "Validate" β β
β β ValidateStack β β
β β β
β POST /api/stacks/:id/validate β β
βββββββββββββββββββββββββββββββββββ β
β β Convert to Ephapax AST β
β ββββββββββββββββββββββββββββ
β β β
β β β Linear type check β
β βββββββββββββββββββββββββββ€
β β β
β β Call Idris2 prover β
β ββββββββββββββββββββββββββββ
β β β
β β β Correctness proofs β
β βββββββββββββββββββββββββββ€
β β β
β β ValidationResult({valid, β β
β errors, warnings}) β β
ββββββββββββββββββββββββββββββββββ€ β
β β β
Update Model with result β β
Frontend Backend Codegen (Rust)
β β β
β Click "Export as β β
β selur-compose" β β
β β ExportToSelurComposeβ β
β β β
β POST /api/stacks/:id/ β β
β export?format=selur β β
βββββββββββββββββββββββββ β
β β NIF call: generate_ β
β β compose_toml(stack) β
β ββββββββββββββββββββββββββ
β β β
β β β compose.toml string β
β βββββββββββββββββββββββββ€
β β β
β β File download β β
β "stack.compose.toml" β β
ββββββββββββββββββββββββ€ β
- Virtual DOM: ReScript compiles to efficient JS
- Debounced updates: Drag events throttled to 60fps
- Canvas optimization: Only redraw changed components
- WebSocket batching: Group rapid updates
- Concurrent validation: Elixir processes per stack
- Caching: Validation results cached with TTL
- NIF for CPU-intensive: Rust NIFs for codegen
- Streaming: Large exports streamed via chunks
- CSP headers: Prevent XSS
- Input validation: All user input sanitized
- CORS: Restrict API access
- Authentication: JWT tokens
- Authorization: Stack ownership validation
- Rate limiting: Per-IP and per-user
- Sandboxing: Idris2/Ephapax run in isolated processes
βββββββββββββββββββ
β Nginx (TLS) β Port 443
ββββββββββ¬βββββββββ
β
ββββββ΄βββββ
β β
βββββΌββββ ββββΌβββββ
β Deno β βPhoenixβ Ports 8000, 4010
βfrontendβ βbackendβ
βββββββββ βββββ¬ββββ
β
βββββββββΌββββββββ
β β β
βββββΌβββ ββββΌβββ βββΌβββββ
βPostgresβIdris2βEphapaxβ
βββββββββ βββββββ ββββββββ
- Collaborative editing - Multiple users editing same stack
- Version control - Git-like stack versioning
- Templates - Pre-built stack templates
- AI suggestions - ML-based component recommendations
- Performance profiling - Predict stack resource usage
- Cost estimation - Cloud deployment cost prediction