<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.lukasrieder.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.lukasrieder.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-08-21T17:03:49+00:00</updated><id>https://www.lukasrieder.com/feed.xml</id><title type="html">Lukas Rieder</title><subtitle>Notes on software, systems, and building small things well.</subtitle><author><name>Lukas Rieder</name></author><entry xml:lang="en"><title type="html">How much cloud fits into one server?</title><link href="https://www.lukasrieder.com/blog/how-much-cloud-fits-into-one-server/" rel="alternate" type="text/html" title="How much cloud fits into one server?" /><published>2025-05-11T07:00:00+00:00</published><updated>2025-05-11T07:00:00+00:00</updated><id>https://www.lukasrieder.com/blog/how-much-cloud-fits-into-one-server</id><content type="html" xml:base="https://www.lukasrieder.com/blog/how-much-cloud-fits-into-one-server/"><![CDATA[<p>In May 2025, I had to deploy Plantronic into an on-premise environment I did not know.</p>

<p>One server. Corporate networking. Controlled egress. An application, a database, ingress, migrations, backups, monitoring, and a deployment path that had to work from staging through production.</p>

<p>This could have become a bespoke server project: install packages, write service units, copy configuration, arrange startup order, add a reverse proxy, improvise database backups, and document which commands an operator must remember.</p>

<p>Instead, years of infrastructure work collapsed into one small k3s installation and a set of Terraform compositions.</p>

<p>That result is why I pride myself on <strong>running the things I build</strong>. I do not consider an application complete when it works on my laptop or when a container image exists. I want to understand how it starts, how it receives configuration, how it migrates data, how it recovers, and how another person can reproduce the deployment without borrowing my memory.</p>

<h2 id="the-deployment-was-unique-most-pieces-were-not">The deployment was unique; most pieces were not</h2>

<p>I did not arrive with a ready-made “Plantronic platform.” I arrived with a portfolio of smaller, declarative parts developed across earlier work.</p>

<p>My public <a href="https://gitlab.com/parlant-co/terraform-modules">Terraform Modules</a> repository contains modules for web workloads, PostgreSQL, GitLab Agent, monitoring, ingress, cloud resources, secrets, jobs, and related infrastructure. One day before the Plantronic deployment milestone, I committed the initial reusable Kubernetes modules that the project composed.</p>

<p>Plantronic selected and connected those pieces:</p>

<ul>
  <li>a containerized Phoenix application;</li>
  <li>an explicit migration step;</li>
  <li>a Kubernetes Deployment and Service;</li>
  <li>ingress and TLS integration;</li>
  <li>PostgreSQL with persistent storage;</li>
  <li>scheduled logical backups and retention;</li>
  <li>GitLab Agent for controlled deployment access;</li>
  <li>monitoring and adjacent operational services.</li>
</ul>

<p>None of these modules was remarkable alone. Their value came from combining them without removing their boundaries.</p>

<p>That is the compounding effect I care about. Knowledge does not compound merely because I remember how I solved a problem before. It compounds when the old solution has a stable interface, remains readable, and can participate in a new system without being copied into it.</p>

<h2 id="why-one-server-still-became-a-kubernetes-cluster">Why one server still became a Kubernetes cluster</h2>

<p>A single-node Kubernetes installation sounds contradictory to some people. Kubernetes is associated with fleets, autoscaling, and highly available control planes. Plantronic had one machine. Why place a cluster API between the application and Systemd?</p>

<p>Because node count was not the problem I wanted Kubernetes to solve.</p>

<p>I wanted a consistent application runtime and a declarative workload model. I wanted an image, configuration, secrets, migrations, services, jobs, ingress, health behavior, and rollout state to be described with the same primitives in staging and production. I wanted GitLab to interact with the deployment without exposing a raw cluster endpoint through the client’s firewall.</p>

<p><a href="https://docs.k3s.io/architecture">k3s</a> packages the Kubernetes control plane and required runtime components for smaller environments. On one server it remains one failure domain, but it gives me the Kubernetes API and controllers without pretending that I am operating a large managed cluster.</p>

<p>That distinction matters:</p>

<ul>
  <li><strong>supervision:</strong> yes, failed workloads can restart and declared state can be reconciled;</li>
  <li><strong>repeatability:</strong> yes, the same resources can be created again;</li>
  <li><strong>host-level high availability:</strong> no, one broken server is still one broken server;</li>
  <li><strong>durability:</strong> only as good as storage, backups, restore procedures, and host recovery.</li>
</ul>

<p>I chose single-node k3s for operational consistency, not for an imaginary availability claim.</p>

<h2 id="systemd-is-not-the-loser">Systemd is not the loser</h2>

<p>The strongest counterproposal is good: build an Elixir release, run it under Systemd, run PostgreSQL as a host service, and keep the deployment small.</p>

<p><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html">Systemd is an excellent process supervisor</a>. It has restart policies, dependency ordering, timers, socket activation, credentials, resource controls, and <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html">extensive execution sandboxing</a>. <a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html">Podman Quadlet</a> offers an appealing middle ground by declaring containers as Systemd-managed units.</p>

<p><a href="https://docs.docker.com/compose/how-tos/production/">Docker Compose in production</a> is another reasonable middle path when the stack is co-located and its lifecycle remains small. It preserves container images and a multi-service declaration without adding a Kubernetes control plane. I prefer admitting when that is enough instead of turning Kubernetes experience into a requirement.</p>

<p>For one application process and one host, I would seriously consider one of those designs. Kubernetes would add an API server, certificates, networking, storage abstractions, controllers, and upgrade work without automatically producing value.</p>

<p>Plantronic crossed my threshold because it was not one process:</p>

<ul>
  <li>application release;</li>
  <li>migration lifecycle;</li>
  <li>database and persistent storage;</li>
  <li>ingress;</li>
  <li>recurring backup jobs;</li>
  <li>secret and runtime configuration;</li>
  <li>monitoring;</li>
  <li>staging and production workloads;</li>
  <li>remote deployment through a constrained network.</li>
</ul>

<p>I could model every one of those with Systemd units, scripts, container commands, timers, and proxy configuration. I would then own a Plantronic-specific orchestration system assembled from excellent host primitives.</p>

<p>I already owned the equivalent Kubernetes abstractions as reusable Terraform modules. In my situation, direct Systemd would have been the bespoke choice.</p>

<p>There is another useful truth hiding here: Systemd still supervises k3s. I did not replace it. I moved application orchestration behind a boundary that could remain consistent across hosts and environments. Systemd owns the node service; Kubernetes owns workloads; OTP owns processes inside the Elixir release.</p>

<p>I prefer these nested responsibilities over asking one layer to understand every failure.</p>

<h2 id="containerizing-elixir-without-fighting-otp">Containerizing Elixir without fighting OTP</h2>

<p>Elixir already has supervision trees. Kubernetes does not improve a GenServer restart by killing the entire pod, and I do not want it trying.</p>

<p>I divide the responsibilities differently:</p>

<ul>
  <li>OTP supervises application processes and internal dependencies;</li>
  <li>the Elixir release defines the runnable application artifact;</li>
  <li>the container fixes the OS-level runtime and native dependencies;</li>
  <li>Kubernetes supervises the container and its relationship to services;</li>
  <li>k3s itself remains a Systemd-supervised node service.</li>
</ul>

<p>Phoenix documents deployment through <a href="https://hexdocs.pm/phoenix/releases.html">OTP releases</a>. A controlled build can produce the release, a smaller runtime image can contain it, and runtime configuration can arrive when the container starts. The same image moves through environments; the release tag and configuration identify what actually runs.</p>

<p>For Plantronic, containerization supplied valuable guarantees in an unfamiliar host:</p>

<ul>
  <li>exact Erlang and Elixir runtime expectations;</li>
  <li>known native libraries;</li>
  <li>explicit listening port and process command;</li>
  <li>reproducible startup behavior;</li>
  <li>one image promoted rather than a host mutated in place.</li>
</ul>

<p>Database migrations remained an explicit deployment step. I prefer that to hiding schema changes inside normal application startup. The deployment can order migration before the new workload, report failure, and avoid having every replica race to alter the database.</p>

<p>Containerization did not make the BEAM reliable. OTP already did much of that work. It made the boundary around the BEAM release portable.</p>

<h2 id="twelve-factor-applied-rather-than-recited">Twelve-Factor, applied rather than recited</h2>

<p>The <a href="https://12factor.net/">Twelve-Factor App</a> is sometimes invoked as a cloud slogan. I find it more useful as a set of pressure tests.</p>

<h3 id="config">Config</h3>

<p>Environment-specific values should not be compiled into the release. Kubernetes configuration and secrets let the same image receive the database address, runtime identity, and deployment-specific values at startup.</p>

<h3 id="backing-services">Backing services</h3>

<p>PostgreSQL is attached infrastructure, not a hidden directory inside the application container. That separation makes its persistence, backup, monitoring, and recovery responsibilities explicit.</p>

<h3 id="build-release-run">Build, release, run</h3>

<p>The image is the build artifact. A tagged image plus runtime configuration forms a release. Kubernetes runs that release. Terraform describes the desired resources around it.</p>

<h3 id="processes-and-disposability">Processes and disposability</h3>

<p>The Phoenix application should start predictably, handle termination, and recover without depending on writable container-local state. Kubernetes can replace the process because durable state lives elsewhere.</p>

<h3 id="devprod-parity">Dev/prod parity</h3>

<p>I do not need every developer laptop to run a production cluster. I do want staging and production to share image, configuration, migration, service, and job concepts. That parity catches deployment mistakes before production without pretending environments are identical.</p>

<p>None of these factors requires Kubernetes. That is important. Kubernetes was useful because my existing modules already expressed these properties coherently.</p>

<h2 id="terraform-all-the-way-up-to-the-application">Terraform all the way up to the application</h2>

<p>I want infrastructure changes to look like code changes: proposed, reviewed, planned, applied, and recorded.</p>

<p>The Plantronic composition referenced reusable modules rather than copying their resources. The application module owned common deployment behavior. The PostgreSQL module owned the database chart and persistence interface. The GitLab Agent module owned its Helm release. Project-level Terraform supplied the choices that were genuinely specific to Plantronic.</p>

<p>This creates two useful levels of change:</p>

<ol>
  <li>improve a reusable module when several applications need the same capability;</li>
  <li>change the Plantronic composition when the requirement belongs only to Plantronic.</li>
</ol>

<p>That boundary prevents two failures I see often. A giant universal platform makes every application wait for abstractions it does not need. Fully bespoke Terraform makes every project rediscover secrets, rollouts, services, and jobs.</p>

<p>I prefer small modules with boring inputs. Composition is where client context enters.</p>

<p>The initial server bootstrap still established the substrate: operating system preparation, k3s, certificates, registry access, and network integration. I do not want to pretend a few documented host steps are already Infrastructure as Code. My direction is to push stable host knowledge into declarative automation while keeping credentials out of code and state exposure under control.</p>

<p>Above that substrate, application infrastructure belonged in Terraform. No hand-maintained Deployment YAML, no deployment recipe that existed only in shell history.</p>

<h2 id="gitlab-agent-across-a-controlled-boundary">GitLab Agent across a controlled boundary</h2>

<p>Traditional CI deployment often assumes that a runner can reach the Kubernetes API directly. That is uncomfortable in an on-premise network and frequently incompatible with firewall policy.</p>

<p>The <a href="https://docs.gitlab.com/user/clusters/agent/">GitLab Agent for Kubernetes</a> changes the direction. The cluster-side agent opens a bidirectional channel to GitLab’s agent server, KAS. GitLab documents this as a way to communicate with clusters behind firewall or NAT.</p>

<p>That fit Plantronic’s egress-controlled environment. I could define a narrow outbound path instead of publishing the cluster API for inbound CI traffic. The same Terraform composition installed the Helm release and connected deployment automation to the cluster.</p>

<p>I use “air-gapped” carefully. A truly air-gapped network cannot talk to GitLab.com; no agent changes that fact. It needs mirrored images and artifacts, a reachable self-managed control plane, or a controlled transfer process. <a href="https://docs.k3s.io/installation/airgap">k3s documents air-gap installation</a>, which makes the substrate adaptable, but egress-controlled and air-gapped are different operating modes.</p>

<p>I value the architecture because it crosses organizational boundaries cleanly. Client networking can remain restrictive. Deployment can remain automated. The interface between both concerns is explicit.</p>

<h2 id="a-practical-guide-to-deciding">A practical guide to deciding</h2>

<p>I would not recommend single-node k3s merely because a project uses containers. I would ask these questions.</p>

<h3 id="choose-direct-systemd-or-quadlet-when">Choose direct Systemd or Quadlet when</h3>

<ul>
  <li>there is one host and only a few processes;</li>
  <li>host-specific integration is desirable;</li>
  <li>the team knows Systemd better than Kubernetes;</li>
  <li>staging/production Kubernetes parity has little value;</li>
  <li>container networking, jobs, secrets, and ingress remain simple;</li>
  <li>no reusable Kubernetes platform already exists.</li>
</ul>

<h3 id="consider-single-node-k3s-when">Consider single-node k3s when</h3>

<ul>
  <li>the application includes several independently managed workloads;</li>
  <li>images and Kubernetes resources already exist elsewhere;</li>
  <li>jobs, migrations, services, ingress, secrets, and health behavior need one declarative model;</li>
  <li>controlled deployment access benefits from an in-cluster agent;</li>
  <li>staging and production should share orchestration primitives;</li>
  <li>the team can operate and upgrade the cluster;</li>
  <li>one host failure domain is acceptable and honestly documented.</li>
</ul>

<h3 id="in-either-case">In either case</h3>

<ul>
  <li>make backups and test restores;</li>
  <li>keep credentials out of repositories;</li>
  <li>pin and plan runtime upgrades;</li>
  <li>document substrate recovery;</li>
  <li>monitor the host, not only the application;</li>
  <li>define who owns the deployment after handover.</li>
</ul>

<p>Technology choice cannot compensate for missing operational ownership.</p>

<h2 id="what-i-bring-when-i-bring-infrastructure">What I bring when I bring infrastructure</h2>

<p>The Plantronic deployment did not prove that Kubernetes belongs on every server. It proved something more useful to me: a broad portfolio of composable infrastructure can make an unfamiliar boundary feel familiar without erasing what is different about it.</p>

<p>I brought lessons from cloud orchestration, container builds, Kubernetes resources, Terraform modules, Elixir releases, database operations, and constrained networks. I did not combine them into a private platform that only I could operate. I assembled public, declarative modules into a client-specific composition from staging to production.</p>

<p>That is the compounding effect I want from experience. Every project should leave behind more than memories and copied snippets. It should sharpen a module, an interface, a runbook, or a decision rule that improves the next project.</p>

<p>I pride myself on running what I build because deployment is where architectural claims meet reality. Configuration must arrive. State must survive. Processes must restart. Networks must permit exactly enough. Another person must be able to see what exists.</p>

<p>One server was enough to make all of those concerns concrete. Years of cloud work were what made one server feel small.</p>]]></content><author><name>Lukas Rieder</name></author><category term="plantronic" /><category term="infrastructure-as-code" /><category term="terraform" /><category term="kubernetes" /><category term="elixir" /><summary type="html"><![CDATA[An unfamiliar on-premise machine looked like a constraint. It became a test of whether years of infrastructure work could collapse into one repeatable deployment.]]></summary></entry><entry xml:lang="en"><title type="html">Finicky - scriptable macOS URL handler</title><link href="https://www.lukasrieder.com/blog/finicky-scriptable-macos-url-handler/" rel="alternate" type="text/html" title="Finicky - scriptable macOS URL handler" /><published>2025-02-11T09:00:00+00:00</published><updated>2025-02-11T09:00:00+00:00</updated><id>https://www.lukasrieder.com/blog/finicky-scriptable-macos-url-handler</id><content type="html" xml:base="https://www.lukasrieder.com/blog/finicky-scriptable-macos-url-handler/"><![CDATA[<p>The macOS default browser setting looks like a choice between applications. Safari, Firefox, Chrome, perhaps another browser.</p>

<p>I wanted the choice to be a program.</p>

<p>My eventual URL path became:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>macOS URL handler → Finicky → JavaScript rule → executable → destination
</code></pre></div></div>

<p>I like this chain because each arrow leaves me a seam I can understand and replace. macOS owns dispatch. Finicky owns URL parsing and routing policy. A normal executable owns the final effect. In my case, that executable forwards unknown URLs to a browser running outside my work machine.</p>

<p>This is exactly the kind of arrangement I want in dotfiles: visible, scriptable, composable, and replaceable one layer at a time.</p>

<h2 id="finicky-is-a-programmable-default-browser">Finicky is a programmable default browser</h2>

<p><a href="https://github.com/johnste/finicky">Finicky</a> is an open-source macOS application that registers as the default browser. It does not need to render pages. It receives a URL, loads <code class="language-plaintext highlighter-rouge">~/.finicky.js</code>, evaluates matching and rewrite rules, then opens the selected browser or application.</p>

<p>A basic configuration can route work domains to one browser and personal domains to another:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">defaultBrowser</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Safari</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">handlers</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">match</span><span class="p">:</span> <span class="nx">finicky</span><span class="p">.</span><span class="nf">matchHostnames</span><span class="p">([</span><span class="dl">"</span><span class="s2">github.com</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">gitlab.com</span><span class="dl">"</span><span class="p">]),</span>
      <span class="na">browser</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Firefox</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">],</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The JavaScript configuration is the attraction. Rules can be functions. URLs can be rewritten. Hostnames can be matched through Finicky’s parsed representation instead of fragile string slicing. The file can live beside shell functions, editor configuration, and the rest of my workstation policy.</p>

<p>I could have used a GUI router, and that would have been easier to start with. I know myself well enough to know where that ends: sooner or later I want to review a rule in Git, generate part of it, share it between machines, or make the destination something other than an installed browser.</p>

<h2 id="i-did-not-want-another-browser-destination">I did not want another browser destination</h2>

<p>My unknown-link policy required a command called <code class="language-plaintext highlighter-rouge">SafeBrowser</code>. Finicky should select it just as it selected Safari, then pass the URL as one argument.</p>

<p>At the beginning of 2025, the version I was using modeled destinations as application names, bundle identifiers, or application paths. That was sensible for a browser picker. It stopped one layer short of the composition I wanted.</p>

<p>So I forked Finicky at <a href="https://github.com/Overbryd/finicky">Overbryd/finicky</a> and added a <code class="language-plaintext highlighter-rouge">command</code> application type. The public implementation commit is <a href="https://github.com/Overbryd/finicky/commit/e380acbf01c20ae16b1a8f65e25cb5379c37aa52"><code class="language-plaintext highlighter-rouge">e380acbf01c…</code></a>.</p>

<p>The configuration schema gained <code class="language-plaintext highlighter-rouge">appType: "command"</code> and a command path. The native side carried that option into its browser descriptor. When selected, Finicky constructed an argument array containing the executable path and URL.</p>

<p>One implementation detail mattered enough to me not to compromise on it: the URL must remain data. The implementation does not produce one shell string such as:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/Users/me/.bin/safebrowser <span class="s2">"</span><span class="k">${</span><span class="nv">url</span><span class="k">}</span><span class="s2">"</span>
</code></pre></div></div>

<p>It uses <code class="language-plaintext highlighter-rouge">Process</code>, invokes <code class="language-plaintext highlighter-rouge">/usr/bin/env</code>, and supplies the command and URL as separate arguments. Shell metacharacters inside an attacker-controlled URL do not become shell syntax. My configuration also uses an absolute executable path, avoiding a <code class="language-plaintext highlighter-rouge">PATH</code> lookup for the security boundary.</p>

<p>I treat a command destination as privileged configuration. Whoever can modify <code class="language-plaintext highlighter-rouge">.finicky.js</code> or the target executable can redirect every external link or run code whenever one opens. I therefore keep both files under my ownership and out of reach of less-trusted processes.</p>

<h2 id="default-to-the-script">Default to the script</h2>

<p>The resulting configuration reverses the usual browser-router pattern. Instead of making Safari the default and adding special cases, I make <code class="language-plaintext highlighter-rouge">SafeBrowser</code> the default and add narrow local exceptions:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">defaultBrowser</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SafeBrowser</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">appType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">command</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">command</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/Users/me/.bin/safebrowser</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">handlers</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">match</span><span class="p">:</span> <span class="nx">finicky</span><span class="p">.</span><span class="nf">matchHostnames</span><span class="p">([</span>
        <span class="sr">/.*</span><span class="se">\.?</span><span class="sr">github</span><span class="se">\.</span><span class="sr">com$/</span><span class="p">,</span>
        <span class="sr">/.*</span><span class="se">\.?</span><span class="sr">example-work-service</span><span class="se">\.</span><span class="sr">com$/</span><span class="p">,</span>
      <span class="p">]),</span>
      <span class="na">browser</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Safari</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">],</span>
<span class="p">};</span>
</code></pre></div></div>

<p>I like the failure direction of this policy. A new domain does not accidentally open in my work browser because I forgot to classify it. My omission produces less trust, not more: the URL follows the unknown route.</p>

<p>I review these rules as if the input were trying to fool them—because sometimes it is. I anchor patterns and match parsed hostnames rather than searching arbitrary strings. A rule matching <code class="language-plaintext highlighter-rouge">github.com</code> anywhere in the full URL could be fooled by a hostname such as <code class="language-plaintext highlighter-rouge">github.com.attacker.example</code> or by placing the trusted text in a query parameter.</p>

<p>I also keep the allowlist small. If my configuration contains half the internet, then I have not written a trust policy; I have built an unmaintainable alternate browser history.</p>

<h2 id="the-script-is-an-adapter">The script is an adapter</h2>

<p><code class="language-plaintext highlighter-rouge">SafeBrowser</code> is deliberately ignorant of Finicky. It accepts one URL and arranges for another machine to open it:</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/osascript</span><span class="w">

</span><span class="k">on</span> <span class="nb">run</span><span class="w"> </span><span class="nv">argv</span><span class="w">
  </span><span class="k">set</span><span class="w"> </span><span class="nv">remoteMachine</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">first</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="se">¬
</span><span class="w">    </span><span class="p">(</span><span class="nb">read</span><span class="w"> </span><span class="p">(</span><span class="nv">POSIX</span><span class="w"> </span><span class="na">path</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">(</span><span class="nb">path to</span><span class="w"> </span><span class="nv">home</span><span class="w"> </span><span class="nv">folder</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="p">)</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="se">¬
</span><span class="w">    </span><span class="s2">".config/vm1.local/eppc"</span><span class="p">)</span><span class="w"> </span><span class="nv">using</span><span class="w"> </span><span class="nv">delimiter</span><span class="w"> </span><span class="s2">"\n"</span><span class="p">)</span><span class="w">

  </span><span class="k">tell</span><span class="w"> </span><span class="nb">application</span><span class="w"> </span><span class="s2">"Finder"</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">machine</span><span class="w"> </span><span class="nv">remoteMachine</span><span class="w"> </span><span class="se">¬
</span><span class="w">    </span><span class="k">to</span><span class="w"> </span><span class="nb">open location</span><span class="w"> </span><span class="p">(</span><span class="nb">item</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">argv</span><span class="p">)</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nb">run</span><span class="w">
</span></code></pre></div></div>

<p>The destination address lives outside the public dotfiles. The script uses Remote Apple Events to tell Finder in a macOS browser VM to open the URL.</p>

<p>I like this adapter because no layer knows too much:</p>

<ul>
  <li>Finicky does not know how the VM is reached.</li>
  <li><code class="language-plaintext highlighter-rouge">SafeBrowser</code> does not know why this URL was selected.</li>
  <li>the VM does not need Finicky installed;</li>
  <li>macOS does not need to know that its “default browser” is a policy engine.</li>
</ul>

<p>I can replace Apple Events with SSH without rewriting matching rules. I can replace the VM while keeping the script’s one-URL contract. I can test the script directly from a terminal. I can point one rule at a different command for a temporary experiment.</p>

<p>I prefer composition over tight vertical integration. It lets me defer decisions, replace one layer, and hack on a new transport whenever I want. If a group of these pieces eventually stands the test of time, I can still compress them into one vertically integrated application, bundle it, and package it. I prefer that compression to be the last step, earned by experience, rather than the first architectural decision.</p>

<h2 id="a-url-is-an-argument-not-a-command">A URL is an argument, not a command</h2>

<p>Arbitrary command support introduces a sharp edge, so the contract should stay narrow:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input: exactly one absolute http or https URL
output: destination receives that URL, or the command fails visibly
</code></pre></div></div>

<p>For my adapter, I want a deliberately unforgiving contract. It should reject missing arguments, extra arguments, unsupported schemes, unreasonable size, and invalid URL syntax. I do not want it logging query strings because they may contain tokens or recipient identifiers. I want a timeout when contacting another machine and a non-zero status when delivery fails.</p>

<p>My hard line is that the adapter must never interpolate the URL into shell source. This applies locally and across SSH. The following shape is dangerous:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh browser-vm <span class="s2">"open '</span><span class="nv">$url</span><span class="s2">'"</span>
</code></pre></div></div>

<p>Quoting untrusted data across two shell parsers is easy to get wrong. A forced remote command reading an encoded value from standard input, or a small protocol that keeps data separate from command text, is safer.</p>

<p>The AppleScript version also needs operational hardening. Remote Apple Events credentials are sensitive. Network access to the guest should be restricted. Failure must not silently fall back to opening the URL locally, because that would erase the policy exactly when isolation is unavailable.</p>

<h2 id="configuration-as-code-is-the-real-feature">Configuration as code is the real feature</h2>

<p>The important part of this setup is not JavaScript as a language. It is that the routing decision is text I own.</p>

<p>A dotfiles-native policy gives me:</p>

<ul>
  <li><strong>history</strong> — I can see when a domain became locally trusted;</li>
  <li><strong>review</strong> — a broad regular expression appears as a diff;</li>
  <li><strong>reproduction</strong> — a new Mac receives the same handler and scripts;</li>
  <li><strong>composition</strong> — rules can target browsers, profiles, applications, or commands;</li>
  <li><strong>rollback</strong> — a broken policy is one Git operation away from its prior state;</li>
  <li><strong>portability</strong> — the command behind a rule can evolve independently.</li>
</ul>

<p>It also removes mystery. When a link opens in the wrong place, I inspect one rule file and one executable. I do not search preference panes, browser extensions, and remembered prompt choices.</p>

<p>I do not mistake configuration as code for automatic security. Finicky executes JavaScript, so my config is executable policy. My regexes can be wrong. My script can leak URLs. My public dotfiles can expose private hostnames or machine details. What I gain is not perfection; I gain the ability to inspect, review, and correct those risks.</p>

<h2 id="test-the-whole-chain">Test the whole chain</h2>

<p>I do not consider a URL router finished after clicking one familiar link and watching the right browser open. I want examples for each decision:</p>

<ul>
  <li>an exact allowlisted hostname;</li>
  <li>a legitimate subdomain;</li>
  <li>a deceptive suffix such as <code class="language-plaintext highlighter-rouge">trusted.example.attacker.invalid</code>;</li>
  <li>an unknown HTTPS domain;</li>
  <li>plain HTTP;</li>
  <li>a URL with spaces or encoded characters;</li>
  <li>a query string containing shell metacharacters;</li>
  <li>a URL shortener;</li>
  <li>a non-web scheme;</li>
  <li>an unavailable browser VM.</li>
</ul>

<p>The expected destination should be explicit for each case. Testing only the JavaScript match misses command dispatch. Testing only the script misses policy errors. The useful test crosses the same boundary as a real click.</p>

<p>I also want failure to remain safe. If Finicky cannot load its configuration or <code class="language-plaintext highlighter-rouge">SafeBrowser</code> cannot reach the guest, the result should be a visible failure—not an automatic local-browser fallback.</p>

<h2 id="keep-the-layers-boring">Keep the layers boring</h2>

<p>“URL handler to Finicky to a script” sounds more complicated than choosing Safari in System Settings. Internally, each layer is simpler than a monolithic tool that parses URLs, manages every destination, authenticates to a VM, and opens a remote browser itself.</p>

<p>macOS already knows how to dispatch URLs. Finicky is good at programmable routing. A script is good at adapting one selected URL to a local environment. The guest is good at containing a browser.</p>

<p>That division is why the setup feels hacker-friendly rather than merely clever. Every piece has a small interface. Every policy lives in a file. Every implementation can be replaced with another command.</p>

<p>Finicky gave me control of the decision. My command extension kept the destination open-ended. Putting both pieces in public dotfiles made the setup durable without pretending it had reached its final form—and that is exactly how I prefer my tools.</p>]]></content><author><name>Lukas Rieder</name></author><category term="dotfiles" /><category term="finicky" /><category term="macos" /><category term="security" /><category term="automation" /><summary type="html"><![CDATA[A programmable URL handler becomes much more useful when its final destination can be any small executable in my dotfiles.]]></summary></entry><entry xml:lang="en"><title type="html">Your default url handler is a security issue</title><link href="https://www.lukasrieder.com/blog/your-default-url-handler-is-a-security-issue/" rel="alternate" type="text/html" title="Your default url handler is a security issue" /><published>2025-02-11T08:00:00+00:00</published><updated>2025-02-11T08:00:00+00:00</updated><id>https://www.lukasrieder.com/blog/your-default-url-handler-is-a-security-issue</id><content type="html" xml:base="https://www.lukasrieder.com/blog/your-default-url-handler-is-a-security-issue/"><![CDATA[<p>I maintained a bunch of small things in my dotfiles in February 2025. I improved language-server support, added a helper for debugging Elixir releases, restored some Go setup, and adjusted how links open on my Mac.</p>

<p>That last change is the one I keep wondering about. Why is everyone who takes workstation hardening seriously not also taking ownership of URL handling?</p>

<p>Think about how many incursions begin on a work computer. A message arrives. It contains a link. The domain looks plausible, the sender appears familiar, or the subject creates just enough urgency. One click transfers an attacker-controlled string through the operating system into a large, networked application containing authenticated sessions, saved permissions, extensions, downloads, and access to local files.</p>

<p><a href="https://attack.mitre.org/techniques/T1204/001/">MITRE ATT&amp;CK calls this User Execution through a Malicious Link</a>. A related path, <a href="https://attack.mitre.org/techniques/T1189/">Drive-by Compromise</a>, begins when a browser visits an attacker-controlled or compromised website. <a href="https://www.cisa.gov/resources-tools/resources/phishing-guidance-stopping-attack-cycle-phase-one">CISA’s phishing guidance</a> treats the link as a central initial-access mechanism.</p>

<p>For years, that path nagged me. In February, I finally dealt with it at the point before a browser opens: OS-level URL handling.</p>

<h2 id="a-click-is-a-dispatch-decision">A click is a dispatch decision</h2>

<p>On macOS, applications hand URLs to the operating system. Launch Services resolves the registered handler for a scheme such as <code class="language-plaintext highlighter-rouge">http</code> or <code class="language-plaintext highlighter-rouge">https</code>, then delivers the URL to that application. Normally, the handler is Safari, Firefox, Chrome, or another browser.</p>

<p>That default optimizes for convenience. It assumes that every link arriving from Mail, Messages, Slack, a terminal, a PDF reader, or a password manager should immediately enter the same browser environment.</p>

<p>My browser is not an empty document viewer. It carries work sessions. It can reach administration panels, source hosts, cloud consoles, invoicing, internal tools, and identity providers. Even with MFA, a live browser session represents authority. Sending every unknown URL directly into that context is an unnecessary default.</p>

<p>I consider the OS-level handoff a trust boundary. Before dispatch, the URL is data. After dispatch, a browser begins resolving, connecting, parsing, rendering, and possibly downloading. I want my own policy between those states instead of accepting one global browser choice as permanent.</p>

<p>One important limit: this boundary governs links handed to macOS. It does not intercept every navigation after I am already inside a browser. A malicious page opened from an allowlisted local page can still navigate elsewhere. URL brokering reduces one exposure path; it does not turn the browser into a safe viewer.</p>

<h2 id="what-can-go-wrong-after-default-dispatch">What can go wrong after default dispatch</h2>

<p>A routing layer cannot know whether a page is benign. It can still reduce several kinds of risk.</p>

<h3 id="phishing-meets-an-authenticated-session">Phishing meets an authenticated session</h3>

<p>The most common danger is not a browser sandbox escape. It is a convincing login page, consent screen, OAuth flow, or support prompt opened in exactly the environment where I normally authenticate.</p>

<p>Separating unknown links from my work browser removes familiar cookies and active sessions from the first interaction. The isolation browser can still be phished, but it begins with less authority. A fake page also looks less natural when it appears in a deliberately untrusted environment.</p>

<h3 id="browsers-process-hostile-input">Browsers process hostile input</h3>

<p>A URL can lead to exploit code, a compromised legitimate site, malicious advertising, or a download crafted for the next stage of an attack. Modern browsers have strong sandboxes, and macOS adds quarantine, Gatekeeper, and notarization checks. None of these controls is a reason to place the browser next to every valuable host resource if I can avoid it.</p>

<p>Moving unknown browsing into a VM adds another containment boundary. A browser compromise must then escape both browser and guest—or abuse an integration channel—before reaching the work host.</p>

<h3 id="links-can-cross-application-boundaries">Links can cross application boundaries</h3>

<p>Web pages and messages may point to non-web schemes that launch other applications. URL-scheme handling has a long history of surprising behavior because the receiving app decides what the payload means.</p>

<p>A broker can reject schemes it does not expect and ensure that its fallback only forwards <code class="language-plaintext highlighter-rouge">http</code> and <code class="language-plaintext highlighter-rouge">https</code>. This does not secure every registered macOS handler; it narrows what this particular path accepts.</p>

<h3 id="the-url-itself-leaks-information">The URL itself leaks information</h3>

<p>Tracking parameters can identify the recipient, campaign, account, or document. Some routing tools can remove known parameters or rewrite hosts before opening. That is primarily privacy hardening, but less unnecessary context reaching a remote server is also useful security hygiene.</p>

<h3 id="profiles-collapse-trust-zones">Profiles collapse trust zones</h3>

<p>Personal browsing, client systems, development tools, and privileged administration do not need the same browser profile. Routing known domains into deliberate profiles reduces accidental credential use and keeps extensions, cookies, and history from becoming one large trust zone.</p>

<h2 id="what-a-url-broker-canand-cannotprevent">What a URL broker can—and cannot—prevent</h2>

<p>A broker can block an unexpected scheme, prompt before dispatch, rewrite a URL, route a known domain to a specific browser profile, or send everything unknown to an isolated machine. It replaces one global default with an explicit policy.</p>

<p>It cannot inspect a page and declare it safe. It cannot guarantee an allowlisted domain has not been compromised. It cannot stop a phishing link clicked from inside an already-open browser. It cannot compensate for an unpatched browser, weak identity controls, or a VM configured with broad host access.</p>

<p>The claim I am comfortable making is smaller: taking ownership of URL dispatch can reduce my exposure and the possible blast radius. More importantly for me, it turns “open every link here” from an ambient assumption into a decision I can inspect and change.</p>

<h2 id="the-macos-url-router-landscape">The macOS URL-router landscape</h2>

<p>There are many browser pickers and routers. This is the credible landscape I found while revisiting the setup, not a claim that no small utility exists outside the list.</p>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>Approach</th>
      <th>Useful distinction</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://github.com/johnste/finicky">Finicky</a></td>
      <td>JavaScript rules and rewrites</td>
      <td>Open source and configuration-as-code; my choice</td>
    </tr>
    <tr>
      <td><a href="https://choosy.app/">Choosy</a></td>
      <td>Prompting and configurable rules</td>
      <td>Mature commercial “smarter default browser”</td>
    </tr>
    <tr>
      <td><a href="https://sindresorhus.com/velja">Velja</a></td>
      <td>Native picker and routing</td>
      <td>Polished Mac utility with tracking-link conveniences</td>
    </tr>
    <tr>
      <td><a href="https://browserosaurus.com/">Browserosaurus</a></td>
      <td>Prompt on each link</td>
      <td>Open-source browser chooser; repository was archived when checked</td>
    </tr>
    <tr>
      <td><a href="https://loshadki.app/openin4/">OpenIn</a></td>
      <td>Rules across browsers, profiles, mail, and apps</td>
      <td>Broad GUI-managed routing</td>
    </tr>
    <tr>
      <td><a href="https://getbumpr.com/">Bumpr</a></td>
      <td>Click-time browser or mail-app choice</td>
      <td>Simple, deliberate chooser</td>
    </tr>
    <tr>
      <td><a href="https://webcatalog.io/en/switchbar/">Switchbar</a></td>
      <td>Browser, profile, and email-client picker</td>
      <td>Supports macOS and Windows</td>
    </tr>
    <tr>
      <td><a href="https://github.com/fluffypony/yojam">Yojam</a></td>
      <td>Rules for browsers, apps, and profiles</td>
      <td>Newer open-source native option</td>
    </tr>
  </tbody>
</table>

<p>I also found smaller open-source implementations including <a href="https://github.com/lailo/bowzer">Bowzer</a>, <a href="https://github.com/vismaytiwari/multi_browse">multi_browse</a>, <a href="https://github.com/mpalczew/punt">Punt</a>, <a href="https://github.com/1789984734/LinkGate">LinkGate</a>, and <a href="https://github.com/amit-t/usher">Usher</a>. Their maturity, maintenance, and licensing vary. Browser routers are popular small projects; evaluate the code that will receive every external URL on your machine.</p>

<p>I could have stopped at a graphical picker if I only wanted to choose between browsers. I wanted the policy in my dotfiles, and I wanted an arbitrary command to be a valid destination. Finicky gave me the useful base without forcing the final shape of the solution.</p>

<h2 id="my-policy-known-local-unknown-elsewhere">My policy: known local, unknown elsewhere</h2>

<p>I made Finicky the default <code class="language-plaintext highlighter-rouge">http</code> and <code class="language-plaintext highlighter-rouge">https</code> handler. Its rules send a small set of expected work destinations to local Safari. The default route does the opposite of a normal browser configuration: everything not explicitly matched goes to <code class="language-plaintext highlighter-rouge">SafeBrowser</code>.</p>

<p>A simplified version looks like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">defaultBrowser</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SafeBrowser</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">appType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">command</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">command</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/Users/me/.bin/safebrowser</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">handlers</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">match</span><span class="p">:</span> <span class="nx">finicky</span><span class="p">.</span><span class="nf">matchHostnames</span><span class="p">([</span>
        <span class="sr">/.*</span><span class="se">\.?</span><span class="sr">github</span><span class="se">\.</span><span class="sr">com$/</span><span class="p">,</span>
        <span class="sr">/.*</span><span class="se">\.?</span><span class="sr">example-work-service</span><span class="se">\.</span><span class="sr">com$/</span><span class="p">,</span>
      <span class="p">]),</span>
      <span class="na">browser</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Safari</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">],</span>
<span class="p">};</span>
</code></pre></div></div>

<p>I care more about the default than the allowlist. New and misspelled domains should not silently inherit local trust just because I have not classified them yet. I prefer them to leave the host until I deliberately add a rule.</p>

<p>I do not trust loose hostname matching here. A substring test for <code class="language-plaintext highlighter-rouge">github.com</code> could also match <code class="language-plaintext highlighter-rouge">github.com.attacker.example</code>. Finicky parses the URL and provides hostname matchers, but I still anchor and review my patterns. Internationalized domains, redirects, URL shorteners, and compromised subdomains are why I never want a matching hostname to become synonymous with safety.</p>

<h2 id="do-not-run-the-untrusted-browser-on-the-work-machine">Do not run the untrusted browser on the work machine</h2>

<p>I considered a separate local browser profile, and for many workflows that may be enough. It still shares the kernel, filesystem permissions, browser installation, and many host integrations. For unknown links, I prefer paying the extra operational cost of a VM in exchange for a stronger boundary.</p>

<p>My fallback browser runs on another macOS machine represented as a VM endpoint. <code class="language-plaintext highlighter-rouge">SafeBrowser</code> is a short AppleScript executable:</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/osascript</span><span class="w">

</span><span class="k">on</span> <span class="nb">run</span><span class="w"> </span><span class="nv">argv</span><span class="w">
  </span><span class="k">set</span><span class="w"> </span><span class="nv">remoteMachine</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">first</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="se">¬
</span><span class="w">    </span><span class="p">(</span><span class="nb">read</span><span class="w"> </span><span class="p">(</span><span class="nv">POSIX</span><span class="w"> </span><span class="na">path</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">(</span><span class="nb">path to</span><span class="w"> </span><span class="nv">home</span><span class="w"> </span><span class="nv">folder</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="p">)</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="se">¬
</span><span class="w">    </span><span class="s2">".config/vm1.local/eppc"</span><span class="p">)</span><span class="w"> </span><span class="nv">using</span><span class="w"> </span><span class="nv">delimiter</span><span class="w"> </span><span class="s2">"\n"</span><span class="p">)</span><span class="w">

  </span><span class="k">tell</span><span class="w"> </span><span class="nb">application</span><span class="w"> </span><span class="s2">"Finder"</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">machine</span><span class="w"> </span><span class="nv">remoteMachine</span><span class="w"> </span><span class="se">¬
</span><span class="w">    </span><span class="k">to</span><span class="w"> </span><span class="nb">open location</span><span class="w"> </span><span class="p">(</span><span class="nb">item</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">argv</span><span class="p">)</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nb">run</span><span class="w">
</span></code></pre></div></div>

<p>The script reads the Remote Apple Events address from a private file and asks Finder in the guest to open the URL. The address and credentials do not belong in dotfiles or logs.</p>

<p>There are other ways to deliver a URL to a browser VM:</p>

<ul>
  <li>SSH with a forced guest command that validates one URL argument and calls <code class="language-plaintext highlighter-rouge">open</code> or <code class="language-plaintext highlighter-rouge">xdg-open</code>;</li>
  <li>an authenticated listener on a host-only network;</li>
  <li>a one-way queue consumed by the guest;</li>
  <li>a hypervisor guest-control command.</li>
</ul>

<p>If I replaced Remote Apple Events today, I would probably begin with SSH because it is portable and already understood. I would not construct <code class="language-plaintext highlighter-rouge">ssh guest "open $url"</code> and let an attacker-controlled URL become shell text. I would pass encoded data or a separate argument into a narrowly constrained command, validate the scheme and length, and avoid a general-purpose remote shell key.</p>

<p>I am willing to sacrifice convenience to preserve this boundary. Shared clipboard, drag-and-drop, mounted host directories, saved work credentials, broad Remote Apple Events access, and guest-control tools all create paths back toward the host. I disable what the browser does not need, keep the guest patched, and prefer snapshots or disposable instances when practical. I treat the host-to-guest opener as security-sensitive software, not harmless desktop glue.</p>

<h2 id="the-practical-hardening-checklist">The practical hardening checklist</h2>

<p>My implementation is small, but its policy is explicit:</p>

<ol>
  <li>Register a broker—not a browser—as the macOS handler for <code class="language-plaintext highlighter-rouge">http</code> and <code class="language-plaintext highlighter-rouge">https</code>.</li>
  <li>Keep local routing rules narrow and anchored to parsed hostnames.</li>
  <li>Make the unknown case the isolated case.</li>
  <li>Accept only intended schemes at the forwarding boundary.</li>
  <li>Pass URLs as arguments, never interpolated shell code.</li>
  <li>Keep router configuration and scripts versioned in dotfiles.</li>
  <li>Protect the router config, executable, and remote-control credentials from untrusted writes.</li>
  <li>Remove VM integrations that contradict the isolation goal.</li>
  <li>Do not place valuable authenticated sessions in the untrusted browser.</li>
  <li>Test known, unknown, malformed, shortened, and non-HTTP URLs.</li>
</ol>

<p>There is still judgment in the allowlist. A locally opened service can link to the rest of the web. A trusted domain can be compromised. The configuration is a reduction of implicit trust, not a declaration that certain hostnames are permanently harmless.</p>

<h2 id="take-ownership-before-the-browser-starts">Take ownership before the browser starts</h2>

<p>The security industry spends enormous effort hardening browsers after they receive a URL. That work is essential. I had paid far less attention to the decision that sends a URL into a particular browser in the first place.</p>

<p>Making Finicky the OS handler gave me a policy point. Adding a script destination let that policy cross the machine boundary. Running unknown links in a VM kept routine convenience while moving hostile parsing and low-trust sessions away from my work environment.</p>

<p>A link is not harmless because it is small enough to fit in a message. It is an instruction to begin a network interaction in some security context. The operating system already asks which application should receive that instruction. I would rather answer with code than with one global default I stopped questioning years ago.</p>]]></content><author><name>Lukas Rieder</name></author><category term="dotfiles" /><category term="security" /><category term="macos" /><category term="browsers" /><category term="virtualization" /><summary type="html"><![CDATA[Taking ownership of macOS URL handling lets unknown links leave the work machine before they reach a privileged browser session.]]></summary></entry><entry xml:lang="en"><title type="html">Give the child a name</title><link href="https://www.lukasrieder.com/blog/give-the-child-a-name/" rel="alternate" type="text/html" title="Give the child a name" /><published>2025-01-22T08:00:00+00:00</published><updated>2025-01-22T08:00:00+00:00</updated><id>https://www.lukasrieder.com/blog/give-the-child-a-name</id><content type="html" xml:base="https://www.lukasrieder.com/blog/give-the-child-a-name/"><![CDATA[<p>Before Plantronic became a substantial production-planning system, it needed a name that described the system rather than the organization around it.</p>

<p>The earliest visible milestone in 2025 is almost comically small: references in the project wiki changed from an earlier working name to <strong>Plantronic</strong>. No scheduler became faster. No factory data was imported. No user got a new screen. Yet the rename marked an important transition.</p>

<p>The name was chosen by the client’s project manager, not invented by the software team as a branding exercise. It survived all the way into production and rallied dozens of early adopters behind one recognizable initiative.</p>

<p>A client project often begins as “the tool for this team” or “the new version of that spreadsheet.” Those names are convenient while a problem is still local. They become restrictive once the software starts connecting planning, people, process steps, imports, permissions, and reporting.</p>

<p>A proper name creates a boundary. Plantronic was not the factory, the source system, or a single planning view. It was the application responsible for turning several kinds of operational information into an inspectable planning process.</p>

<h2 id="a-working-title-is-already-teaching-the-organization">A working title is already teaching the organization</h2>

<p>Introducing software into an organization starts long before production. The name appears in meetings, invitations, tickets, demonstrations, documentation, support requests, and conversations between future users. Every repetition teaches people what the thing is and where it belongs.</p>

<p>That is why I hate working titles that do not make it to production. A working title does not remain an innocent placeholder once people begin using it. It builds a shared mental model. Changing it later means more than renaming modules and links: the organization must unlearn one identity while learning another. For a while, both names coexist, and every conversation must spend time establishing whether everyone is still talking about the same thing.</p>

<p>There are exceptions. A small discovery group may need a disposable label, and confidential work may require a code name. But once a project is introduced to the people expected to adopt, operate, or explain it, the name should be treated as durable.</p>

<p>Plantronic demonstrated the benefit. When someone used the name, there was never doubt about where the project was heading or who should communicate with whom about what. The name gave everyone a stable point of reference while the software itself was still taking shape. It helped materialize an idea into an organizational reality.</p>

<h2 id="names-become-architecture">Names become architecture</h2>

<p>A name alone cannot rescue a muddled design. It can, however, make muddle easier to see.</p>

<p>Once the application had its own identity, questions became sharper:</p>

<ul>
  <li>Which concepts belong inside Plantronic?</li>
  <li>Which facts remain owned by external systems?</li>
  <li>Which changes are planning decisions rather than imported truth?</li>
  <li>Which reports explain Plantronic’s state, and which merely repeat source data?</li>
</ul>

<p>Those questions appeared repeatedly during implementation. Task routing, team workloads, process transitions, SAP-facing imports, public views, and reporting all needed boundaries. The project name became shorthand for one side of those boundaries.</p>

<p>This matters especially in consulting work. A code name borrowed from a customer or an existing system quietly encourages accidental coupling. Developers reproduce vocabulary and assumptions without deciding whether they belong in the new product. A distinct name gives the team permission to model the problem deliberately.</p>

<h2 id="rename-while-it-is-still-cheap">Rename while it is still cheap</h2>

<p>Renaming later is expensive. The old term spreads into repositories, modules, database identifiers, deployment scripts, documentation, and conversation. Even when a technical rename is possible, the mental model lingers. The social migration is usually larger than the code diff.</p>

<p>January 2025 was early enough. The wiki change preceded most implementation milestones and happened before broad adoption hardened the earlier label. From then on, Plantronic remained Plantronic—from the first adopters to production.</p>

<p>Looking back after the project was completed, I like that its history begins this way. Long projects are remembered through large releases and difficult migrations, but their direction is often set by smaller acts of clarification. Giving the child a name was one of them.</p>]]></content><author><name>Lukas Rieder</name></author><category term="plantronic" /><category term="product-development" /><category term="consulting" /><summary type="html"><![CDATA[How an early rename helps define the boundary of long-running software projects.]]></summary></entry><entry xml:lang="en"><title type="html">Stop typing `git push –set-upstream`</title><link href="https://www.lukasrieder.com/blog/stop-typing-git-push-set-upstream/" rel="alternate" type="text/html" title="Stop typing `git push –set-upstream`" /><published>2025-01-03T08:00:00+00:00</published><updated>2025-01-03T08:00:00+00:00</updated><id>https://www.lukasrieder.com/blog/stop-typing-git-push-set-upstream</id><content type="html" xml:base="https://www.lukasrieder.com/blog/stop-typing-git-push-set-upstream/"><![CDATA[<p>Early in January 2025, I made a small change to my dotfiles:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[push]</span><span class="w">
    </span><span class="py">autoSetupRemote</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">true</span>
</code></pre></div></div>

<p>It removed a small interruption that I had accepted for far too long.</p>

<p>Whenever I created a local branch and pushed it for the first time, Git used to stop and tell me that the branch had no upstream.
It also printed the command I was expected to run:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push <span class="nt">--set-upstream</span> origin my-branch
</code></pre></div></div>

<p>The message was helpful.
The repetition was not.
Nearly every time, I wanted exactly what Git suggested: publish this branch to my normal remote under the same name, then track it.</p>

<p>Furthermore, it should have also suggested the configuration to adapt the default behavior, rather than the quick fix.</p>

<p>With <code class="language-plaintext highlighter-rouge">push.autoSetupRemote</code> enabled, the first plain <code class="language-plaintext highlighter-rouge">git push</code> does that setup automatically.
Later pushes work as before. This is not an alias wrapping Git and it does not invent a new workflow.
It tells Git that, when no upstream exists, my usual intent is obvious enough to act on.</p>

<h2 id="convention-over-configuration-configuration-over-repetition">Convention over configuration, configuration over repetition</h2>

<p>I am careful about convenience settings that hide meaningful state. Remotes matter.
Tracking relationships matter. A push can publish work. I still want to know which repository I am changing.</p>

<p>But typing <code class="language-plaintext highlighter-rouge">--set-upstream origin</code> was not making me reconsider any of those things.
In repositories with one conventional remote, it was ceremony after the decision had already been made.</p>

<p>That distinction is useful beyond Git. Automate repeated mechanics, not unresolved choices.
If I regularly worked with several equally plausible push remotes, I would leave this setting off.
Git stopping would then protect a real decision.</p>

<p>In my normal repositories, there is one clear remote and matching branch names are intentional.
For me, it is rather the exception to work with multiple remotes.</p>]]></content><author><name>Lukas Rieder</name></author><category term="git" /><category term="dotfiles" /><category term="tools" /><summary type="html"><![CDATA[One Git setting removes friction when publishing a branch for the first time.]]></summary></entry><entry xml:lang="en"><title type="html">The tides of Freelancing</title><link href="https://www.lukasrieder.com/posts/the-tides-of-freelancing/" rel="alternate" type="text/html" title="The tides of Freelancing" /><published>2018-06-29T13:38:03+00:00</published><updated>2018-06-29T13:38:03+00:00</updated><id>https://www.lukasrieder.com/posts/the-tides-of-freelancing</id><content type="html" xml:base="https://www.lukasrieder.com/posts/the-tides-of-freelancing/"><![CDATA[<p>This week, a colleague approached me with a couple of questions about freelancing.
He asked about the contract I am using, taxes and the usual technical things.</p>

<p>As we were chatting, he revealed his intentions to become a freelancer to better battle the overhours
and pay he was getting from the startup he is working on.</p>

<p>I ended up writing a couple of elaborate answers, here is what I think.</p>

<p><img src="https://raw.githubusercontent.com/Overbryd/overbryd.github.io/source/static/the-tides-of-freelancing/tides.jpg" alt="tides" /></p>

<p>The core to my answers was, that I certainly think that freelancing can give you a good position to negotiate on time and money.
And you have to exercise that position, otherwise you end up in a situation that neither you want,
nor any client can be happy with.</p>

<p>I see this tightly coupled with some of my best strenghts, that I can bring to any project and differentiate
me from others.
The most obvious being, my <strong>very high level of motivation</strong>.</p>

<p>Lets ponder over this a little bit deeper.</p>

<p>The time I have in my life, is my most precious resource. If everything goes well, I think I can do about 80 years.
And I choose to live in a society and contribute to it, so work is naturally something that I want to
spend time with.</p>

<p>So look at your day, your week and see what portion of time is spent working.
I can tell you exactly how much time I have spent working productively, because I meticulously keep track of time.
This is due to the fact, that I work based on an hourly rate.</p>

<p>My time entries are exact down to the minute. In an overview, and I can proudly show you weeks I logged 50+ hours.
I can also show you, with the same pride, weeks with &lt; 15 work hours or less, even over a couple of months.</p>

<p>There are times, I see myself in need, the project can make use of my skills and I am motivated to be working on it.
I will happily contribute my time, skill and motivation.</p>

<p>But I am equally content with times where I can spend most of the day playing with my children, reading books and
doing ‘nothing’. ‘Nothing productive’.</p>

<p>So, over the course of the years, I became accustomed to this low- and high tide way of working.</p>

<p>There is a strong relation between these intense, fast and long work sessions and doing nothing.
For me, one cannot exist without the other.</p>

<p>I thrive in critical situations, facing challenging problems. And if I can honestly take a challenge,
my whole body and mind are supporting me for the cause.
Over the years, I have learned how much energy I can tap into for my work. And with the right amount of
downtime, this is a hell lot of energy.</p>

<p>People see me doing 14+ hours a day, three times a week, and for the naive bystander it looks
like I am working crazy hard. In reality, I am just very aware of how much of my time I can use for work,
and when I need a break.</p>

<p><strong>I need a strong position</strong> to negotiate about my own time.
I can not, and will not let a company, client or who-ever I am working with dictate how much time I can spend on
a cause that I trade my time against money.</p>

<p>Here you have one of my main reasons, I choose to be a freelancer over being an employee.</p>

<h2 id="conclusion">Conclusion</h2>

<p>If you are in a position that someone is ‘using’ you, or sometimes you could even say ‘abusing’ you.
Make up your mind, and start deciding for yourself.</p>

<ul>
  <li>
    <p>How much of your time are you willing to invest for some other cause than your own?</p>
  </li>
  <li>
    <p>How much can you give sustainably, without taking a risk or damaging your health?</p>
  </li>
  <li>
    <p>What is really driving your motivation? What do you, expect from yourself at work?</p>
  </li>
</ul>

<p>Leave a comment, and subscribe if you like what I am writing about freelancing.</p>]]></content><author><name>Lukas Rieder</name></author><category term="freelancing" /><category term="work" /><summary type="html"><![CDATA[On using freelancing to negotiate time, motivation, intense work, and necessary rest.]]></summary></entry><entry xml:lang="en"><title type="html">50a875be3eac870121c72c3e0951d8c8257db1fc Initializing master branch for publication</title><link href="https://www.lukasrieder.com/posts/initializing-master-branch-for-publication/" rel="alternate" type="text/html" title="50a875be3eac870121c72c3e0951d8c8257db1fc Initializing master branch for publication" /><published>2018-06-28T18:00:55+00:00</published><updated>2018-06-28T18:00:55+00:00</updated><id>https://www.lukasrieder.com/posts/initializing-master-branch-for-publication</id><content type="html" xml:base="https://www.lukasrieder.com/posts/initializing-master-branch-for-publication/"><![CDATA[<p>It was about time to restart my blog. There are so many great things happening right now.
All want to be shared, dissected and written up.</p>

<p>Now I have completely reset my content pipeline. <strong>New</strong> static site generator. <strong>New</strong> repository layout. <strong>New</strong> template.</p>

<p>I have a repository <a href="https://github.com/Overbryd/overbryd.github.io" title="Overbryd/overbryd.github.io">overbryd.github.io</a>. Its first commit dates back to Feb 27, 2009.</p>

<p>I have a number of articles in my pipeline, all waiting to get deployed. Since I started my career, I am now at my 3rd programming language, that I use professionally and have applications running in production.</p>

<p>First, since I am a hacker by heart, I am going to dissect every bit and piece of my new content pipeline, along the way.</p>

<p>Lean back, subscribe, and dive right into my world. I really hope you can enjoy reading my blog.</p>

<p>We are now at <code class="language-plaintext highlighter-rouge">50a875be3eac870121c72c3e0951d8c8257db1fc Initializing master branch for publication</code>.</p>

<p>So much to do, so much to write.</p>]]></content><author><name>Lukas Rieder</name></author><category term="meta" /><category term="publishing" /><category term="static-sites" /><summary type="html"><![CDATA[Restarting a decade-old blog with a new static publishing pipeline.]]></summary></entry><entry xml:lang="en"><title type="html">Connecting Apis With Flow</title><link href="https://www.lukasrieder.com/posts/connecting-apis-with-flow/" rel="alternate" type="text/html" title="Connecting Apis With Flow" /><published>2018-01-01T02:20:00+00:00</published><updated>2018-01-01T02:20:00+00:00</updated><id>https://www.lukasrieder.com/posts/connecting-apis-with-flow</id><content type="html" xml:base="https://www.lukasrieder.com/posts/connecting-apis-with-flow/"><![CDATA[<p>Build an API connector with <code class="language-plaintext highlighter-rouge">Flow</code>.</p>

<p>Say you need to consume events and act upon them. There are numerous services that fit this pattern.
A recent client of mine wanted just that. In this case it was an API connector, subscribing on events from AMQP, mapping the data to the target API structure and posting to it.</p>

<p>Given the projects deadline and budget, I was tempted to be a little more defensive and have proper errors explaining when things go wrong. Besides full test coverage, of course.</p>

<p>I found Elixir <code class="language-plaintext highlighter-rouge">Flow</code> handling that just perfectly well.</p>

<h2 id="the-flow-must-go-on">The flow must go on</h2>

<p>Most of the examples building Elixir flows deal with the ad-hoc/scripting/console usage of Flow. By using <code class="language-plaintext highlighter-rouge">Flow.from_enumerable/1</code> you hook up a flow to some sort of enumerable and with <code class="language-plaintext highlighter-rouge">Enum.into/2</code> you can easily build a nice parallel processing engine.</p>

<p>Taking this a step further, as a <em>forever</em> running application, <code class="language-plaintext highlighter-rouge">Flow.from_stage/1</code> and <code class="language-plaintext highlighter-rouge">Flow.start_link</code> are the way to go.</p>

<p>Here is a showcase of the processing pipeline module:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defmodule Pipeline do

  def start_link() do
    pipeline = Flow.from_stage(Pipeline.Dispatcher, max_demand: 100, stages: 10)
               |&gt; flow
    Flow.start_link(pipeline, name: __MODULE__)
  end

  def flow(input) do
    input
    |&gt; put(:item, &amp;Poison.decode/1)
    |&gt; put(:transformed, &amp;transform/1)
    |&gt; put(:response, &amp;post/1)
    |&gt; Flow.each(&amp;report/1)
  end

  def transform(%{item: item}) do
    # returns {:ok, transformed} or {:error, ...}
    TargetApi.object_from_item(item)
  end
  
  def post(%{transformed: object}) do
    # also returns {:ok, ...} or {:error, ...}
    TargetApi.post(object)
  end
  
  def report({:ok, _state}), do: Logger.info("posted successfully")
  
  def report({:error, key, error, state}) do
    Logger.error("processing errored at #{key}: #{inspect error}, #{inspect state}")
  end
  
  defp put(flow, key, fun) do
    Flow.map(flow, fn
      {:ok, state} -&gt;
        case fun.(state) do
          {:ok, result} -&gt; {:ok, Map.put(state, key, result)}
          error -&gt; {:error, key, error, state}
        end
      pass -&gt; pass
    end)
  end

end
</code></pre></div></div>

<p>I hope you get the idea. There are two nice benefits coming out of this:</p>

<ol>
  <li>Testability</li>
  <li>Error handling</li>
</ol>

<h3 id="testability">Testability</h3>

<p>By having the input of the flow construction as an argument, the flow can be isolated in tests. There is no need to hook up or startup the <code class="language-plaintext highlighter-rouge">Pipeline.Dispatcher</code> GenStage. I use <code class="language-plaintext highlighter-rouge">Flow.from_enumerable/1</code> in my tests to provide input, and I append <code class="language-plaintext highlighter-rouge">Enum.into/2</code> to receive output from the pipeline.</p>

<p>My tests look like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defmodule PipelineTest do

  test "flow fails when provided invalid JSON" do
    assert {:error, :item, _error, _state} = run("not json")
  end

  def run(data) do
    [{:ok, data}]
    |&gt; Flow.from_enumerable
    |&gt; Pipeline.flow
    |&gt; Enum.into([])
    |&gt; List.first
  end
  
end
</code></pre></div></div>

<p>And since the pipeline itself mostly calls into other modules, most of the logic is already tested in the respective tests of these modules. And as long as these functions adhere to the <code class="language-plaintext highlighter-rouge">:ok</code>/<code class="language-plaintext highlighter-rouge">:error</code> tagged returns, the pipeline will deliver. More about that in the next section about error handling.</p>

<h3 id="error-handling">Error handling</h3>

<p>Looking at this flow, it acts as one state transformation. Each result is put in its own key, and can provide input for further actions.
This also acts as the protocol between functions that depend on the output of the previous. The return values can clearly being looked up. Errors can clearly be tracked back to where they occured.</p>

<p>But first, a small fundamental discussion about Elixir error handling.
It is quite a complex topic, mostly due to the fact that there are so many different ways to handle errors in Elixir.
First there is the let-it-crash mentality. Processes isolate the execution, and they stay unaffected by errors from other processes.
Then there is the very common <code class="language-plaintext highlighter-rouge">:ok</code>/<code class="language-plaintext highlighter-rouge">:error</code> construct, tagged return values that indicate whether the called function succeeded in its execution.</p>

<p>Over the course of Elixir development, you will have to pick and choose between them, or mix and match what is best for the users of your application.
In this case, my users are of two different kinds.</p>

<p>The first kind is the end user of the application, who should be affected the least by any malfunction. This user will never see any error output, since the whole pipeline is attached to a notification system that triggers events after he has interacted with the front-facing application. Worst case, this user will just never receive the intended consequences of his interaction.</p>

<p>The second kind are the developers of the front facing application. They are maintaining the application the user is interacting with, they are the ones responsible if something along the way fails and the principal users do not get what they want.</p>

<p>So thought mainly about the latter, the developers. I want them to quickly reason about any potential failures that might occur during processing.</p>

<p>I choose to implement <code class="language-plaintext highlighter-rouge">:ok</code>/<code class="language-plaintext highlighter-rouge">:error</code> for everything that I <em>expect</em> to fail. The main promise there is to quickly get to know what went wrong how.</p>

<p>The second is, that severe crashes happen under supervision, and basic process isolation takes care of keeping the application running and restarts its parts. This good for any <em>unforeseen</em> errors.</p>

<h2 id="dispatcher">Dispatcher</h2>

<p>The ever running input provided to the pipeline is a queueing GenStage dispatching on demand.
In this case the AMQP consumer forwards incoming events to the Dispatcher like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Dispatcher.async_push(Pipeline.Dispatcher, {:ok, data})
</code></pre></div></div>

<p>For completeness sake, here is the implementation of a queueing GenStage producer, dispatching on demand:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defmodule Dispatcher do
  use GenStage

  def start_link(name) do
    GenStage.start_link(__MODULE__, nil, name: name, id: name)
  end

  def init(_) do
    {:producer, {Queue.new, 0}, dispatcher: GenStage.DemandDispatcher}
  end

  def async_push(name, event) do
    GenStage.cast(name, {:push, event})
  end

  def queue_size(name) do
    GenStage.call(name, :queue_size)
  end

  def handle_call(:queue_size, _from, {queue, _demand} = state) do
    {:reply, Queue.size(queue), [], state}
  end

  def handle_cast({:push, event}, {queue, demand}) do
    dispatch_events(Queue.put_front(queue, event), demand, [])
  end

  def handle_demand(incoming_demand, {queue, demand}) when incoming_demand &gt; 0 do
    dispatch_events(queue, demand + incoming_demand, [])
  end

  defp dispatch_events(queue, demand, events) do
    with true &lt;- demand &gt; 0,
      {event, remaining_queue} &lt;- Queue.pop(queue)
    do
      dispatch_events(remaining_queue, demand - 1, [event | events])
    else
      _ -&gt; {:noreply, Enum.reverse(events), {queue, demand}}
    end
  end

end
</code></pre></div></div>]]></content><author><name>Lukas Rieder</name></author><category term="elixir" /><category term="flow" /><category term="genstage" /><category term="api" /><summary type="html"><![CDATA[Building a testable, failure-aware API connector with Elixir Flow and GenStage.]]></summary></entry><entry xml:lang="en"><title type="html">Elli on Elixir</title><link href="https://www.lukasrieder.com/posts/elli-on-elixir/" rel="alternate" type="text/html" title="Elli on Elixir" /><published>2017-10-18T18:57:32+00:00</published><updated>2017-10-18T18:57:32+00:00</updated><id>https://www.lukasrieder.com/posts/elli-on-elixir</id><content type="html" xml:base="https://www.lukasrieder.com/posts/elli-on-elixir/"><![CDATA[<p>In addition to the answers above, I would like to write up a complete guide on getting <code class="language-plaintext highlighter-rouge">knutin/elli</code> to work with Elixir.
The above answers are correct, but I needed some more information to get up and running in my new elixir project.</p>

<ol>
  <li>
    <p>Implement a handler that has <code class="language-plaintext highlighter-rouge">@behaviour :elli_handler</code></p>

    <p>This is your router/controller. A bare minimum example version of this looks like this:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # lib/elli_handler.ex
 defmodule ElliHandler do
   @behaviour :elli_handler
   alias :elli_request, as: Request
    
   def handle(req, args) do
     handle(Request.method(req), Request.path(req), req, args)
   end
    
   def handle(:GET, _, req, args) do
     # do something with the request.
     # e.g. you can use Request.get_arg/2 to fetch a query param
     say = Request.get_arg("say", req)
     # Return a tuple with 3 elements
     # status code, list of header tuples, response body
     {200, [], "echo, #{say}"}
   end
    
   # Here you can react to all kinds of events of the full connection/request/response cycle
   # You must implement this, otherwise elli wont function properly, as it evaluates
   # the return value of handle_event/3.
   def handle_event(event, args, config) do
     # Here would be a good point to handle logging.
     # IO.inspect([event, args, config])
     :ok
   end
    
 end
</code></pre></div>    </div>
  </li>
  <li>
    <p>Lets use an app that starts an elli supervisor</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # lib/elli_supervisor.ex
 defmodule ElliSupervisor do
   use Supervisor

   def start_link(ref, options) do
     Supervisor.start_link(__MODULE__, options, name: ref)
   end

   def init(options) do
     children = [
       worker(:elli, [options], id: :elli_http_server)
     ]
     supervise(children, strategy: :one_for_one)
   end

   def shutdown(ref) do
     case Supervisor.terminate_child(ref, :elli_http_server) do
       :ok -&gt; Supervisor.delete_child(ref, :elli_http_server)
       err -&gt; err
     end
   end
 end

 # lib/app.ex
 defmodule App do
   use Application

   def start(_type, _args) do
     import Supervisor.Spec, warn: false
     # lets start our elli supervisor, setting its options
     # callback should be set to the elli handler that implements the elli_handler behaviour
     # port will be the port elli is listening on, defaults to 8080
     ElliSupervisor.start_link(__MODULE__, callback: ElliHandler, port: 3000)
   end

 end

 # in mix.exs
 def application do
   [
     mod: {App, []}
   ]
 end
</code></pre></div>    </div>
  </li>
  <li>
    <p>Add elli as a dependency to your mix.exs</p>

    <p>Run <code class="language-plaintext highlighter-rouge">mix get.deps</code> to install your dependencies.
 Start your server with <code class="language-plaintext highlighter-rouge">mix run --no-halt</code> or in a console using <code class="language-plaintext highlighter-rouge">iex -S mix</code>.</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # in mix.exs
 defp deps do
   [
     # elli is our web server layer
     {:elli, github: "knutin/elli"}
   ]
 end
</code></pre></div>    </div>
  </li>
</ol>]]></content><author><name>Lukas Rieder</name></author><category term="elixir" /><category term="web-servers" /><summary type="html"><![CDATA[A complete guide to running the Elli web server in an Elixir application.]]></summary></entry><entry xml:lang="en"><title type="html">The world would be a better place if…</title><link href="https://www.lukasrieder.com/2009/10/05/the-world-would-be-a-better-place-if.html" rel="alternate" type="text/html" title="The world would be a better place if…" /><published>2009-10-05T10:00:00+00:00</published><updated>2009-10-05T10:00:00+00:00</updated><id>https://www.lukasrieder.com/2009/10/05/the-world-would-be-a-better-place-if</id><content type="html" xml:base="https://www.lukasrieder.com/2009/10/05/the-world-would-be-a-better-place-if.html"><![CDATA[<p style="float:right;"><img src="https://raw.githubusercontent.com/Overbryd/overbryd.github.io/jekyll-archive/content_images/marriage.jpg" style="border:2px solid firebrick;" alt="" /></p>
<p>&#8230; <a href="http://ballparkapp.com">Ballpark</a> would marry <a href="http://mite.yo.lk">mite</a>. These two fabulous web apps would get along perfectly!</p>
<p>I don&#8217;t want to ask too much, and I know the feature I&#8217;m requesting may fall out of the 80 % category of what the majority wants. But please, dear Ballpark team, consider it. <i>Maybe in a stand up meeting right now!</i></p>
<p>I think this is actually a business opportunity on both sides.<br />
There would be more potentially interested clients. (Don&#8217;t ask me numbers, I don&#8217;t run these apps – ask <a href="http://twitter.com/mite">@mite</a> or <a href="http://twitter.com/metalab">@metalab</a>.)</p>
<p>For more information about each app, here are the links to their tours:</p>
<p>&#8594; <a href="http://mite.yo.lk/en/tour.html">”Fast input, powerful output: mite lets you track your time in a structured way. Advanced reporting helps you and your team to stay on top of your daily business.”</a></p>
<p>&#8594; <a href="http://getballpark.com">”Using Ballpark is going to be your daily bottom of the ninth, full count, walk-off home run moment of invoice zen.”</a></p>
<h2>Managing both without pain</h2>
<p>It is a lot of work to write invoices or estimates. I do this by hand for about a year now. But the more I think about the process, the more I get the feeling it can be done with less effort.<br />
At the end of each month, I sit down and get this quirky process started:</p>
<p>1. Download the reports for every project of the last month from mite as a <span class="caps">PDF</span>.<br />
2. Get the total and write it in the blank with Pages<br />
3. Get the full address of the client and fill it in the blank<br />
4. Save the edited blank as the new invoice<br />
5. Launch Adobe Acrobat<br />
6. Combine those PDFs into one.<br />
7. Send the combined <span class="caps">PDF</span> per E-Mail to the client with a few nice words.</p>
<h3>This scenario could be cut down:</h3>
<p>1. Create a new invoice for the client on my ballpark account.<br />
2. Select the client&#8217;s project with all time entries from last month.<br />
3. Add a few nice words and send the invoice to the client via ballpark.</p>
<h2>The design outline</h2>
<p><img src="https://raw.githubusercontent.com/Overbryd/overbryd.github.io/jekyll-archive/content_images/mite_and_ballpark_in_love.png" alt="" /></p>
<h2>References for development</h2>
<ul>
	<li><a href="http://mite.yo.lk/en/api/">mite. <span class="caps">API</span> documentation</a></li>
	<li><a href="http://github.com/yolk/mite-rb">mite-rb ruby <span class="caps">API</span> implementation</a></li>
</ul>]]></content><author><name>Lukas Rieder</name></author><category term="freelancing" /><category term="invoicing" /><category term="tools" /><summary type="html"><![CDATA[A 2009 product idea connecting mite time tracking with Ballpark invoicing.]]></summary></entry></feed>