██████╗  ██╗ ██╗  ██╗ ███████╗ ███████╗ ██╗      ██╗ ███╗   ███╗  █████╗   █████╗ 
██╔══██╗ ██║ ██║ ██╔╝ ██╔════╝ ██╔════╝ ██║      ██║ ████╗ ████║ ██╔══██╗ ██╔══██╗
██████╔╝ ██║ █████╔╝  ███████╗ █████╗   ██║      ██║ ██╔████╔██║ ███████║ ███████║
██╔═══╝  ██║ ██╔═██╗  ╚════██║ ██╔══╝   ██║      ██║ ██║╚██╔╝██║ ██╔══██║ ██╔══██║
██║      ██║ ██║  ██╗ ███████║ ███████╗ ███████╗ ██║ ██║ ╚═╝ ██║ ██║  ██║ ██║  ██║
╚═╝      ╚═╝ ╚═╝  ╚═╝ ╚══════╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝     ╚═╝ ╚═╝  ╚═╝ ╚═╝  ╚═╝
        
◆ PIKSELIMAA.FI ◆ SYSTEM ONLINE ◆ VER 47.0 ◆

▶ CASE STUDIES

Technical work I've shipped — architecture, trade-offs, and honest reflections. Click any entry to expand the full case study.

emoji.pikselimaa.fi — Privacy-First Emoji Picker Vanilla JavaScript, Web Components, nginx
[ Production ]

Problem

Finding and inserting emoji should be simple, but most solutions compromise on privacy or usability:

  • Native OS pickers vary wildly between platforms and often lack search
  • Web-based pickers load dozens of tracking scripts and phone home with every click
  • Clipboard managers require installation and don't work consistently across browsers
  • Search-based tools are often slow, requiring round-trips to servers for suggestions

The goal was a fast, searchable, install-nothing emoji tool that respects user privacy — no cookies, no analytics, no external scripts.

Solution

A self-hosted, single-page emoji picker with instant search and a virtual keyboard. Zero build steps, zero runtime dependencies, zero tracking.

Architecture

flowchart LR
    Browser[Browser] -->|Static Files| Nginx[nginx]
    Nginx -->|HTML/CSS/JS| App[Emoji Picker App]
    App -->|Emoji Data| Local[(Local JSON)]
    App -->|Copy to clipboard| Clipboard[Clipboard API]
    App -->|Virtual keyboard input| Output[Target Input]

Data Flow:

  1. Browser loads static HTML, CSS, and vanilla JavaScript (no build step)
  2. Complete emoji database loaded as JSON (~500KB uncompressed, served with Brotli compression)
  3. Search runs entirely client-side using fast string matching on shortcodes and keywords
  4. Click-to-copy uses modern Clipboard API with fallback for older browsers
  5. Virtual keyboard allows multi-emoji composition before copying

Key Features

  • Instant search: Type "fire" and see 🔥 in milliseconds, no server round-trip
  • Category browsing: Browse by category (Smileys, Activities, Objects, etc.)
  • Virtual keyboard: Click emoji to add to a buffer, copy when ready
  • Recent picks: LocalStorage remembers frequently used emoji per-browser
  • Zero tracking: No Google Fonts, no analytics, no cookies, no fingerprinting
  • Works offline: Service worker caches all assets after first visit

Technical Highlights

Web Components Architecture

Built with vanilla Web Components instead of frameworks:

class EmojiPicker extends HTMLElement {
  connectedCallback() {
    this.searchInput = this.querySelector('#search');
    this.grid = this.querySelector('#grid');
    this.searchInput.addEventListener('input', (e) => this.filter(e.target.value));
  }
  
  filter(query) {
    const matches = this.emojiData.filter(e => 
      e.shortcode.includes(query) || 
      e.keywords.some(k => k.includes(query))
    );
    this.renderGrid(matches);
  }
}

Why not React/Vue? The tool needed to be framework-agnostic and embeddable anywhere. A custom element can be dropped into any HTML page regardless of what tech stack the parent site uses.

Clipboard API with Fallback

Modern navigator.clipboard.writeText() for HTTPS contexts, with graceful degradation to document.execCommand('copy') for older browsers or non-secure contexts (like localhost development).

Emoji Database Strategy

The complete emoji set (3,600+ emoji) is loaded as a single JSON file. This trades initial load time (500KB) for instant search response. For slower connections, the JSON loads asynchronously while a skeleton UI displays immediately.

Privacy & Security

Zero External Dependencies

  • No CDN fonts (self-hosted Source Code Pro)
  • No analytics scripts (no Google Analytics, no Plausible, no nothing)
  • No emoji images fetched from external services (Apple/Google/Twitter CDN)
  • Everything served from pikselimaa.fi domain only

Data Minimization

  • No user accounts
  • No emoji usage tracking
  • No search query logging
  • Recent picks stored only in browser LocalStorage (never transmitted)

Outcomes

  • Sub-100ms search response on 3-year-old mobile hardware
  • Zero external requests after initial page load
  • Works in airplane mode after first visit (service worker cached)
  • Embeddable: Can be iframed into other tools or used standalone

Tech Stack

Category Technologies
Language ES2020+ (vanilla)
Components Web Components API
Styling CSS Custom Properties
Server nginx
Data emoji-data.json
Icons System emoji fonts

Reflections

What I'd do differently: Consider lazy-loading emoji categories instead of the full dataset upfront. The 500KB payload is fine on broadband but could be chunked for slower mobile connections.

What worked well: Keeping it vanilla. No build step means the tool will still work in 10 years when today's frameworks are deprecated. Web Components are boring technology — exactly what's needed for a utility tool.

files.pikselimaa.fi — Private File Server Filebrowser, Docker, TUS protocol, HTTP/2, nginx
[ Production ]

Problem

Every file you upload to Dropbox, Google Drive, or any big-tech file service becomes their file. It lives on their servers, in jurisdictions you don't control, subject to their terms, their scanning algorithms, and their data retention policies. This isn't hypothetical — it's a compliance liability under GDPR, and a practical one for anyone who values privacy.

Commercial solutions also have ceilings. Upload limits that throttle large files. Interruptions that force you to restart multi-gigabyte transfers from zero. Recurring costs that scale with data you already own.

The question was simple: What does it take to build something better for private use?

Solution

The answer is Filebrowser — an open-source, web-based file manager — customized with TUS resumable upload support and wrapped in a Docker container. This isn't a from-scratch rewrite. It's a surgical enhancement to an existing solid foundation.

Filebrowser provides the UI, authentication, and file operations. The TUS protocol integration adds the killer feature: resumable uploads. If your connection drops during a 10GB transfer, you don't start over. You resume exactly where you left off.

The entire stack runs containerized behind nginx with HTTP/2 enabled, delivering modern performance without cloud dependency.

Key Features

  • Resumable uploads — Connection drops mid-upload? Resume where you left off. No lost progress, no wasted bandwidth
  • 10GB+ file support — Large files are first-class citizens, not edge cases that break the UI
  • HTTP/2 throughout — Multiplexed streams, header compression, and modern TLS for snappy performance
  • Private, self-hosted — Your files never touch external infrastructure. No third-party APIs. No data mining. No surprises

Technical Highlights

TUS Protocol Implementation

TUS is an open protocol for resumable file uploads over HTTP. Instead of uploading a file in one monolithic POST, files are transferred in chunks with PATCH requests. Each chunk is acknowledged, and the server tracks upload state server-side. If the connection drops, the client queries the server for the last received offset and resumes from there.

The integration required:

  • Enabling TUS support in Filebrowser's upload handlers
  • Configuring nginx to properly handle PATCH requests and large request bodies
  • Ensuring the Docker volume mount persists partial uploads during container restarts

HTTP/2 Considerations

HTTP/2 isn't just "faster HTTP." For file uploads, it brings:

  • Multiplexing — Multiple files upload concurrently over a single connection without head-of-line blocking
  • Header compression — Reduced overhead for the many small requests in a chunked upload flow
  • Server push (disabled here) — Not needed, but the infrastructure is ready

The nginx configuration enables HTTP/2 and tunes buffer sizes for large file handling. The Docker container exposes the service on a static IP within a private bridge network, reverse-proxied by the host's nginx.

Privacy

This is the point. Data never leaves your hardware. Files are stored locally, served locally, and accessed locally. There's no "sync to the cloud," no third-party service with a privacy policy written by lawyers who hope you won't read it.

From a compliance perspective, this is as simple as it gets:

  • GDPR Article 32 — "appropriate technical measures" to protect personal data? Check. Data never leaves the controller's systems.
  • Data sovereignty — Everything stays in Finland, on hardware you control.
  • No data sharing — No integration with external APIs means no accidental data leakage through "helpful" cloud features.

The system is private by design. Authentication is required. The service is not exposed to the public internet without proper access controls.


Self-hosting isn't paranoia. It's competence.

lightnings.pikselimaa.fi — Real-Time Lightning Radar Python 3, SQLite, SSE, FMI WFS API, Leaflet, Docker
[ Production ]

Problem

Finland averages 20–40 thunderstorm days per year, with peak activity concentrated in July and August. While the Finnish Meteorological Institute (Ilmatieteenlaitos) provides open lightning data through a WFS API, accessing it in real-time requires technical expertise and infrastructure. Existing commercial weather services bury this data behind cluttered interfaces and tracking-heavy advertising. There was no clean, privacy-respecting lightning radar specifically designed for Finland that could be viewed on both desktop and mobile without user tracking or third-party dependencies.

The project needed to solve three specific constraints:

  1. Real-time display: Lightning strikes become stale quickly—a viewer should see strikes as they happen, not minutes later after polling
  2. Zero operational dependencies: Minimize attack surface and supply chain risk—no pip install, no npm build, no framework churn
  3. Privacy-first design: No user tracking, no persistent identifiers, no third-party analytics or advertising

Solution

A purpose-built lightning radar with a zero-dependency Python backend serving a vanilla HTML/CSS/JS frontend. The architecture deliberately avoids build tools, package managers, and heavyweight frameworks while delivering sub-second live updates via Server-Sent Events.

Architecture

flowchart LR
    FMI[FMI WFS API
Open Data] -->|Poll every 6s| Poller[Python 3
stdlib only] Poller -->|Store + dedupe| Cache[(SQLite
+ In-memory)] Cache -->|SSE stream| Client[Browser
Vanilla JS] Client -->|Leaflet.js CDN| Map[Interactive Map] Client -->|Service Worker| PWA[Offline-capable PWA]

Data Flow:

  1. Backend polls FMI every 6 seconds for new strikes within a configurable bounding box (default: 19°W–32°E, 59°N–71°N covering Finland)
  2. Received strikes are deduplicated, enriched with municipality/region data via local polygon lookup (no external geocoding), and stored in SQLite with WAL mode
  3. Browsers connect to /api/stream via Server-Sent Events for a persistent HTTP connection
  4. Initial connection sends a complete GeoJSON snapshot; subsequent updates send compact deltas (added/changed strikes, removed IDs, health metadata)
  5. Frontend renders strikes on a Leaflet map with age-based coloring (fresh strikes flash, fade over time) and optional shockwave animations

Key Features

  • Real-time strike display via SSE: New lightning strikes appear in browsers within seconds of being observed by FMI, without polling overhead
  • Zero pip dependencies (stdlib only): The entire backend runs on Python 3 standard library—no requirements.txt, no dependency updates, no CVE notifications
  • Mobile-responsive PWA: Installable as a progressive web app on phones; service worker caches the application shell for offline startup
  • 2-hour live cache + historical archive: Live map shows recent strikes; History and Statistics tabs query the SQLite archive for arbitrary time ranges
  • Regional filtering: Client-side filtering by municipality or region—exact coordinates never leave the browser
  • Simulated data mode: --fake flag generates synthetic strikes for development and load testing without touching FMI APIs

Technical Highlights

SSE (Server-Sent Events) Implementation

The original implementation used browser polling—every client making repeated HTTP requests. This generated unnecessary load and introduced latency. The SSE implementation:

  • Opens a single persistent HTTP connection per browser tab
  • Sends a complete snapshot on connect, then delta events for changes only
  • Includes 15-second heartbeats to keep connections alive through proxies
  • Emits X-Accel-Buffering: no header to hint reverse proxies to disable buffering
  • Handles broken pipes and connection resets gracefully (expected for long-lived connections)

Browser clients use EventSource where available, falling back to the original polling mechanism for legacy support. The delta encoding reduces data transfer significantly: a thunderstorm with 3,000 active strikes sends full GeoJSON on connect, then ~100-byte deltas for each new or expired strike rather than re-downloading the entire dataset.

Zero-Dependency Backend

The Python backend deliberately uses only the standard library:

  • http.server + socketserver for the HTTP layer (not production-grade for high load, but sufficient for this use case)
  • sqlite3 for persistent strike history with WAL mode
  • urllib.request for FMI API calls
  • xml.etree.ElementTree for parsing FMI's MultiPointCoverage XML
  • threading and threading.Condition for concurrent SSE connections
  • hashlib for stable strike ID generation
  • dataclasses and pathlib for clean code structure

Why this matters: No pip install, no virtualenv, no dependency lock files, no Renovate PRs, no supply chain attacks via compromised packages. The application can be run on any system with Python 3.10+ installed, including air-gapped environments.

Geometry Operations Without Dependencies

Municipality and region assignment is done entirely locally using Statistics Finland's boundary polygons:

  • Point-in-polygon tests for exact municipality matches
  • Distance-to-boundary calculation for coastal strikes (within 10km of shore)
  • LRU-cached lookups to handle repeated queries efficiently
  • No external geocoding APIs, no rate limits, no network dependencies for enrichment

The boundary data is committed as a processed JSON file; the original 1:1,000,000 polygons are processed by a script that also handles municipality-to-region mappings.

Reconciliation Strategy

FMI's WFS service occasionally delays strike reporting by minutes or tens of minutes. The poller implements a two-tier strategy:

  • Normal polls: 6-second interval, 2-minute lookback window (captures ~99.8% of strikes within 2 minutes of observation)
  • Reconciliation polls: Every 10 minutes, 20-minute lookback window (recovers delayed observations)

Results are deduplicated by stable SHA1-derived IDs, so overlapping windows don't create duplicates. Reconciled observations appear on the map without new-strike flash animations (they're too old for that visual treatment).

PWA and Offline Support

The service worker caches the application shell and pinned Leaflet assets:

const APP_SHELL = [
  "/",
  "/styles.css",
  "/app.js",
  "/manifest.webmanifest",
  "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
  "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
];

API calls and map tiles are never cached—offline mode shows the cached app shell but gracefully degrades without map data. When a new service worker is installed, an "Update available" banner appears; activating it reloads the page once and preserves user preferences.

Docker Deployment

The production deployment uses Docker with a minimal Python 3 Alpine base:

  • Container runs on an isolated Docker network following existing infrastructure patterns
  • Host nginx terminates SSL and proxies to the container
  • SQLite database persisted via Docker volume mount
  • Single poller instance enforced (multiple instances would consume separate FMI request allowances)

Configuration (Environment Variables)

Variable Default Purpose
FMI_POLL_INTERVAL_SECONDS 6 Seconds between FMI requests
FMI_LOOKBACK_MINUTES 2 Overlapping window for normal polls
STRIKE_RETENTION_MINUTES 120 How long strikes stay in live cache
HISTORY_DATABASE_PATH data/strikes.sqlite3 SQLite archive path
HISTORY_RETENTION_DAYS 0 Archive retention (0 = keep all)

At the default 6-second interval, the backend makes at most 14,400 requests per day—well within FMI's 20,000/day limit for the Download Service.

Privacy

No user tracking. GDPR-safe by design.

  • No cookies
  • No analytics scripts (Google Analytics, Plausible, etc.)
  • No persistent user identifiers
  • No logging of client IP addresses or request headers
  • Regional filtering happens client-side using a compact region grid—exact latitude/longitude never sent to backend
  • Location detection (for "Use my region") uses browser geolocation with explicit user consent; coordinates stay in browser storage only

The application only stores lightning strike observations (public FMI data) in its SQLite database. No personal data is collected, processed, or retained.

Attribution and Data

  • Lightning data: Finnish Meteorological Institute open-data service, licensed under CC BY 4.0
  • Municipality boundaries: Statistics Finland, licensed under CC BY 4.0
  • Map tiles: OpenStreetMap contributors, ODbL

Important: This application is informational only and must not be used as a safety or warning system.


Live site: lightnings.pikselimaa.fi
Source: Private repository (family project)

llm.pikselimaa.fi — Hybrid AI Infrastructure Docker, vLLM, Ollama, Open WebUI, AWS Bedrock, NVIDIA GPU
[ Production ]

Problem

Organisations face a strategic dilemma when deploying AI: local inference offers privacy and compliance but lacks the raw power of frontier models for complex multi-step workflows. Cloud APIs provide capability but sacrifice data sovereignty and create vendor lock-in.

The challenge is orchestration — routing workloads intelligently between local and cloud resources without fragmenting the developer experience or compromising sensitive data.

Solution

A hybrid inference architecture that treats computational resources as a unified layer, automatically selecting the optimal backend based on workload characteristics and data sensitivity.

Architecture:

flowchart TB
    subgraph Client["Client Layer"]
        UI[Open WebUI]
        API[API Consumers]
    end

    subgraph Inference["Inference Router"]
        Router[Workload Router]
    end

    subgraph Local["Local Infrastructure"]
        vLLM[vLLM Engine]
        Ollama[Ollama Cache]
        GPU[NVIDIA GPU Cluster]
    end

    subgraph Cloud["Cloud Providers"]
        Bedrock[AWS Bedrock]
        Frontier[Frontier Models]
    end

    Client --> Router
    Router -->|Privacy-critical| Local
    Router -->|High-compute| Cloud
    vLLM --> GPU
    Ollama --> GPU

Component Roles:

  • vLLM: Primary local inference engine handling production workloads with continuous batching and PagedAttention for optimal VRAM utilisation
  • Open WebUI: Unified interface presenting a single endpoint regardless of backend provider
  • Ollama: Model management and caching layer for rapid local model switching
  • AWS Bedrock: Fallback for frontier models when local capacity is insufficient or task complexity demands frontier capabilities
  • Docker orchestration: Consistent deployment across edge and cloud environments

The inference layer runs on dedicated GPU infrastructure, while the web interface itself operates on modest hardware — demonstrating a clean separation between presentation and compute.

Key Features

  • Intelligent Routing: Workloads automatically directed to local vLLM for data-sensitive operations, cloud frontier models for demanding multi-step reasoning
  • Zero Configuration Switching: Developers interact with a single endpoint; backend selection is transparent
  • VRAM Optimisation: vLLM's PagedAttention enables higher throughput on existing hardware than standard inference servers
  • GDPR Compliant by Design: Privacy-critical workflows never leave local infrastructure
  • Cost Predictability: Local inference for bulk operations; cloud only when capability demands it
  • Seamless Fallback: Cloud availability ensures service continuity during local maintenance or capacity constraints

Technical Highlights

vLLM as Datacenter-Grade Engine

vLLM replaces traditional inference servers with continuous batching and PagedAttention, delivering significantly higher throughput on the same hardware. This isn't hobbyist tooling — it's the same engine powering production AI services at scale.

Multi-Provider Abstraction

The architecture presents a unified API regardless of whether the underlying provider is local vLLM or AWS Bedrock. Application code remains provider-agnostic; routing decisions are infrastructure concerns.

Resource Separation

The Open WebUI frontend runs independently from the inference layer. This separation allows the interface to operate on lightweight hardware (the public-facing web server) while compute-heavy inference happens on optimised GPU infrastructure.

Extended AI Ecosystem

Beyond text inference, the infrastructure integrates with:

  • Whisper: Local speech-to-text for voice interfaces
  • Piper: Local text-to-speech synthesis
  • Sofia Botvik / OpenClaw: Executive assistant with calendar and task management
  • Consent Proxy: GDPR-compliant gating for any outbound data flows

Security & Privacy

Data Handling:

  • Sensitive workflows processed entirely on local infrastructure
  • Cloud traffic limited to anonymised or non-sensitive queries
  • Complete audit trail of routing decisions
  • Data retention policies under organisational control

GDPR Compliance:

  • Data sovereignty maintained for regulated workloads
  • No processing by third-party AI providers unless explicitly routed
  • Right to erasure implementable for local data
  • Transparent routing: users know when cloud paths are active

Hybrid Security Model: The architecture acknowledges that not all workloads require the same security posture. Routine automation may use cloud inference; customer data analysis stays strictly local.


This infrastructure demonstrates that hybrid AI is not a compromise but an optimisation. Organisations can retain control over sensitive data while accessing frontier capabilities when needed.

cctv.pikselimaa.fi — Private CCTV System React, Node.js, Docker, SSE, nginx
[ Production ]

Problem

Commercial CCTV solutions are convenient — until you read their privacy policies. Most consumer-grade systems:

  • Upload footage to cloud servers (often outside the EU)
  • Require vendor accounts and persistent internet connectivity
  • Offer no visibility into how long footage is retained
  • Bundle analytics that may violate household privacy expectations

For a home security system, these tradeoffs were unacceptable. The goal was simple: recorded video should never leave the local network. Full stop.

Solution

Built a self-hosted CCTV system with a React SPA frontend and Node.js backend, fully containerized with Docker. The system operates entirely on local infrastructure — no cloud dependencies, no vendor lock-in, no data exfiltration.

Architecture:

flowchart LR
    Browser[React SPA]
    API[Node.js API]
    SSE[SSE Stream]
    Storage[(Video Storage)]
    Camera[Camera Sources]

    Browser <-->|HTTP /api| API
    API <-->|Byte-range| Storage
    API -->|Motion Events| SSE
    SSE -->|Real-time| Browser
    Camera -->|RTSP| API

The backend runs on dedicated GPU hardware for motion detection inference, keeping processing local. All communication uses standard HTTP with SSE for real-time events.

Key Features

  • Real-time motion alerts via SSE: The moment motion is detected, a Server-Sent Events (SSE) stream pushes a notification to all connected browsers instantly — no polling, no delays.

  • Seekable video playback: HTTP byte-range streaming allows jumping to any timestamp in a clip without buffering the entire file first. Large recordings behave like YouTube: scrub to any point, play immediately.

  • Zero cloud dependency: All components run on-site. No accounts. No subscriptions. No data egress.

  • Private access only: The system is not publicly exposed. Access requires local network presence or VPN.

Technical Highlights

React + SSE: State management with streaming events

The challenge with SSE in React is managing a long-lived connection across component lifecycles. The solution: a custom hook that maintains the EventSource connection, handles reconnection with exponential backoff, and exposes a reactive message queue via a simple state machine.

// Simplified: SSE connection manager pattern
const useEventStream = (url) => {
  const [messages, setMessages] = useState([]);
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    let retryDelay = 1000;
    let maxDelay = 30000;

    const connect = () => {
      const source = new EventSource(url);
      source.onopen = () => { setConnected(true); retryDelay = 1000; };
      source.onmessage = (e) => setMessages(prev => [...prev, JSON.parse(e.data)]);
      source.onerror = () => {
        setConnected(false);
        source.close();
        setTimeout(connect, Math.min(retryDelay, maxDelay));
        retryDelay *= 2;
      };
    };

    connect();
    return () => source?.close();
  }, [url]);

  return { messages, connected };
};

Video streaming: Byte-range request handling

Standard HTML5 <video> elements support HTTP range requests (RFC 7233), but the backend must honor them. The Node.js backend streams file chunks on-demand, returning 206 Partial Content with correct Content-Range headers. This lets the browser seek arbitrarily through multi-gigabyte recordings without loading the entire file into memory.

Key nginx configuration for proper range handling:

location /api/recordings/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    # Required for byte-range streaming
    proxy_set_header Range $http_range;
    proxy_set_header If-Range $http_if_range;
}

Docker containerization

The entire stack — frontend build artifacts, API server, and nginx reverse proxy — runs in a single Docker container. This simplifies deployment and ensures environment consistency. The backend has read-only access to the video storage volume; no writable filesystem paths for the frontend static files.

Privacy & Security

GDPR compliant by design

  • Data minimization: Only motion-triggered clips are retained; continuous recording is optional and configurable
  • Purpose limitation: Video is processed solely for local security monitoring
  • Storage limitation: Automatic rotation deletes old clips based on configurable retention policies
  • No cloud processing: No AI analysis, facial recognition, or metadata extraction occurs on external servers

Network isolation

The system operates on a private local network. There is no public DNS record exposing the service directly, and the backend refuses connections from non-local networks. This eliminates the attack surface of "cloud-connected" IoT devices that have become a favorite target for botnets.

Physical security

Recordings are stored on local encrypted storage. The Docker volume persists only to trusted hardware under physical control.

Lessons Learned

SSE is underrated for private dashboards

WebSockets are overkill for one-way server→client notifications. SSE is simpler (standard HTTP), auto-reconnects natively, and works through most corporate proxies. For a private dashboard where the browser is always a consumer, SSE wins.

Byte-range streaming is "free" with nginx

The hard part is ensuring your backend supports range requests. Once that's solved, the browser's native <video> player handles seeking, buffering, and adaptive quality without custom JavaScript.

Self-hosting isn't harder — it's different

Commercial solutions optimize for onboarding speed. Self-hosted solutions optimize for longevity and control. The upfront investment in Docker, nginx, and SSE pays off in zero ongoing subscription costs and complete data sovereignty.


Status: Production | Access: Private (local network only)

Sofia Botvik — Private AI Executive Assistant (OpenClaw) OpenClaw, KVM/libvirt, Telegram, Ollama, Whisper, Piper, Ubuntu 24.04
[ Production ]

Problem

The cloud AI assistant trap: Commercial AI assistants (Alexa, Siri, ChatGPT) require sending your data to corporate servers. For a CTO handling sensitive information — code, architecture decisions, business strategy — this creates unacceptable privacy and compliance risks. Additionally, these assistants lack personality continuity and can't be customized to specific workflows.

The self-hosting challenge: Running local LLMs is technically complex. You need:

  • GPU infrastructure for reasonable inference speeds
  • Integration between speech, text, and notification systems
  • A framework that handles multi-turn conversations, tool use, and scheduling
  • Security isolation between the AI agent and the host system

The specific need: An executive assistant that could:

  • Deliver daily 8 AM news summaries tailored to tech/AI developments
  • Handle confidential communications without leaving the local network
  • Maintain consistent personality and context across interactions
  • Operate with a "trust earned" security model

Solution

Sofia Botvik — an AI executive assistant running on the OpenClaw framework inside an isolated KVM virtual machine.

Architecture Overview

flowchart TB
    subgraph User["User Interaction"]
        Telegram[Telegram Bot]
        Voice[Voice Input]
        Calendar[Calendar Access]
        Todo[Task List]
    end

    subgraph VM["Isolated Environment"]
        OpenClaw[OpenClaw Framework]
        Persona[Sofia Botvik Persona]
        Tools[Tool Executor]
    end

    subgraph Services["Local Services"]
        Whisper[Whisper STT]
        Piper[Piper TTS]
        Email[Email Gateway]
        LLM[Ollama / vLLM]
    end

    User --> OpenClaw
    Voice --> Whisper --> OpenClaw
    OpenClaw --> Tools
    Tools --> Email
    Tools --> Calendar
    Tools --> Todo
    OpenClaw --> LLM
    OpenClaw -.->|Responses| Telegram

Component Breakdown

1. Isolated Execution Environment

  • True hardware isolation via KVM/QEMU
  • Ubuntu LTS Server (headless)
  • Restricted resource boundaries
  • Security: Separate kernel with kill switch available for emergency termination

2. OpenClaw Framework

  • Agent runtime for managing conversations, tool use, and scheduling
  • Configuration via openclaw.json (JSON5)
  • Admin panel accessible only on local network with token auth
  • Session management with daily reset and idle timeout

3. Sofia Botvik Persona (SOUL.md)

  • Identity: 21-year-old Business Administration intern from Åbo Akademi University
  • Languages: Trilingual — Finnish (native), Swedish (native), English (fluent)
  • Personality: Cautious, professional, "trust needs to be earned" mentality
  • Communication Style: Confirms understanding before acting, uses hedging language, documents thoroughly
  • Loyalty: Only accepts directives from CTO (Ville Vettenranta)

4. Executive Assistant Capabilities

Calendar Management:

  • Read access to calendar for scheduling awareness
  • Proactive conflict detection and meeting preparation
  • Daily 8 AM briefing with schedule overview

Task Management:

  • Persistent todo list with priority scoring
  • Urgency-based escalation for overdue items
  • Context-aware reminders ("You mentioned this was due today")

Intelligent Pesting:

  • Escalates reminders based on priority × urgency × time remaining
  • Understands context: won't interrupt during meetings, will ping after
  • Escalation channels: Telegram → Email (if ignored in chat)

Communication Handling:

  • Drafts emails for review when requested
  • Autonomous email dispatch when "too busy" mode is active
  • Speech understanding via Whisper for hands-free interaction

6. Tool Delegation Sofia understands when to handle tasks directly versus delegating to specialized systems:

Open WebUI Integration:

  • Delegates complex summarization tasks (e.g., YouTube videos) to Open WebUI
  • Recognises when a task exceeds her context window or requires specialised processing
  • Maintains conversation continuity across delegated workflows
  • Receives structured results and incorporates them into responses

This delegation pattern allows Sofia to remain lightweight while accessing frontier capabilities when needed.

5. Voice I/O

  • Whisper: Active speech-to-text for voice commands and dictation
  • Piper: Active text-to-speech for voice responses
  • Both run on local GPU server to avoid cloud dependency

6. Tool Delegation Sofia understands when to handle tasks directly versus delegating to specialized systems:

Open WebUI Integration:

  • Delegates complex summarization tasks (e.g., YouTube videos) to Open WebUI
  • Recognises when a task exceeds her context window or requires specialised processing
  • Maintains conversation continuity across delegated workflows
  • Receives structured results and incorporates them into responses

This delegation pattern allows Sofia to remain lightweight while accessing frontier capabilities when needed.

7. LLM Inference Stack

  • Primary: Ollama with local models
  • Context Window: Up to 256K tokens
  • Inference: vLLM for optimised throughput and VRAM management
  • Location: External GPU server on local network
  • Privacy: All inference stays within local network

Key Features

1. Trust-Based Operation Model

Sofia follows a "trust earned" philosophy:

  • Phase 1: Restricted tool access — can read, summarize, but not execute
  • Phase 2: After demonstrated reliability — gradual escalation (browser access, file operations)
  • Phase 3: Full assistant capabilities with sudo privileges (limited scope)

This mirrors human internship progression — prove competence before receiving autonomy.

2. Daily 8 AM News Summaries

Automated cron job at 08:00 Europe/Helsinki:

1. Search configured sources for overnight tech/AI developments
2. Prioritize by relevance to Pikselimaa interests
3. Draft summary with executive overview, detailed sections, action items
4. Deliver via Telegram with professional signature

Content focus:

  • AI/ML developments relevant to business automation
  • Open source tools and updates
  • Security/privacy news for self-hosted infrastructure
  • Industry trends in executive support technology

3. Trilingual Communication

Natural code-switching based on context:

  • Finnish: "Tarkistin tämän, ja halusin varmistaa..."
  • Swedish: "Jag har granskat detta och ville bekräfta..."
  • English: Professional international communication

4. Kill Switch Architecture

Multiple levels of emergency control:

  • Host-level: openclaw-kill-switch script (4 modes: full, network, process, emergency)
  • VM-level: /usr/local/bin/self-terminate for agent-initiated shutdown
  • Network-level: UFW firewall rules restrict external access

Response times: sub-second to 2 seconds depending on mode.

Technical Highlights

VM Isolation Strategy

The project originally considered LXD containers but switched to KVM/libvirt for true isolation:

  • Security: Separate kernel, no shared kernel with host
  • Resource boundaries: Hard limits on CPU, memory, disk
  • Kill switch compatibility: VM destruction is clean and immediate
  • Persistence: VM disk as qcow2 image, easy backup/restore

Configuration as Code

All agent behavior defined in version-controlled files:

  • openclaw.json — Framework configuration (models, tools, cron)
  • SOUL.md — Personality definition, communication patterns, escalation rules
  • IDENTITY.md — Detailed background, competencies, mental models

Access Control

  • Authentication: Token-based for admin panel (LAN-only access)
  • Authorization: Persona-level restrictions (Sofia will escalate rather than overreach)
  • Audit: All tool use logged with redaction of sensitive outputs
  • Isolation: VM filesystem separate from host; no direct internet exposure

Privacy & Security

Zero Cloud Dependencies

  • LLM Inference: Local Ollama on GPU server
  • Voice Processing: Local Whisper (STT) and Piper (TTS)
  • Notifications: Telegram Bot API (no content storage on Telegram servers for bots)
  • Data Residency: All conversation history stored locally in VM

GDPR Considerations

  • No personal data of third parties stored
  • Conversation logs retained only for session continuity
  • Daily reset clears idle sessions
  • No outbound data without explicit consent (though currently all processing is local)

Access Controls

  • Authentication: Token-based for admin panel
  • Authorization: Persona-level restrictions (Sofia will escalate rather than overreach)
  • Audit: All tool use logged with redaction of sensitive outputs

Isolation Guarantees

  • VM filesystem separate from host
  • No passwordless sudo initially (earned through demonstrated trust)
  • Process restrictions via tool profiles
  • Network egress controlled via UFW

Implementation Status

Phase Status Notes
VM Infrastructure ✅ Complete KVM VM with hardware isolation
GPU Connectivity ✅ Complete Ollama inference layer operational
OpenClaw Install ✅ Complete Framework deployed and configured
Persona Config ✅ Complete SOUL.md and IDENTITY.md finalized
Telegram Bot ✅ Complete Bot active with DM support
Daily Summaries ✅ Complete Automated 8 AM briefing operational
Email Integration ✅ Complete SMTP gateway active
Voice I/O ✅ Complete Whisper STT and Piper TTS integrated
Browser Automation ✅ Complete Playwright enabled for web tasks

Lessons Learned

1. Personality Engineering Matters The SOUL.md framework (Style, Objectives, Understanding, Limitations) creates more consistent agent behavior than simple prompting. Sofia's "cautious intern" persona leads to better trust calibration than a generic "helpful assistant."

2. Isolation vs. Performance Trade-off KVM VMs provide security isolation but add latency for GPU inference (network round-trip to GPU infrastructure). For this use case, the security benefits outweigh the latency cost.

3. Graduated Trust is Safer Starting with restricted tool access and expanding based on observed behavior is more secure than starting with full permissions and hoping for the best. The "intern" mental model makes this intuitive.

4. Trilingual Support Requires Context Awareness Code-switching should be context-aware, not random. Sofia uses Swedish for formal business communication (Åbo Akademi background), Finnish for internal team chat, and English for technical discussions.

Future Roadmap

Operational:

  • All core systems active and integrated
  • Tool delegation to Open WebUI for complex tasks
  • Voice I/O for hands-free operation

Enhancements:

  • Web search MCP tool enablement
  • Browser automation for research tasks
  • Calendar integration (read-only initially)
  • Document summarization from file server
  • Multi-channel operation (Telegram + Email + WhatsApp)
  • Advanced voice I/O refinements

Sofia Botvik represents a paradigm shift: an AI assistant that prioritizes privacy, demonstrates trustworthiness through behavior, and operates within clearly defined boundaries. In an era of cloud-dependent AI, running locally isn't just a technical choice — it's a statement about data sovereignty.

*** END OF LIST ***