Skip to content

Latest commit

Β 

History

History
400 lines (329 loc) Β· 14.1 KB

File metadata and controls

400 lines (329 loc) Β· 14.1 KB

stapeln Architecture

Overview

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

Frontend Architecture (ReScript-TEA)

The Elm Architecture (TEA)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Model  β”‚  Current application state
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚
     ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  View   β”‚  Renders UI from Model
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚
     ↓ (User Interaction)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Msg   β”‚  User events, API responses
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚
     ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Update  β”‚  Model β†’ Msg β†’ (Model, Cmd)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Component Structure

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

Canvas Implementation

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:

  1. User clicks component in palette β†’ StartDragComponent
  2. Mouse moves β†’ DragMove(position)
  3. 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)

Backend Architecture (Elixir + Phoenix)

Service Layers

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 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)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Validation Service (Ephapax + Idris2)

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
end

Idris2 (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
end

Code Generation (Rust)

Why 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)
end

AffineScript Alternative (MVP)

Note: 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(())
}

API Design

REST Endpoints

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

WebSocket Channel

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
end

Data Flow

1. User Adds Component

Frontend (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

2. User Validates Stack

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          β”‚                          β”‚

3. User Exports Stack

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" β”‚                        β”‚
    ←───────────────────────                        β”‚

Performance Considerations

Frontend

  • 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

Backend

  • 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

Security

Frontend

  • CSP headers: Prevent XSS
  • Input validation: All user input sanitized
  • CORS: Restrict API access

Backend

  • Authentication: JWT tokens
  • Authorization: Stack ownership validation
  • Rate limiting: Per-IP and per-user
  • Sandboxing: Idris2/Ephapax run in isolated processes

Deployment

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Nginx (TLS)     β”‚ Port 443
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
    β”‚         β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β–Όβ”€β”€β”€β”€β”
β”‚ Deno  β”‚ β”‚Phoenixβ”‚ Ports 8000, 4010
β”‚frontendβ”‚ β”‚backendβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”˜
              β”‚
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”
      β”‚       β”‚       β”‚
  β”Œβ”€β”€β”€β–Όβ”€β”€β” β”Œβ”€β”€β–Όβ”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β”
  β”‚Postgresβ”‚Idris2β”‚Ephapaxβ”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜

Future Enhancements

  1. Collaborative editing - Multiple users editing same stack
  2. Version control - Git-like stack versioning
  3. Templates - Pre-built stack templates
  4. AI suggestions - ML-based component recommendations
  5. Performance profiling - Predict stack resource usage
  6. Cost estimation - Cloud deployment cost prediction

References