<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Alex&#039;s Notebook</title>
	<atom:link href="https://blog.alexseifert.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.alexseifert.com</link>
	<description>Technology and Horror</description>
	<lastBuildDate>Fri, 31 Jul 2026 15:16:38 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2024/02/cropped-favicon.png?fit=32%2C32&#038;ssl=1</url>
	<title>Alex&#039;s Notebook</title>
	<link>https://blog.alexseifert.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">3693140</site>	<item>
		<title>Downsizing</title>
		<link>https://blog.alexseifert.com/2026/07/31/downsizing/</link>
					<comments>https://blog.alexseifert.com/2026/07/31/downsizing/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Fri, 31 Jul 2026 15:16:38 +0000</pubDate>
				<category><![CDATA[General]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Life]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12679</guid>

					<description><![CDATA[Thinking about downsizing my list of personal projects makes me giddy. So, why not just do it?]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="1024" height="559" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/minimalist-desk.png?resize=1024%2C559&#038;ssl=1" alt="AI-generated image of a minimalist in a mountain cabin" class="wp-image-12681" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/minimalist-desk.png?resize=1024%2C559&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/minimalist-desk.png?resize=400%2C218&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/minimalist-desk.png?resize=150%2C82&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/minimalist-desk.png?resize=768%2C419&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/minimalist-desk.png?w=1408&amp;ssl=1 1408w" sizes="(max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">AI-generated image of a minimalist in a mountain cabin</figcaption></figure>
</div>


<p class="wp-block-paragraph">Recently, I read <a href="https://manuelmoreale.com/thoughts/downsizing" target="_blank" rel="noreferrer noopener">a post by Manuel Moreale</a> that inspired me to think seriously about downsizing my own personal project load. Just the very thought of downsizing feels like dreaming about fresh mountain air when you’re stuck in a city of pollution. The prospect of a breath of clean air inspired me to take concrete steps in that direction, the first of which was taking stock of what my project situation currently looks like.</p>



<p class="wp-block-paragraph">It’s really amazing how many projects I seem to have going on at any given time. At the moment, I can count <strong>eleven</strong> active programming projects, <strong>three</strong> active writing projects and <strong>three</strong> renovation projects on our home. That’s not even including the projects I actively work on for my job or the regular chores, household responsibilities and fatherly duties that occupy much of my non-working time. Oh, and did I mention I currently have <strong>twelve</strong> domain names I’m paying for, some of which don’t even have active websites behind them because I optimistically bought them thinking that a project might get far enough along to actually release?</p>



<p class="wp-block-paragraph">The main focus of this post is going to be my personal programming projects which I’m currently inundated with. I tend to start a lot of them, then abandon them as my interests wax and wane. That means I end up with a graveyard of hardly finished, half-finished and mostly finished projects that never actually see the light of day. One day, I’ll work on one of them, then shift gears the next day to another one, but then decide an hour later to start a brand new one because the creative muse suddenly struck the back of my head with its cudgel. Two days later, I’ll pick up an old one again, but then just as quickly abandon it once more. And on and on it goes. Some go for years without seeing any activity while others, like this blog, get regular updates.</p>



<p class="wp-block-paragraph">It sounds crazy, but it gives me a safe space to experiment with new technologies, new ideas, new archictectures and new methodologies. I don’t think I’ll ever stop doing it because my curiosity won’t (and shouldn’t) be tamed. However, it also means I have to clean house every once in a while.</p>



<p class="wp-block-paragraph">The first thing I’m going to do is cancel all domain names I’m not actively using. The next thing will be to “archive” all of my inactive projects as well as the projects I’ve decided to no longer work on. That involves moving everything from my MacBook to an “archive” folder on the 4 TB hard drive on my desktop PC. This not only frees up space on my primary machine (my MacBook), but also cleans up mental space. Most of them are also on <a href="https://github.com/eiskalteschatten" target="_blank" rel="noreferrer noopener">GitHub</a> which means I will also archive those repositories.</p>



<p class="wp-block-paragraph">This whole process isn’t anything new for me, but it is the first time I’ve written about it that I can recall. That said, what makes this time different is that I’m thinking it’s time to clean up some of my older <em>released</em> projects that have been around in some cases for decades. One such example is my history blog, <a href="https://www.historyrhymes.info" target="_blank" rel="noreferrer noopener">History Rhymes</a>. I started it in 2008 and actively posted until about 2019/2020. I’ve been hosting and maintaining it now for years without actually doing anything with it. So, I’ve been thinking about shutting it down.</p>



<p class="wp-block-paragraph">While I would import the posts into this blog and set up redirects so that the posts are preserved, shutting the blog down will free up some mental space in my head. In the end, it’s really about consolidation. The less active sites I have, the less overhead I have. It’s the same with my <a href="https://blog.alexseifert.de" target="_blank" rel="noreferrer noopener">German blog</a>. I hardly write anything on it, so why keep it around as a separate entity and add to my maintenance overhead? Instead, I’ll just import the posts into this blog too and shut it down as well. That’s two of the four blogs I currently have to maintain — a 50% reduction of maintenance overhead and mental bandwidth.</p>



<p class="wp-block-paragraph">I’ve already staged this scenario locally by running a WordPress instance in a Docker container using a recent database dump from this blog. I then took the WordPress export files from both History Rhymes and my German blog, customized the tags and categories manually in the XML, then imported them locally. I repeated this process until they imported cleanly with the right tag/category mixture that felt right to me. In essence, I’m ready to do it and just need to pull the import trigger.</p>



<p class="wp-block-paragraph">At some point in the future, I may consolidate <a href="https://www.the-beskirted-man.com" target="_blank" rel="noreferrer noopener">the last remaining blog</a> into this one as well and just have one single blog for everything, but that won’t be for quite some time, if I do it at all.</p>



<p class="wp-block-paragraph">Other contenders for consolidation are my <a href="https://www.alexseifert.com" target="_blank" rel="noreferrer noopener">portfolio site</a> and this blog. I’ve always kept them separate using WordPress for my blog and whatever flavor of interesting technology happens to be tickling my tongue at the moment to power my portfolio site. I’ve used Frontpage 2000, plain HTML (pre-CSS), ASP (pre-.NET), PHP, Java, Node.js (with and without TypeScript), and Next.js (which is currently powering it). Most iterations used vanilla PHP or PHP with <a href="https://symfony.com" target="_blank" rel="noreferrer noopener">Symfony</a>.</p>



<p class="wp-block-paragraph">That said, I’m thinking it’s about time for WordPress to power it for the first time ever. While not glamorous or exciting, it would allow me to consolidate the two websites into a single entity. I use a fully custom theme for my blog anyway which gives me absolute control over the design and what I can do with it. By creating custom templates combined with CSS and JavaScript, I can do just as much with a WordPress page as I can with any other PHP page. In fact, a lot of the pages on this blog are already custom templates with content fully maintained in code rather than the CMS proper. Even the <a href="https://blog.alexseifert.com/sitemap/" target="_blank" rel="noreferrer noopener">Sitemap page</a> is generated automatically by code. I only use the CMS to power the blog and a few of the basic pages like the Privacy Statement.</p>



<p class="wp-block-paragraph">But I digress. There are a couple of other released projects like <a href="https://www.hauntedhousesoftware.com" target="_blank" rel="noreferrer noopener">Haunted House Software</a> I may just shut down entirely or consolidate into this blog and/or my portfolio site without any fanfare whatsoever. We’ll see. The real point is that I want less. I only want what’s most important to me right now and everything else can be shut down, consolidated and archived.</p>



<p class="wp-block-paragraph">Planning and executing such consolidation processes takes its own effort and mental spaces, but once it’s done, it will feel enormously refreshing — like a breath of fresh mountain air.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/31/downsizing/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12679</post-id>	</item>
		<item>
		<title>Gemini&#8217;s Technology Recommendations for a Publishing Platform</title>
		<link>https://blog.alexseifert.com/2026/07/22/geminis-technology-recommendations-for-a-publishing-platform/</link>
					<comments>https://blog.alexseifert.com/2026/07/22/geminis-technology-recommendations-for-a-publishing-platform/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Wed, 22 Jul 2026 15:44:14 +0000</pubDate>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Software Architecture]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Conversations with AI]]></category>
		<category><![CDATA[frameworks]]></category>
		<category><![CDATA[Node.js]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12626</guid>

					<description><![CDATA[I had a conversation with Gemini about the technology and architecture it would choose for creating a publishing platform. This is the conversation.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" decoding="async" width="1024" height="559" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/Gemini_Generated_Image_hnmg7ohnmg7ohnmg.jpeg?resize=1024%2C559&#038;ssl=1" alt="AI-generated image of a robot writing a blog article" class="wp-image-12640" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/Gemini_Generated_Image_hnmg7ohnmg7ohnmg.jpeg?resize=1024%2C559&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/Gemini_Generated_Image_hnmg7ohnmg7ohnmg.jpeg?resize=400%2C218&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/Gemini_Generated_Image_hnmg7ohnmg7ohnmg.jpeg?resize=150%2C82&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/Gemini_Generated_Image_hnmg7ohnmg7ohnmg.jpeg?resize=768%2C419&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/Gemini_Generated_Image_hnmg7ohnmg7ohnmg.jpeg?w=1408&amp;ssl=1 1408w" sizes="(max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">AI-generated image of a robot writing a blog article</figcaption></figure>
</div>


<p class="wp-block-paragraph">Going through my recent technical conversations with AI about various architecture and technology choices, I thought I would post this one first. It&#8217;s a shorter conversation, but is still typical of the discussions I&#8217;ve had. In this case, it&#8217;s about the technology and architecture choices that would work well for a publishing platform.</p>



<p class="wp-block-paragraph">Given my interest in blogging and the fact that I currently have four more or less active blogs all running on WordPress, I&#8217;ve often wondered what the best technology stack would be to create a custom replacement for WordPress. I&#8217;ve thought about this and made several failed attempts at actually implementing one for years. WordPress is incredibly sticky though and works just well enough that I haven&#8217;t fully gone through the effort of creating a full replacement. That said, I still like to think about it and that&#8217;s what this conversation is mainly about.</p>



<p class="wp-block-paragraph">However, in this conversation, my curiosity gets the better of me and I go off on a tangent about massive publications like BBC that process enormous amounts of requests and what it would choose for that situation as well. It&#8217;s an interesting insight.</p>



<p class="wp-block-paragraph">I did restrict it, though, to the four main languages I&#8217;m familiar with: TypeScript/Node.js, PHP, .NET and Swift (using the Vapor web framework). Even if it were to actually choose something else if I gave it free rein (which I&#8217;ve done in other conversations), I originally began this one with the idea that I might actually implement it. Needless to say, that never happened.</p>



<p class="wp-block-paragraph">You can see my thoughts on the conversation at the end of the post. Also, you can view the original conversation on Gemini <a href="https://share.gemini.google/i3IBUqFVkZsq" data-type="link" data-id="https://share.gemini.google/i3IBUqFVkZsq" target="_blank" rel="noreferrer noopener">here</a>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<pre class="wp-block-preformatted"><strong>Prompt:</strong> <em>I’ve been thinking about what it would take to create a publishing platform for a website that needs the following features: posts with categories, subcategories and tags, users with several roles (reader, admin, contributor, editor, author), a CMS for editing and publishing contents, comments, media, etc., a frontend that is publicly accessible, a frontend cache, the ability for registered users (reader role) to leave comments, RSS feeds, an API for apps to access published content, ability to create, send and manage newsletters, support for multiple languages, the ability to automatically post to social media when new content goes live, support for multiple podcasts, support for video content, and the ability to add several other features. I would like to use TypeScript, PHP, .NET or Swift+Vapor as a technology stack. What would you recommend? Also, what would you recommend in terms of other parts of the tech stack, for example databases?</em></pre>



<p class="wp-block-paragraph">Building a platform with this level of complexity—encompassing strict RBAC, multi-tenant-like media handling, multi-language support, and heavy API/caching requirements—demands an architecture that prioritizes long-term maintainability and performance.</p>



<p class="wp-block-paragraph">Given the requirement for a robust, long-term solution, here is a breakdown of how the candidate technologies stack up for this specific feature set, along with recommendations for the wider architecture.</p>



<h3 class="wp-block-heading">The Backend Application Layer</h3>



<p class="wp-block-paragraph"><strong>1. TypeScript (Node.js with Fastify or NestJS)</strong></p>



<ul class="wp-block-list">
<li><strong>The Verdict:</strong> The strongest pragmatic choice for this specific feature set.</li>



<li><strong>Why:</strong> A platform doing social media auto-posting, RSS generation, newsletter dispatching, and media handling benefits massively from the Node ecosystem. You won&#8217;t have to write integrations for AWS SES (newsletters) or Twitter/LinkedIn APIs from scratch.</li>



<li><strong>Architecture Fit:</strong> Using Fastify gives you excellent throughput for the API and frontend rendering. Having TypeScript on the backend also allows you to share types and validation logic directly with your frontend, speeding up iteration.</li>
</ul>



<p class="wp-block-paragraph"><strong>2. Swift + Vapor</strong></p>



<ul class="wp-block-list">
<li><strong>The Verdict:</strong> The best choice for raw performance, memory safety, and a low-dependency footprint.</li>



<li><strong>Why:</strong> If you want a highly performant, tightly controlled architecture with minimal overhead, Vapor is outstanding. It is particularly strong for building custom, lightning-fast APIs and handling heavy concurrent read requests.</li>



<li><strong>The Trade-off:</strong> The Swift server-side ecosystem is smaller. You will likely find yourself writing custom wrappers for third-party APIs (like social media auto-posting or newsletter service integrations) that would be off-the-shelf packages in TypeScript or PHP.</li>
</ul>



<p class="wp-block-paragraph"><strong>3. PHP</strong></p>



<ul class="wp-block-list">
<li><strong>The Verdict:</strong> Highly capable, but depends on your approach.</li>



<li><strong>Why:</strong> If you utilize a heavy framework like Laravel, this entire feature list is practically its blueprint. However, if you prefer building custom architectures or using vanilla PHP for long-term stability, managing the sheer volume of features (especially asynchronous tasks like bulk newsletter sending and video processing) can become complex compared to event-driven environments.</li>
</ul>



<p class="wp-block-paragraph"><strong>4. .NET</strong></p>



<ul class="wp-block-list">
<li><strong>The Verdict:</strong> An enterprise powerhouse, but potentially heavier than necessary.</li>



<li><strong>Why:</strong> It has phenomenal out-of-the-box support for complex user roles, authentication, and localization. However, it can feel bloated if your goal is a streamlined, low-dependency application.</li>
</ul>



<p class="wp-block-paragraph"><strong>Recommendation:</strong>&nbsp;If you want to move quickly and lean on existing integrations for the auxiliary features (social, newsletters), go with&nbsp;<strong>TypeScript</strong>. If you prioritize a lean, compiled, and highly performant core and don&#8217;t mind writing custom integrations, go with&nbsp;<strong>Swift + Vapor</strong>.</p>



<h3 class="wp-block-heading">The Wider Tech Stack Recommendations</h3>



<p class="wp-block-paragraph">To support the heavy media, caching, and background processing requirements, you need a decoupled architecture.</p>



<p class="wp-block-paragraph"><strong>1. Primary Database: PostgreSQL</strong>&nbsp;PostgreSQL is the undeniable choice here.</p>



<ul class="wp-block-list">
<li><strong>Hierarchical Data:</strong> It natively handles the recursive queries needed for categories and subcategories.</li>



<li><strong>Relational Integrity:</strong> Perfect for managing complex many-to-many relationships (users to roles, posts to tags).</li>



<li><strong>Multi-language:</strong> Postgres’s <code>JSONB</code> column type is incredibly efficient for storing localized string variations (e.g., storing a post&#8217;s title and metadata in multiple languages) without needing overly complex table joins.</li>
</ul>



<p class="wp-block-paragraph"><strong>2. Caching Layer: Redis &amp; SQLite</strong></p>



<ul class="wp-block-list">
<li><strong>Redis:</strong> Essential for managing rate limiting on the API, session storage, and queue management for asynchronous tasks (like sending a 10,000-subscriber newsletter).</li>



<li><strong>Read Cache:</strong> For the publicly accessible frontend, you need a lightning-fast read cache. Pushing compiled, published markdown/HTML into a highly optimized, file-based SQLite database acting purely as a read-cache for the frontend is a brilliant, low-latency pattern that drastically reduces the load on your primary Postgres database.</li>
</ul>



<p class="wp-block-paragraph"><strong>3. Object Storage: S3-Compatible Storage (AWS S3, Cloudflare R2, MinIO)</strong></p>



<ul class="wp-block-list">
<li>Do not store podcasts, videos, or high-res images on the local server block. Offload all media to an object storage bucket placed behind a CDN. Cloudflare R2 is highly recommended here due to its zero-egress fee structure, which is vital when serving bandwidth-heavy podcast and video files.</li>
</ul>



<p class="wp-block-paragraph"><strong>4. Background Workers / Queues</strong></p>



<ul class="wp-block-list">
<li>Features like &#8220;automatically posting to social media,&#8221; &#8220;sending newsletters,&#8221; and &#8220;processing video content&#8221; cannot run on the main thread. You will need a dedicated background worker process. In TypeScript, this would be BullMQ; in Vapor, you would utilize SwiftNIO&#8217;s event loops or a dedicated queuing package to handle these jobs asynchronously.</li>
</ul>



<p class="wp-block-paragraph">Given the heavy media requirements for podcasts and video, are you planning to encode and transcode those media files yourself on the server, or will you offload that processing to a specialized third-party service?</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<pre class="wp-block-preformatted"><strong>Prompt:</strong> <em>This is just a thought experiment. I feel like I really want to create something like this, but can’t justify doing it for myself since I have no real use for it.</em></pre>



<p class="wp-block-paragraph">That &#8220;itch&#8221; is incredibly real. There is a specific kind of developer joy in designing a sprawling, beautifully engineered machine just to watch the gears turn perfectly. It’s the digital equivalent of building a highly detailed model railway in the basement—you don&#8217;t build it because you need a tiny train to commute to work; you build it for the sheer mastery of the craft.</p>



<p class="wp-block-paragraph">When you don&#8217;t have a commercial &#8220;use case&#8221; or a client breathing down your neck, the justification changes. It stops being about &#8220;product-market fit&#8221; and becomes about&nbsp;<strong>technical play</strong>.</p>



<p class="wp-block-paragraph">If you look at it through that lens, building a platform like this is actually the ultimate sandbox. It lets you:</p>



<ul class="wp-block-list">
<li><strong>Test extreme architectures:</strong> You can try out patterns that a risk-averse company would never let you deploy—like using a compiled, zero-dependency Swift/Vapor binary as a lightning-fast API, or using a flat-file markdown system with an SQLite database acting as an ultra-high-speed, read-only cache.</li>



<li><strong>Build a reusable blueprint:</strong> If you design it with strict modularity, you aren&#8217;t just building a single website. You’re building a personal, battle-tested framework. The next time you (or a friend, or a client) need a fast blog, a portfolio, a newsletter manager, or a podcast feed, you can spin up a lightweight, highly optimized instance of your own engine in minutes rather than reaching for a bloated, dependency-heavy third-party tool.</li>
</ul>



<p class="wp-block-paragraph">Sometimes, the best justification for building something is simply that you want to see if you can make it run faster, cleaner, and more elegantly than anything else currently out there.</p>



<p class="wp-block-paragraph">If you did let yourself build even just a small, highly optimized slice of this system purely for the intellectual fun of it, which specific architectural piece would you want to tackle first?</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<pre class="wp-block-preformatted"><strong>Prompt:</strong> <em>Back to the original question. How would your recommendation change if the project were for a major news organization like BBC?</em></pre>



<p class="wp-block-paragraph">For an organization at the scale of the BBC, the architectural requirements shift dramatically. You are no longer just optimizing for CPU cycles or memory footprints on a single server; you are optimizing for&nbsp;<strong>global high availability, extreme read-to-write ratios, talent pool hiring, and disaster recovery.</strong></p>



<p class="wp-block-paragraph">A breaking news event can instantly drive millions of concurrent users to the platform. If the system fails for even five minutes, it is national news.</p>



<p class="wp-block-paragraph">Here is how the recommendations and the wider architecture would change for an enterprise news organization of this scale.</p>



<h2 class="wp-block-heading">1. The Technology Stack Shift</h2>



<p class="wp-block-paragraph">At BBC scale, a single monolithic backend is replaced by a&nbsp;<strong>decoupled, microservices, or federated API architecture</strong>.</p>



<pre class="wp-block-code"><code>                           &#91; Edge CDN (Fastly / Cloudflare) ]
                                          |
                        &#91; BFF / API Gateway (Node.js / GraphQL) ]
                                          |
         +--------------------------------+-------------------------------+
         |                                |                               |
&#91; Editorial CMS ]                &#91; Content Delivery Service ]    &#91; Media &amp; Transcoding ]
 (.NET / Java / Go)                   (Go / Node.js)               (AWS Elemental / S3)
</code></pre>



<h3 class="wp-block-heading">The Backend Tier: TypeScript &amp; Go/Java/ .NET</h3>



<ul class="wp-block-list">
<li><strong>The Winner for Delivery:</strong> <strong>TypeScript (Node.js)</strong> remains highly recommended, but specifically for the <strong>BFF (Backend-for-Frontend)</strong> or API Gateway layer. The BBC actually uses Node.js heavily for its web rendering tier because of its asynchronous I/O performance and the massive talent pool available in the UK and globally.</li>



<li><strong>The Winner for Core Services &amp; Workflows:</strong> For the heavy-lifting core services (like user permission management, editorial workflows, and localization engines), <strong>.NET</strong>, <strong>Java</strong>, or <strong>Go</strong> become the standards. They offer robust typing, enterprise-grade stability, and seamless integration with legacy newsroom systems (like ENPS or AP ENPS).</li>



<li><strong>The Swift + Vapor Verdict:</strong> <strong>Ruled out.</strong> While technically brilliant, a major news organization cannot risk building core infrastructure on a niche server-side technology. The talent pool of server-side Swift developers is too small to sustain round-the-clock global engineering teams.</li>
</ul>



<h2 class="wp-block-heading">2. The Database &amp; Storage Strategy</h2>



<p class="wp-block-paragraph">The simple PostgreSQL database and SQLite cache model must scale out to handle massive write distribution and global read replication.</p>



<ul class="wp-block-list">
<li><strong>Primary Database:</strong> A globally distributed database cluster. Instead of standard PostgreSQL, you would use <strong>Amazon Aurora PostgreSQL</strong> (with multi-region read replicas) or a distributed SQL database like <strong>Google Cloud Spanner</strong> or <strong>CockroachDB</strong>. This ensures that if an entire AWS region goes dark, the CMS and public site remain online.</li>



<li><strong>Content Store (NoSQL):</strong> Article body content and historical archives (which can span decades and millions of posts) are often stored in document stores like <strong>MongoDB</strong> or <strong>DynamoDB</strong> because of their horizontal scaling capabilities and flexible schemas for different content types (live blogs, photo galleries, long-form articles).</li>
</ul>



<h2 class="wp-block-heading">3. Caching: The Real Savior (Edge Caching)</h2>



<p class="wp-block-paragraph">At this scale,&nbsp;<strong>99.9% of the public-facing read traffic must never hit your origin servers.</strong></p>



<ul class="wp-block-list">
<li><strong>The Edge CDN:</strong> You would use enterprise-tier CDNs like <strong>Fastly</strong> or <strong>Cloudflare</strong> with instant purging capabilities.</li>



<li><strong>Surrogate Keys / Cache Tagging:</strong> When an editor updates a breaking news article, the CMS sends a purge request to the CDN using a unique surrogate key (e.g., <code>article-12345</code>). Within milliseconds, that specific page is invalidated globally across all edge nodes, while the rest of the site remains cached.</li>



<li><strong>Stale-While-Revalidate:</strong> The CDN is configured to serve stale content to users for a few seconds while fetching the freshly updated article in the background. This prevents &#8220;cache stampedes&#8221; (where thousands of simultaneous requests hit the database at the exact moment a cache expires).</li>
</ul>



<h2 class="wp-block-heading">4. Media, Video, and Podcasts</h2>



<p class="wp-block-paragraph">At BBC scale, you cannot host or transcode video files yourself on a standard server block.</p>



<ul class="wp-block-list">
<li><strong>Transcoding Pipelines:</strong> Raw video and audio uploaded by journalists must be pushed to a dedicated pipeline like <strong>AWS Elemental MediaConvert</strong>. It automatically packages the video into multiple bitrates and formats (HLS/DASH) for seamless playback on everything from low-bandwidth mobile devices in rural areas to 4K smart TVs.</li>



<li><strong>Multi-CDN Strategy:</strong> To deliver video and podcast files globally without buffering during high-traffic events, you distribute the bandwidth load across multiple CDN providers (e.g., Akamai, Cloudflare, and Fastly) dynamically based on the user&#8217;s geographic performance.</li>
</ul>



<h2 class="wp-block-heading">5. Security &amp; RBAC (Role-Based Access Control)</h2>



<p class="wp-block-paragraph">Security changes from a simple database table to a compliance-heavy identity provider.</p>



<ul class="wp-block-list">
<li><strong>Identity &amp; Access Management (IAM):</strong> Rather than handling passwords in-house, the CMS integrates with enterprise identity providers (like <strong>Okta</strong> or <strong>Ping Identity</strong>) via SAML/OIDC.</li>



<li><strong>Audit Trails:</strong> For a major news outlet, an unauthorized post can cause geopolitical or financial chaos. The system requires cryptographic audit logs tracking exactly who wrote, edited, approved, and published every single character.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<pre class="wp-block-preformatted"><strong>Prompt:</strong> <em>And how would the architecture change for a small publication with only about 1000-3000 views per day?</em></pre>



<p class="wp-block-paragraph">For a small publication pulling in 1,000 to 3,000 views per day, the architectural priorities flip completely. At this scale, you are no longer architecting for global redundancy or massive concurrent spikes. Instead, your primary design goals are:</p>



<ol class="wp-block-list">
<li><strong>Extreme cost efficiency</strong> (ideally keeping hosting under $10–$20/month, or even entirely free).</li>



<li><strong>Minimal maintenance overhead</strong> (no complex Kubernetes clusters, microservices, or multi-node databases to monitor).</li>



<li><strong>Simplicity and longevity</strong> (the system should be able to run for years without touching it).</li>
</ol>



<p class="wp-block-paragraph">Here is how the architecture collapses into a highly optimized, elegant, and low-cost setup.</p>



<h2 class="wp-block-heading">1. The Technology Stack Shift</h2>



<p class="wp-block-paragraph">At 3,000 views a day, a single user is visiting the site roughly once every 30 seconds on average. Any modern CPU can handle this load in its sleep.</p>



<h3 class="wp-block-heading">The Backend Tier: Swift + Vapor or PHP (Vanilla/Lightweight)</h3>



<ul class="wp-block-list">
<li><strong>The Winner for Performance &amp; Fun:</strong> <strong>Swift + Vapor</strong>. Because Vapor compiles to a single, highly efficient native binary, it uses an incredibly small memory footprint (often under 30–50MB of RAM at idle). You can easily run the entire application, database, and cache on the cheapest $4–$5/month VPS (like a shared-CPU Hetzner or DigitalOcean droplet) with resources to spare.</li>



<li><strong>The Winner for Simplicity:</strong> <strong>Vanilla PHP</strong> or a micro-framework. It is virtually zero-maintenance, runs on any cheap hosting provider, and has zero compile steps.</li>



<li><strong>TypeScript &amp; .NET Verdict:</strong> Node.js/TypeScript is still fine, but its memory footprint is higher than Swift. .NET is massive overkill and will require a larger, more expensive VPS just to keep the runtime comfortable.</li>
</ul>



<h2 class="wp-block-heading">2. The Storage &amp; Database Strategy</h2>



<p class="wp-block-paragraph">You do not need dedicated, managed database instances.</p>



<ul class="wp-block-list">
<li><strong>The Database: SQLite</strong>
<ul class="wp-block-list">
<li><strong>Why:</strong> For 3,000 views a day, <strong>SQLite is the absolute king</strong>. Modern SQLite handles concurrent reads incredibly well (especially in WAL mode), requires zero server setup, and exists as a single file on disk. This makes backups as simple as copying the database file to an S3 bucket once a day.</li>



<li>You do not need PostgreSQL, MySQL, or Mongo. SQLite can easily handle millions of rows and a few thousand requests a day without breaking a sweat.</li>
</ul>
</li>
</ul>



<h2 class="wp-block-heading">3. Caching &amp; Frontend: Static Generation (SSG) or Edge-Cached SSR</h2>



<p class="wp-block-paragraph">The absolute best architecture for a small publication is to&nbsp;<strong>decouple the CMS from the frontend entirely.</strong></p>



<pre class="wp-block-code"><code>&#91; Writer / Admin ] ---&gt; &#91; CMS (Vapor or PHP + SQLite) ] 
                               |
                   (Generates static HTML / RSS)
                               |
                               v
               &#91; Cloudflare Pages / Vercel (Free) ] &lt;---&gt; &#91; Reader ]
</code></pre>



<h3 class="wp-block-heading">Option A: Static Site Generation (SSG) —&nbsp;<em>Highly Recommended</em></h3>



<ul class="wp-block-list">
<li>Your CMS is just a private, password-protected backend.</li>



<li>When you publish a post, the backend compiles the website into static HTML, CSS, and RSS files and pushes them to a free static hosting service like <strong>Cloudflare Pages</strong>, <strong>GitHub Pages</strong>, or <strong>Vercel</strong>.</li>



<li><strong>The Result:</strong> Your public site is virtually un-hackable, loads instantly worldwide, has <strong>zero server hosting costs</strong>, and can handle a sudden spike of 1,000,000 users without you paying a single extra penny.</li>
</ul>



<h3 class="wp-block-heading">Option B: Server-Side Rendering (SSR) with SQLite Cache</h3>



<ul class="wp-block-list">
<li>If you want dynamic features like reader comments, you run your Swift/Vapor or PHP app on a cheap $4 VPS.</li>



<li><strong>The Cache:</strong> You use a simple in-memory cache (like Vapor&#8217;s built-in memory cache) or build a custom, ultra-fast read-cache using a secondary, memory-mapped SQLite database.</li>



<li><strong>The CDN:</strong> You put a free <strong>Cloudflare</strong> proxy in front of it. Cloudflare will cache your static pages and assets at the edge, meaning 90%+ of your 3,000 daily views never even hit your $4 VPS.</li>
</ul>



<h2 class="wp-block-heading">4. Media, Newsletters, and Podcasts</h2>



<p class="wp-block-paragraph">At this scale, you want to offload specialized tasks to free or low-cost tiers.</p>



<ul class="wp-block-list">
<li><strong>Media Storage:</strong> Use <strong>Cloudflare R2</strong> or <strong>Backblaze B2</strong> for hosting images, podcast MP3s, and video. Both have incredibly generous free tiers (R2 gives you 10GB of storage and millions of class A/B operations per month for $0).</li>



<li><strong>Newsletters:</strong> Writing a custom bulk-email engine is painful and risks your server&#8217;s IP getting blacklisted. Instead, use your backend to collect subscriber emails in your SQLite database, and use a service like <strong>Mailgun</strong> or <strong>Brevo</strong> (which has a free tier of up to 300 emails/day) to send the newsletters via a simple API call.</li>



<li><strong>Social Auto-Posting:</strong> Instead of writing complex API integrations for every social media platform, your CMS can simply output a clean, standard <strong>RSS feed</strong>. You can then use a free automation tool (like Zapier, Make, or a simple custom cron job script) to watch that feed and post to Mastodon, Bluesky, or LinkedIn when a new item appears.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">My Thoughts on the Conversation</h2>



<p class="wp-block-paragraph">Most of its advice made a lot of sense to me, even if I might implement some of it differently. Not much was new to me in terms of technologies or ideas which, I suppose, shouldn&#8217;t be that surprising. I&#8217;ve never worked on a platform that will see a sudden spike of 1,000,000 users, but I&#8217;ve certainly worked on enough platforms to be familiar with concepts such as edge caching that make such spikes possible without the system crashing.</p>



<p class="wp-block-paragraph">I really do enjoy using AI for conversations like this one. I find it very inspirational and it motivates me to actually sit down and program something, even if I never actually finish it. In this case, I didn&#8217;t really do anything after having this discussion, but I nonetheless enjoy the thought experiment.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/22/geminis-technology-recommendations-for-a-publishing-platform/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12626</post-id>	</item>
		<item>
		<title>A Few New Pages</title>
		<link>https://blog.alexseifert.com/2026/07/20/a-few-new-pages/</link>
					<comments>https://blog.alexseifert.com/2026/07/20/a-few-new-pages/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Mon, 20 Jul 2026 20:37:34 +0000</pubDate>
				<category><![CDATA[News]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[IndieWeb]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12585</guid>

					<description><![CDATA[Not too long ago, I ran into an idea that is gaining traction in the blog/personal website space called slash pages. I decided to create a few of them myself.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Not too long ago, I ran into an idea that is gaining traction in the blog/personal website space called <a href="https://slashpages.net" data-type="link" data-id="https://slashpages.net" target="_blank" rel="noreferrer noopener">slash pages</a>. Essentially, it is a list of standardized URLs for pages that have certain functions and/or contain specific information about the person or website. The idea behind it is that people will automatically know where to find what information. How well that works certainly depends on how familiar the visitor is with slash pages, so milage will vary.</p>



<p class="wp-block-paragraph">Nonetheless, I thought I&#8217;d jump on the bandwagon and create a few of them myself. Most of them I don&#8217;t really see a need to bother with, so I won&#8217;t, but I did create a few I thought would be interesting and/or helpful.</p>



<p class="wp-block-paragraph">Here are the new pages I just created:</p>



<ul class="wp-block-list">
<li><a href="/ai">/ai</a> &#8211; How I use AI.</li>



<li><a href="/blogroll">/blogroll</a> &#8211; A few of the blogs I&#8217;ve subscribed to.</li>



<li><a href="/changelog">/changelog</a> &#8211; Where I document changes made to this website.</li>



<li><a href="/colophon">/colophon</a> &#8211; The technologies that power this website.</li>



<li><a href="/sitemap">/sitemap</a> &#8211; A directory of all the pages on this website.</li>
</ul>



<p class="wp-block-paragraph">I may or may not add more as I go, but that&#8217;s all I felt like doing tonight. The list above is an incomplete list of pages on this website, but you can see all of them on the <a href="/sitemap" data-type="link" data-id="/sitemap" target="_blank" rel="noreferrer noopener">Sitemap page</a> which WordPress automatically generates and keep up to date.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/20/a-few-new-pages/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12585</post-id>	</item>
		<item>
		<title>Conversations I’ve Had with AI</title>
		<link>https://blog.alexseifert.com/2026/07/18/conversations-ive-had-with-ai/</link>
					<comments>https://blog.alexseifert.com/2026/07/18/conversations-ive-had-with-ai/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Sat, 18 Jul 2026 20:48:22 +0000</pubDate>
				<category><![CDATA[News]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Conversations with AI]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12538</guid>

					<description><![CDATA[Lately, I’ve been having a lot of interesting conversations with LLMs about programming and software architecture. I've decided to start posting some of them.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" decoding="async" width="1024" height="559" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=1024%2C559&#038;ssl=1" alt="AI-generated image of a man talking to a robot over tea" class="wp-image-12537" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=1024%2C559&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=400%2C218&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=150%2C82&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=768%2C419&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?w=1408&amp;ssl=1 1408w" sizes="(max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">AI-generated image of a man talking to a robot over tea</figcaption></figure>
</div>


<p class="wp-block-paragraph">Lately, I’ve been having a lot of very interesting conversations with LLMs about a wide range of topics. However, most of them have pertained to technical topics and I thought it would be interesting to post some of them on my blog. I figure there are probably plenty of other people who might be interested in the questions I ask and what AI has to say about it, so why not post them? If nothing else, then at least it’s a way of preserving them for my future self.</p>



<p class="wp-block-paragraph">Most of my recent discussions have been about the pros and cons of various frameworks and programming languages for specific use cases. I’ve also asked about how best to architect specific scenarios such as the massively complex single-page frontend applications used by AWS, Google Cloud Platform and Microsoft Azure. I’ve thoroughly enjoyed the conversations as they’ve been extremely informative and thought-provoking, even if I wouldn’t always necessarily take its advice verbatim.</p>



<p class="wp-block-paragraph">Most of these conversations I’ve had have been with Gemini as I’ve found it to give me the most thought-provoking answers. I’ve also had a few with GitHub Copilot using the Claude Sonnet and Codex models, but due to the recent extreme increase in price, I’ve started saving my tokens for actual programming tasks rather than casual conversations.</p>



<p class="wp-block-paragraph">One of the aspects I enjoy most about these talks with AI is that I find the occasional gem that really helps me out. For example, a couple of days ago, I was discussing the longevity of PHP projects in a classic LAMP stack and mentioned that I was using Apache with mod_php — something I’ve done for nearly three decades. It suggested switching to php-fpm instead in order to boost my websites’ speed. Of course I’d heard of php-fpm, but had never actually used it personally. Changing my server config was a simple task that actually tangibly boosted the speed of all the websites I host on it, including this blog. It was a simple thing I wouldn’t have thought of that had a major impact.</p>



<p class="wp-block-paragraph">In any case though, I’m going to go through some of my recent conversations and copy them into posts on this blog. I’ll start with an introduction to explain a little bit of the background of each conversation and conclude them with my thoughts about it and whether it led to any direct action on my part either at work or in my personal projects. I may edit out parts of the discussions that aren’t strictly relevant as I tend to go off on tangents before eventually telling AI to get back to the original question, but I’ll try to keep them as close to the original conversation as possible.</p>



<p class="wp-block-paragraph">I hope you’ll enjoy them as much as I have and that maybe they’ll even prove inspirational.</p>



<p class="wp-block-paragraph"><em>Just a side note about my blogs: Other than some images (like the one above), I don’t post any AI-generated content that isn’t explicitly marked as such. Blogging is a passion of mine and I feel no need to fluff my post count with articles not written by me. You can read more about how I use AI on the <a href="/ai" data-type="link" data-id="/ai" target="_blank" rel="noreferrer noopener">AI page</a>.</em></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/18/conversations-ive-had-with-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12538</post-id>	</item>
		<item>
		<title>The Bride of Ashbyrn House by Amy Cross</title>
		<link>https://blog.alexseifert.com/2026/07/12/the-bride-of-ashbyrn-house-by-amy-cross/</link>
					<comments>https://blog.alexseifert.com/2026/07/12/the-bride-of-ashbyrn-house-by-amy-cross/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Sun, 12 Jul 2026 12:20:59 +0000</pubDate>
				<category><![CDATA[Books]]></category>
		<category><![CDATA[Featured]]></category>
		<category><![CDATA[Ghosts]]></category>
		<category><![CDATA[Horror]]></category>
		<category><![CDATA[Amy Cross]]></category>
		<category><![CDATA[Book Reviews]]></category>
		<category><![CDATA[Spoiler]]></category>
		<guid isPermaLink="false">https://haunting.alexseifert.com/?p=572</guid>

					<description><![CDATA[Before reading The Bride of Ashbyrn House, I had never read anything by Amy Cross. Not only did the novel impress me, but it quickly became one of my all-time favorites.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=1024%2C768&#038;ssl=1" alt="The Bride of Ashbyrn House by Amy Cross" class="wp-image-577" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">The Bride of Ashbyrn House by Amy Cross</figcaption></figure>
</div>


<p class="wp-block-paragraph">Before reading <em>The Bride of Ashbyrn House</em>, I had never read anything by <a href="https://blog.alexseifert.com/tag/amy-cross/" data-type="post_tag" data-id="904" target="_blank" rel="noreferrer noopener">Amy Cross</a>. Not only did the novel impress me, but it quickly became one of my all-time favorites. That&#8217;s an impressive feat for a new-to-me author to achieve after only a single book. As such, I knew I had to write about it next.</p>



<p class="wp-block-paragraph">The story had everything I love about ghost stories: an old haunted manor, a vengeful ghost, an intriguing story and a sense of thrill that kept the pages turning. This time though, they were digital pages as I read it on my Kindle rather than as a paperback. As of this writing, it isn&#8217;t available in paperback form.</p>



<p class="wp-block-paragraph">Here is the description of the book from&nbsp;<a href="https://goodreads.com/book/show/32714766-the-bride-of-ashbyrn-house" target="_blank" rel="noreferrer noopener">Goodreads</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“I have waited so long for your return.”</p>



<p class="wp-block-paragraph">In the English countryside, miles from the nearest town, there stands an old stone house. Nobody has set foot in the house for years. Nobody has dared. For it is said that even though the lady of the house is long dead, a face can sometimes be seen at one of the windows. A pale, dead face that waits patiently behind a silk wedding veil.Seeking an escape from his life in London, Owen Stone purchases Ashbyrn House without waiting to find out about its history. As far as Owen is concerned, ghosts aren&#8217;t real and his only company in the house will be the thin-legged spiders that lurk on the walls. Even after he moves in, and after he starts hearing strange noises in the night, Owen insists that Ashbyrn House can&#8217;t possibly be haunted. But Owen knows nothing about the ghostly figure that is said to haunt the house. Or about the mysterious church bells that ring out across the lawn at night. Or about the terrible fate that befell the house&#8217;s previous inhabitants when they dared defy the bride. Even as Owen starts to understand the horrific truth about Ashbyrn House&#8217;s past, he might be too late to escape the clutches of the presence that watches his every move. The Bride of Ashbyrn House is a ghost story about a man who believes the past can&#8217;t hurt him, and about a woman whose search for a husband has survived even her own tragic death.</p>
<cite><a href="https://goodreads.com/book/show/32714766-the-bride-of-ashbyrn-house" data-type="link" data-id="https://goodreads.com/book/show/32714766-the-bride-of-ashbyrn-house" target="_blank" rel="noreferrer noopener">Goodreads</a></cite></blockquote>



<p class="wp-block-paragraph">Since I&#8217;m going to freely discuss the story, <strong><em>there will be spoilers!</em></strong> Continue at your own risk!</p>



<p class="wp-block-paragraph">Recently, it feels like there has been an onslaught of horror stories that have to do with dead brides. I seem to see books and movies about them all over the place, but of all of them I have read or seen, <em>The Bride of Ashbyrn House</em> stands out as the best. The story primarily takes place at Ashbyrn House in Cornwall, England. It alternates between the events of 1859 when an upperclass woman named Katinka Ashbyrn is due to marry her fiancé, and the present when a man named Owen buys the allegedly haunted property. The further the story moves along, the clearer the bridge between the two time periods becomes.</p>



<h2 class="wp-block-heading">Katinka</h2>



<p class="wp-block-paragraph">The story begins from Katinka’s perspective. She is watching from the house as her husband comes up to the gates outside, sees her, then turns around and leaves, abandoning her. It immediately leaves the reader with a feeling of unease, a sense of sympathy for Katinka, and many questions. Fortunately, they are answered throughout the story, however, some of them not until the last few pages. The reader has no idea at this point that the opening scene takes place in the present.</p>



<p class="wp-block-paragraph">Most of the story from Katinka’s perspective takes place in 1859, shortly before her planned wedding. She wants it to be absolutely perfect and nothing will stop her from getting what she wants. First and foremost, however, she is to be the picture-perfect centerpiece of it all. In her vanity, she commissions a portrait of herself which arrives shortly before the wedding. For the next several days, much of her time is occupied with sitting and admiring herself in the painting. Eventually, she convinces herself that she appears much more attractive in the picture than in real life which she won’t tolerate. Thus, she begins a strict regiment of self-mutilation to ensure she is skinnier and, in her mind, prettier — the perfect centerpiece for the wedding.</p>



<p class="wp-block-paragraph">At this point in the story, it’s clear that she is a selfish, highly toxic person who doesn’t quite have all of her ducks in a row. Aside from what she does to herself, she practically worships her dead father while belittling and berating her mother and her younger sister, Pippa. Her fiancé is also subjected to torrents of verbal abuse which culminates in her catching him cheating on her in the pantry with Pippa. For that, Katinka murders her sister coldheartedly without so much as batting an eye.</p>



<p class="wp-block-paragraph">Come the day of the wedding, all of the guests have assembled at Ashbyrn house. To make her grand entrance, Katinka has arranged to ride in a coach. However, as the coach rumbles down the rough driveway, tragedy strikes. Katinka, who has used a healthy dose of paraffin — a highly flammable substance — on her skin to make herself appear perfect, is jerked out of her seat and her face comes into contact with the candle lighting the interior of the coach. Instantly catching fire, she stumbles out of the coach screaming for help, but no one comes to her rescue. Instead, they just watch as she jumps into a pond where her weighted dress pulls her down to the bottom where she ultimately drowns. As she is dying, she can see the guests assembling around the pond watching her, but not bothering to help her. She dies unwed.</p>



<h2 class="wp-block-heading">Owen</h2>



<p class="wp-block-paragraph">Fast forward to the present. A man named Owen buys the abandoned Ashbyrn House to escape the hustle of London and focus on his writing. He is told of the story of the ghost bride, but dismisses it as pure fancy. Naturally, he comes to regret that. The ghost of Katinka seems almost cautious at first, but the longer Owen occupies the house, the more confident she becomes and eventually starts playing the “perfect bride.” While he is concentrated on his writing, she brings him food and water and takes care of him in every way. However, it’s as though Owen is in a trance during that time and has no idea about what is actually happen. Not only does he stay blissfully unaware of Katinka’s existence, but he can quite literally sit and write for days without any clue as to how much time has passed.</p>



<p class="wp-block-paragraph">Owen’s ex-fiancé, Vanessa, who is worried about him ignoring all correspondence, eventually travels from London to check on him. Owen still doesn’t realize what has been going on and refuses to leave the house or acknowledge that anything might be off.&nbsp; The more Vanessa tries to convince him, though, the more aggressive Katinka becomes in defending Owen, who, for better or worse, she essentially considers her husband at this point.</p>



<p class="wp-block-paragraph">Eventually, Owen snaps out of it and figures out what is actually happening. He tries to escape Ashbyrn House with Vanessa, but Katinka has other plans and refuses to let them leave the grounds. Owen and Vanessa come to the conclusion that if Owen officially marries Katinka in the ruins of the Ashbyrn’s private church where she was originally supposed to be married, that she might at least let Vanessa go. Katinka seems convinced by the idea and so Vanessa officiates the wedding between Owen and the ghost of Katinka.</p>



<p class="wp-block-paragraph">Shortly thereafter, Vanessa escapes, but Owen stays. Vanessa, however, eventually comes back for Owen. This time, Katinka isn’t having it though and kills her. Owen then manages to escape with Vanessa’s body, never to set foot in Ashbyrn House again.</p>



<p class="wp-block-paragraph">The story ends with a mirrored version of the opening scene. Owen is looking through the locked gate of Ashbyrn House decades later after having served a long prison sentence for the murder of Vanessa. He sees Katinka watching him from the house and decides to leave again without actually entering the property. Despite not unlocking the gate, Owen already had the key to it in his hand which leads the reader to wonder what exactly his intensions were since he very well knows what waits for him beyond the gate.</p>



<h2 class="wp-block-heading">My Conclusion</h2>



<p class="wp-block-paragraph">Owen’s and Katinka’s perspectives alternate as both stories simultaneously lead to their individual, yet related, climaxes. It was an incredibly exciting journey that really kept me turning the digital pages wanting to see how it continued. As it progressed, it was hard to determine which character deserved my sympathy the most. In some ways, despite her somewhat evil nature, I couldn’t help but feel sorry for Katinka. In other ways, it was Vanessa who was the most innocent, yet arguably paid the highest price of them all. For some reason, I could never really feel much sympathy for Owen until the end when the reader finds out he had to spend decades in prison for a murder he didn’t commit.</p>



<p class="wp-block-paragraph">I found the ending to be unnerving in that no one got their comeuppance or was vindicated. Instead, it was the most realistic outcome possible for such a wild, supernatural story. There was no satisfaction or relief for any of the characters whatsoever which made its realism all the more disturbing. What I found excellent about the ending, however, was how it tied in directly to the beginning, just from Owen’s perspective rather than Katinka’s. It also clarified who the anonymous husband actually was and why exactly he chose to just turn around and leave again after seeing her. It was one of the best wrap-ups I’ve seen in a story.</p>



<p class="wp-block-paragraph"><em>The Bride of Ashbyrn House</em> instantly made me a fan of Amy Cross. I&#8217;ve since read two more of her books and I can say with certainty that she has firmly established herself as a staple in my personal library.</p>



<p class="wp-block-paragraph">You can find more about Amy Cross on <a href="https://www.amycross.com/" target="_blank" rel="noreferrer noopener">her website</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/12/the-bride-of-ashbyrn-house-by-amy-cross/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">572</post-id>	</item>
		<item>
		<title>Building a Web App in Swift Using Only SwiftNIO</title>
		<link>https://blog.alexseifert.com/2026/06/29/building-a-web-app-in-swift-using-only-swiftnio/</link>
					<comments>https://blog.alexseifert.com/2026/06/29/building-a-web-app-in-swift-using-only-swiftnio/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Mon, 29 Jun 2026 12:18:27 +0000</pubDate>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[Featured]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Web Development]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12418</guid>

					<description><![CDATA[I’ve been experimenting a lot with Swift for web applications using frameworks like Vapor, but I got curious as to how I could create a low-level, framework-less web application using only SwiftNIO. This is the result.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="559" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=1024%2C559&#038;ssl=1" alt="AI-generated image of a man presenting &quot;Web App Using SwiftNIO&quot; to an audience" class="wp-image-12422" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=1024%2C559&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=400%2C218&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=150%2C82&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=768%2C419&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?w=1408&amp;ssl=1 1408w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">AI-generated image of a man presenting &#8220;Web App Using SwiftNIO&#8221; to an audience</figcaption></figure>
</div>


<p class="wp-block-paragraph">Recently, I’ve been experimenting a lot more with Swift as a language for web applications. I’ve primarily been focusing my efforts on the web framework <a href="https://www.vapor.codes" target="_blank" rel="noreferrer noopener">Vapor</a> which is the most complete and popular web framework for Swift and is used by the likes of some very large projects such as the <a href="https://swiftpackageindex.com" target="_blank" rel="noreferrer noopener">Swift Package Index</a>.</p>



<p class="wp-block-paragraph">The framework is based on <a href="https://github.com/apple/swift-nio" target="_blank" rel="noreferrer noopener">SwiftNIO</a> which is Apple’s open-source “cross-platform asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers &amp; clients.” I also recently read somewhere that Apple itself uses SwiftNIO to power its newer web services as it is low-level and low-latency — both critical aspects considering the sheer enormity of the number of requests that Apple’s web services have to process. Whether or not that is true is something I can’t confirm, but it sounds plausible considering it is an Apple-developed framework that does exactly what a high-performance web service would need.</p>



<p class="wp-block-paragraph">So, naturally, I asked myself what it would take to make my own basic web app using SwiftNIO without any other dependencies beyond the three others that SwiftNIO itself relies on. As it turns out, it doesn’t actually take as much as I thought. Since I wanted to keep this experiment simple, I focused on basic functionality rather than implementing a fully featured web framework. If I wanted that, I would just go with Vapor.</p>



<p class="wp-block-paragraph">Before we start, I want to mention that I pushed the entire experiment to <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless" target="_blank" rel="noreferrer noopener">GitHub</a> with thoroughly commented code so that you can see what I did and/or play around with it yourself. There will be several links to various parts of the code on GitHub through the post.</p>



<h2 class="wp-block-heading"><strong>Setting Up the Server</strong></h2>



<p class="wp-block-paragraph">The first, and arguably most critical, feature any web application needs is a server. In this case, one that uses SwiftNIO for network communication. In order to accomplish that, I created two classes: <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L166" target="_blank" rel="noreferrer noopener">HTTPServer</a> and <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L28" target="_blank" rel="noreferrer noopener">HTTPHandler</a>. As the names might suggest, the first sets up the server and network infrastructure while the latter handles each individual connection.</p>



<p class="wp-block-paragraph">The <code>HTTPServer</code> class includes a method called <code>start()</code> which uses SwiftNIO’s <a href="https://swiftpackageindex.com/apple/swift-nio/main/documentation/nioposix/serverbootstrap" target="_blank" rel="noreferrer noopener">ServerBootstrap</a> to bind a TCP port and configure how connections are handled:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">public func start() async throws {
    let router = self.router

    let bootstrap = ServerBootstrap(group: group)
        // How many pending connections the OS kernel should queue before the
        // application has a chance to accept them.
        .serverChannelOption(ChannelOptions.backlog, value: 256)
        // Allow the port to be reused immediately after the process restarts,
        // without waiting for the OS TIME_WAIT period to expire.
        .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
        // For each accepted connection, configure the NIO pipeline:
        //   1. Built-in HTTP/1.1 encoder + decoder
        //   2. Our custom HTTPHandler for routing
        .childChannelInitializer { channel in
            channel.pipeline.configureHTTPServerPipeline().flatMap {
                channel.pipeline.addHandler(HTTPHandler(router: router))
            }
        }

    let channel = try await bootstrap.bind(host: host, port: port).get()
    print(&quot;Server running on http://\(host):\(port)&quot;)

    // Suspend here until the channel is closed. In normal operation this never
    // returns — the server runs until the process is killed.
    try await channel.closeFuture.get()
}
</code></pre>
</div>



<p class="wp-block-paragraph">That’s all that the <code>HTTPServer</code> class actually does. The rest happens in the <code>HTTPHandler</code> class which, as you may have noticed is added as a handler to the channel pipeline.</p>



<p class="wp-block-paragraph">For each incoming request, a new <code>HTTPHandler</code> instance is created and is responsible for four different tasks:</p>



<ol class="wp-block-list">
<li>Receiving request parts</li>



<li>Bridging to Swift concurrency via Task</li>



<li>Writing the response</li>



<li>Closing the connection</li>
</ol>



<p class="wp-block-paragraph">This is the lifecycle of a single request:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">Client connects
        ↓
ServerBootstrap accepts → creates HTTPHandler
        ↓
SwiftNIO fires channelRead(.head)  → store head
SwiftNIO fires channelRead(.body)  → accumulate bytes  (0 or more times)
SwiftNIO fires channelRead(.end)   → dispatch to router via Task
        ↓
Router matches route → calls handler → returns HTTPResponse
        ↓
eventLoop.execute → write response head + body + end
        ↓
Connection closed
</code></pre>
</div>



<h3 class="wp-block-heading">Receiving Request Parts</h3>



<p class="wp-block-paragraph">The <code>HTTPHandler</code> class implements SwiftNIO’s <code>ChannelInboundHandler</code> protocol which expects, among other things, the function <code>channelRead()</code>. This function is called multiple times per request and a value identifying which part of the request is currently being handled is passed via one of three different enums: <code>.head</code>, <code>.body</code> and <code>.end</code>. With these, we can determine our course of action and how to handle the data being sent.</p>



<p class="wp-block-paragraph">One gotcha, however, is that SwiftNIO breaks down large bodies into chunks which means there is the potential for multiple <code>.body</code> parts. I go into more detail about that in the section below where I describe how I implemented the RESTful methods.</p>



<h3 class="wp-block-heading">Bridging to Swift Concurrency via Task</h3>



<p class="wp-block-paragraph">SwiftNIO operates with event loops which, like when working with Node.js, should never be blocked. As such, we need to ensure that route handlers are executed asynchronously. I implemented that by stuffing them inside a Swift <code>Task</code> when SwiftNIO passes <code>.end</code>:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">Task { [head = head] in
    let response = await router.handle(head: head, body: body)
    // ↑ runs on Swift's cooperative thread pool, not the NIO thread
    
    eventLoop.execute {
        self.write(response: response, context: context)
        // ↑ hop back onto the NIO event loop to write the response
    }
}
</code></pre>
</div>



<h3 class="wp-block-heading">Writing The Response</h3>



<p class="wp-block-paragraph">The <code>HTTPHandler</code> class is also responsible for serializing the <code>HTTPResponse</code> object returned by the routes (more on that below) into SwiftNIO’s three-part format which includes:</p>



<ul class="wp-block-list">
<li>HTTPResponseHead (status &amp; headers)</li>



<li>ByteBuffer (body bytes)</li>



<li>End marker (signals that the response is complete)</li>
</ul>



<p class="wp-block-paragraph">This logic takes place in the <code>write()</code> function (<a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L127" target="_blank" rel="noreferrer noopener">see on GitHub</a>):</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">private func write(response: HTTPResponse, context: ChannelHandlerContext) {
    // Allocate a NIO `ByteBuffer` sized to the response body and copy the bytes in.
    var buffer = context.channel.allocator.buffer(capacity: response.bodyData.count)
    buffer.writeBytes(response.bodyData)

    var headers = HTTPHeaders()
    headers.add(name: &quot;Content-Type&quot;, value: response.contentType)
    // Telling the client the exact byte count avoids chunked transfer encoding
    // and lets browsers display progress correctly.
    headers.add(name: &quot;Content-Length&quot;, value: &quot;\(buffer.readableBytes)&quot;)
    headers.add(name: &quot;Connection&quot;, value: &quot;close&quot;)

    let responseHead = HTTPResponseHead(version: .http1_1, status: response.status, headers: headers)

    // Write all three parts. The first two use `write` (buffered); the last uses
    // `writeAndFlush` to push everything to the network in one syscall.
    context.write(wrapOutboundOut(.head(responseHead)), promise: nil)
    context.write(wrapOutboundOut(.body(.byteBuffer(buffer))), promise: nil)

    let channel = context.channel
    context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { _ in
        // Close the TCP connection once all bytes have been sent.
        channel.close(promise: nil)
    }
}
</code></pre>
</div>



<p class="wp-block-paragraph"><code>HTTPResponse</code> is a <code>Sendable</code> struct I created that contains the response HTTP status, the content type and the body data as a Swift <code>Data</code> object. You can view it on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/Router.swift#L16" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Closing The Connection</h3>



<p class="wp-block-paragraph">The <code>write()</code> function above also contains the code that closes the connection after every response. This is the bit of code responsible for doing that:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">let channel = context.channel
context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { _ in
    // Close the TCP connection once all bytes have been sent.
    channel.close(promise: nil)
}
</code></pre>
</div>



<h3 class="wp-block-heading">Starting the Server</h3>



<p class="wp-block-paragraph">Now that we have everything in place, all that is left is to instantiate the <code>HTTPServer</code> class and call the <code>start()</code> function at the end of the <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/main.swift" target="_blank" rel="noreferrer noopener">main.swift</a> file to start the server:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">let server = HTTPServer(host: &quot;127.0.0.1&quot;, port: 8080, router: router)
try await server.start()
</code></pre>
</div>



<p class="wp-block-paragraph">Now that we’ve looked at how the custom server works, let’s see what else the application can do using the server.</p>



<h2 class="wp-block-heading">What This Basic Web App Can Do</h2>



<p class="wp-block-paragraph">I’ve mentioned several times already that the web app I made is basic and simple. It does, however, contain everything necessary for a basic API application or even a web application with light templating needs. Here is an overview:</p>



<ul class="wp-block-list">
<li>RESTful methods</li>



<li>Routing</li>



<li>Basic templating with XSS safety</li>



<li>Static content</li>



<li>HTTP/1.1 support</li>
</ul>



<h3 class="wp-block-heading">RESTful Methods</h3>



<p class="wp-block-paragraph">First off, the application includes support for routes with all RESTful methods (GET, POST, PUT, PATCH, DELETE) including body and header parsing. Initially, I wasn’t going to include support for anything but GET, but SwiftNIO made it surprisingly trivial to include all of them.</p>



<p class="wp-block-paragraph">Supporting body parsing was a bit trickier, but also wasn’t all that difficult once I figured out how SwiftNIO actually handles requests. It doesn’t actually ever give you a complete HTTP request in one go. Instead, it streams it piece by piece. These can be identified via enums like this: <code>.head  →  .body  →  .body  →  ...  →  .end</code>. As you can see though, the <code>.body</code> case can arrive multiple times for large payloads since SwiftNIO breaks it down into chunks. That made it a bit trickier to solve, but also not terrible. I just had to accumulate the chunks and then put them together at the end of the request:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">private var bodyBuffer: ByteBuffer?

// ...

case .body(var buffer):
    // Accumulate body chunks. For small bodies this will be a single call;
    // for larger bodies NIO may deliver multiple chunks.
    if bodyBuffer == nil {
        bodyBuffer = buffer
    } else {
        // Append the new chunk to the existing buffer.
        bodyBuffer!.writeBuffer(&amp;buffer)
    }

case .end:
    if var buf = bodyBuffer, let bytes = buf.readBytes(length: buf.readableBytes) {
        body = Data(bytes)   // ByteBuffer → [UInt8] → Data
    } else {
        body = Data()        // no body (GET, HEAD, etc.)
    }
</code></pre>
</div>



<p class="wp-block-paragraph">The code above is heavily cherry-picked. You can see the whole context on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L71" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Routing</h3>



<p class="wp-block-paragraph">Of course, no web application would be worth anything if there wasn’t some form of routing. To accomplish this, I created a <code>Router</code> class that is responsible for registering all routes with their respective methods and handler functions, parsing the incoming URLs and calling said handler functions. The latter always return an <code>HTTPResponse</code> object as described above. I figured the best way to do this would be the regex-route (pun intended) which can be seen in the <code>add()</code> function of the <code>Router</code> class:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">public func add(_ method: HTTPMethod, _ path: String, _ handler: @escaping Handler) {
    var paramNames: [String] = []

    // Replace each `:paramName` segment with a regex capture group `([^/]+)`,
    // and record the parameter name so we can map captures back to names later.
    let regexString = path.replacing(/:([a-zA-Z0-9_]+)/) { match in
        paramNames.append(String(match.output.1))
        return &quot;([^/]+)&quot;
    }

    guard let regex = try? Regex(&quot;^&quot; + regexString + &quot;$&quot;) else { return }
    lock.withLock { routes.append(Route(method: method, regex: regex, paramNames: paramNames, handler: handler)) }
} 
</code></pre>
</div>



<p class="wp-block-paragraph">Then all you have to do is add your routes and handlers in the <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/main.swift" target="_blank" rel="noreferrer noopener">main.swift</a> file:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">router.add(.GET, &quot;/&quot;) { head, _, _, _ in
    let pageHtml = layoutView(title: &quot;Homepage&quot;, content: homeView())
    return HTTPResponse(status: .ok, contentType: &quot;text/html&quot;, body: pageHtml.description)
}

router.add(.GET, &quot;/hello/:name&quot;) { head, params, urlComponents, _ in
    let name = params[&quot;name&quot;] ?? &quot;unknown&quot;

    // Extract the optional `?german=true` query parameter.
    let germanParam = urlComponents.queryItems?.first(where: { $0.name == &quot;german&quot; })?.value
    let isGerman = germanParam == &quot;true&quot;

    let title = isGerman ? &quot;Hallo \(name)!&quot; : &quot;Hello \(name)!&quot;
    let pageHtml = layoutView(title: title, content: helloView(name: name, isGerman: isGerman), name: name)

    return HTTPResponse(status: .ok, contentType: &quot;text/html&quot;, body: pageHtml.description)
}
</code></pre>
</div>



<p class="wp-block-paragraph">You might have noticed in the example that parameterization of URLs is also supported. This includes both inline parameters (i.e. <code>/hello/:name</code>) as well as query parameters (i.e. <code>/hello/:name?german=true</code>). Inline parameters are parsed using the regex above and query parameters are extracted from the <code>urlComponents</code> variable injected into the handler function.</p>



<p class="wp-block-paragraph">You can find the entire <code>Router</code> class on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/Router.swift" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Basic Templating with XSS Safety</h3>



<p class="wp-block-paragraph">There were a number of approaches I considered when it came to adding templating to the project. My first instinct was, of course, to reach for one of the off-the-shelf solutions such as <a href="https://docs.vapor.codes/leaf/overview/" target="_blank" rel="noreferrer noopener">Vapor’s Leaf</a>, but I wanted to avoid extra dependencies and keep it as simple as possible. As such, I just decided on simple string interpolation where each template is a standard Swift function that returns a string of HTML. That may not be the best approach for a large web application with a complex frontend, but works perfectly well for this experiment.</p>



<p class="wp-block-paragraph">Fortunately, that also means it’s fairly straightforward. There is a layout view that provides the structure for the site as well as a specific view for each route that should return HTML.</p>



<p class="wp-block-paragraph">The layout view:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">func layoutView(title: String, content: HTML, name: String = &quot;&quot;) -&gt; HTML {
    return &quot;&quot;&quot;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
        &lt;head&gt;&lt;title&gt;\(title)&lt;/title&gt;&lt;/head&gt;
        &lt;link rel=&quot;stylesheet&quot; href=&quot;/css/main.css&quot;&gt;
        &lt;script src=&quot;/js/scripts.js&quot;&gt;&lt;/script&gt;
        &lt;body&gt;
            &lt;nav&gt;
                &lt;a href=&quot;/&quot;&gt;Home&lt;/a&gt;
            &lt;/nav&gt;
            &lt;main&gt;\(content)&lt;/main&gt;
            &lt;p&gt;Input your name: &lt;input type=&quot;text&quot; id=&quot;name&quot; value=&quot;\(name)&quot;&gt;&lt;/p&gt;
            &lt;div class=&quot;buttons&quot;&gt;
                &lt;button type=&quot;button&quot; id=&quot;sayHelloButton&quot;&gt;Say Hello!&lt;/button&gt;
                &lt;button type=&quot;button&quot; id=&quot;sayHelloGermanButton&quot;&gt;Say Hello in German!&lt;/button&gt;
            &lt;/div&gt;
        &lt;/body&gt;
    &lt;/html&gt;
    &quot;&quot;&quot;
}
</code></pre>
</div>



<p class="wp-block-paragraph">The <code>/hello/:name</code> route’s view:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">func helloView(name: String, isGerman: Bool = false) -&gt; HTML {
    let greeting = isGerman ? &quot;Hallo \(name)!&quot; : &quot;Hello \(name)!&quot;
    return &quot;&lt;h1&gt;\(greeting)&lt;/h1&gt;&quot;
}
</code></pre>
</div>



<p class="wp-block-paragraph">The layout and route-specific views get put together in each route’s handler. If you look back at the previous examples of adding routes, you’ll see the <code>layoutView()</code> function takes a content parameter. This is where the route-specific view is injected into the layout.</p>



<p class="wp-block-paragraph">You might have also noticed the mysterious <code>HTML</code> type. This doesn’t come from SwiftNIO, but instead is a custom interpolation struct that provides XSS (cross-site scripting) safety. While a little excessive for this particular basic project, I happened to find out while making it that Swift supports such a thing and decided to give it a shot. I won’t go into detail about it here, but you can view the commented struct on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/HTML.swift" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Static Content</h3>



<p class="wp-block-paragraph">If you have templates, you have to have static content as well. What modern website doesn’t have CSS and JavaScript? Even my basic example does, but more as a proof of concept than anything else.</p>



<p class="wp-block-paragraph">Static files are located in the <code>Public</code> folder at the root of the project. The application will automatically figure out the path to it relative to the executable and then return the requested file via a wildcard GET route added to the router after all of the user-defined routes have been added. If no file is found, a 404 “not found” error is returned.</p>



<p class="wp-block-paragraph">This was surprisingly one of the trickier parts of the whole experiment to figure out, but once I had it, it wasn’t terrible to implement. Essentially, I had to read the static file as a Swift <code>Data</code> object and pass it along with the <code>mimeType</code> to the <code>HTTPResponse</code> to be handled in the same way it handles a standard string response:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">guard let data = try? Data(contentsOf: URL(fileURLWithPath: fullPath)) else {
    return HTTPResponse(status: .notFound, body: &quot;Not Found&quot;)
}

let ext = (fullPath as NSString).pathExtension
return HTTPResponse(status: .ok, contentType: mimeType(for: ext), data: data)
</code></pre>
</div>



<p class="wp-block-paragraph">There is a little bit of magic that also happens in there in the form of path sanitization so that a user can’t use standard Unix/Linux paths like &#8220;/..&#8221; to reach other files in the OS:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">let sanitized = filePath
    .components(separatedBy: &quot;/&quot;)
    .filter { !$0.isEmpty &amp;&amp; $0 != &quot;..&quot; }
    .joined(separator: &quot;/&quot;)

guard !sanitized.isEmpty else {
    return HTTPResponse(status: .notFound, body: &quot;Not Found&quot;)
}
</code></pre>
</div>



<p class="wp-block-paragraph">You can see the wildcard route and how the files are read on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/Router.swift#L121" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Only HTTP/1.1 is Supported</h3>



<p class="wp-block-paragraph">One notable limitation of the web application in its current form is that it only supports HTTP/1.1. SwiftNIO also has additional Apple-developed libraries that extend it to support HTTP/2 (<a href="https://github.com/apple/swift-nio-http2" target="_blank" rel="noreferrer noopener">swift-nio-http2</a>) and even an experimental library to allow it to support HTTP/3 (<a href="https://github.com/apple/swift-nio-http3" target="_blank" rel="noreferrer noopener">swift-nio-http3</a>), but I figured sticking with HTTP/1.1 would be good enough for this small experiment.</p>



<h2 class="wp-block-heading">What Else It Could Have Done</h2>



<p class="wp-block-paragraph">I enjoyed creating this project enough that it was hard to keep it simple. I could have just kept tacking on feature after feature until it did everything a standard web application can do, but I was (fairly) disciplined and kept it basic. These are some of the ways I thought it might be nice to expand it and, perhaps, I will fire up a new repo one day to do so.</p>



<p class="wp-block-paragraph">As mentioned above, the server currently only supports HTTP/1.1. It would probably be fairly trivial to add at least support for HTTP/2 with <a href="https://github.com/apple/swift-nio-http2" target="_blank" rel="noreferrer noopener">swift-nio-http2</a>, but I haven’t attempted that yet.</p>



<p class="wp-block-paragraph">Also, a proper templating engine might be useful for a real application. I’ve worked with <a href="https://docs.vapor.codes/leaf/overview/" target="_blank" rel="noreferrer noopener">Vapor’s Leaf</a>, but have also dabbled with <a href="https://github.com/stencilproject/Stencil" target="_blank" rel="noreferrer noopener">Stencil</a> which is another template language for Swift. Either one would be a good choice.</p>



<p class="wp-block-paragraph">Then, of course, rare is the web app that doesn’t have a database behind it. The easiest solution would be to statically link to the operating system’s C SQLite library and call it using raw queries directly in your Swift code. For other databases such as MySQL and PostgreSQL, the makers of Vapor also provide NIO-libraries: <a href="https://github.com/vapor/mysql-nio" target="_blank" rel="noreferrer noopener">MySQLNIO</a> and <a href="https://github.com/vapor/postgres-nio" target="_blank" rel="noreferrer noopener">PostgresNIO</a>. If you would prefer a full-fledged ORM, then <a href="https://docs.vapor.codes/fluent/overview/" target="_blank" rel="noreferrer noopener">Vapor’s Fluent</a> is probably the way to go.</p>



<p class="wp-block-paragraph">However, if you’ve already got Leaf and Fluent, then you might as well save yourself the trouble of everything else and just use <a href="https://www.vapor.codes" target="_blank" rel="noreferrer noopener">Vapor</a> itself which, as mentioned above, is also based on SwiftNIO.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">So, would I ever use this on an actual production system? Honestly, probably not. For the vast majority of projects, a complete web framework like Vapor makes a lot more sense because it makes development so much faster and easier with much less code to maintain yourself. The performance penalty is so negligible that you probably wouldn’t be able to measure the difference in anything but nanoseconds.</p>



<p class="wp-block-paragraph">However, if you are serving requests at the same scale as Apple, then every nanosecond counts and you might want to consider going with raw SwiftNIO. The margin for error is wide, but the ability to finely tweak performance is vast. Since no project I have ever worked on — professionally or personally — has ever even come close to receiving the amount of requests an Apple service does, I honestly would always go with a framework like Vapor.</p>



<p class="wp-block-paragraph">That said, I did enjoy the experiment and it was interesting to piece together how it works behind the scenes. I could have easily just kept bolting on more features until I had my own mini framework, but I decided to just keep it simple and focus on the core of the application. After all, the point of the experiment was to figure out how it worked and if I could even make it work at all. Fortunately, I was successful in both endeavors.</p>



<p class="wp-block-paragraph">And just as a small aside: The README file in the <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless" target="_blank" rel="noreferrer noopener">GitHub repository</a> has a few more technical details than I’ve included here. There are also instructions on how to get it to run and build on Mac (with and without Xcode) as well as Linux. It will only work on Windows with WSL since SwiftNIO relies on POSIX calls for its thread-pooling.</p>



<h2 class="wp-block-heading">Links</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless" target="_blank" rel="noreferrer noopener">GitHub repository with my experiment</a></li>



<li><a href="https://github.com/apple/swift-nio" target="_blank" rel="noreferrer noopener">SwiftNIO</a></li>



<li><a href="https://www.vapor.codes" target="_blank" rel="noreferrer noopener">Vapor</a></li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/06/29/building-a-web-app-in-swift-using-only-swiftnio/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12418</post-id>	</item>
		<item>
		<title>ScratchBook 2.2 Released</title>
		<link>https://blog.alexseifert.com/2026/06/15/scratchbook-2-2-released/</link>
					<comments>https://blog.alexseifert.com/2026/06/15/scratchbook-2-2-released/#comments</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Mon, 15 Jun 2026 12:09:42 +0000</pubDate>
				<category><![CDATA[Haunted House Software]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[ScratchBook]]></category>
		<category><![CDATA[ScratchPad]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12400</guid>

					<description><![CDATA[ScratchBook 2.2 has just been released in the Mac App Store with bugfixes and a new menu item to reset text formatting to the default.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">ScratchBook 2.2 has just been released in the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>!</p>



<p class="wp-block-paragraph">Changes include:</p>



<ul class="wp-block-list">
<li>Added a new menu item to reset the selected text&#8217;s or an entire note&#8217;s formatting. You can find it under Format -> Reset Formatting</li>



<li>Fixed a bug where using cmd+option+shift+v didn&#8217;t paste anything</li>
</ul>



<p class="wp-block-paragraph">You can download ScratchBook for free on the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel=" noreferrer noopener"><img data-recalc-dims="1" loading="lazy" decoding="async" width="156" height="40" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=156%2C40&#038;ssl=1" alt="Download on the Mac App Store" class="wp-image-12140" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?w=156&amp;ssl=1 156w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=150%2C38&amp;ssl=1 150w" sizes="auto, (max-width: 156px) 100vw, 156px" /></a></figure>
</div>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/06/15/scratchbook-2-2-released/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12400</post-id>	</item>
		<item>
		<title>The Relaunch of The Old West and Why I Chose Vanilla PHP</title>
		<link>https://blog.alexseifert.com/2026/06/02/the-relaunch-of-the-old-west-and-why-i-chose-vanilla-php/</link>
					<comments>https://blog.alexseifert.com/2026/06/02/the-relaunch-of-the-old-west-and-why-i-chose-vanilla-php/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 08:26:38 +0000</pubDate>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[History]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Next.js]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[React]]></category>
		<category><![CDATA[The Old West]]></category>
		<category><![CDATA[Web Development]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12337</guid>

					<description><![CDATA[I rewrote my project, The Old West, using vanilla PHP despite it not being glamorous. This is why. ]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="654" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11.png?resize=1024%2C654&#038;ssl=1" alt="The Old West in 2026" class="wp-image-12338" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=1024%2C654&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=400%2C256&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=150%2C96&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=768%2C491&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=1536%2C981&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=2048%2C1308&amp;ssl=1 2048w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">The Old West in 2026</figcaption></figure>
</div>


<p class="wp-block-paragraph">Two days ago, I relaunched a website that <a href="https://blog.alexseifert.com/2021/04/19/the-old-west/" data-type="link" data-id="https://blog.alexseifert.com/2021/04/19/the-old-west/" target="_blank" rel="noreferrer noopener">I first set up in 2021</a> called <a href="https://www.the-old-west.com" target="_blank" rel="noreferrer noopener">The Old West</a>. It not only features a new design, but also a lot of new content. The purpose of the website is to consolidate Wikipedia articles about the old American west into one spot, making it easy to browse them. I import the articles into my database using Wikipedia&#8217;s API and display them under their&nbsp;<a href="https://en.wikipedia.org/wiki/Wikipedia:Copyrights" target="_blank" rel="noreferrer noopener">Creative Commons Attribution-ShareAlike License</a>. The articles are defined in config files and I wrote a script that imports and updates them. The script is run via a cronjob once a week to keep the articles up to date with the latest changes from Wikipedia.</p>



<p class="wp-block-paragraph">A complete rewrite was long overdue as I could no longer build the old website. At the time, I was heavily into React and <a href="https://nextjs.org" data-type="link" data-id="https://nextjs.org" target="_blank" rel="noreferrer noopener">Next.js</a> which is the technology I chose to power the website. Unfortunately, I also used the <a href="https://mui.com/material-ui/" data-type="link" data-id="https://mui.com/material-ui/" target="_blank" rel="noreferrer noopener">Material UI</a> component library which became the main liability. Material UI released a major new update at some point and I <em>really</em> didn&#8217;t like their new approach to CSS handling. Not only did I dislike it, it would have basically required a major rewrite of the website and all of its CSS. Therefore, I decided not to update. Of course, as time went on, the old version became incompatible with newer versions of React and Next.js and I got stuck with Next.js 12 which is several versions behind.</p>



<p class="wp-block-paragraph">That by itself isn&#8217;t as much of an issue, however, Node itself also continued to move on and eventually, Next.js 12 no longer supported the version of Node I had installed locally. I&#8217;m aware I could have used <a href="https://github.com/nvm-sh/nvm" data-type="link" data-id="https://github.com/nvm-sh/nvm" target="_blank" rel="noreferrer noopener">nvm</a> combined with a specific Node version defined in a <code>.nvmrc</code> file at the root of the project, but that is more than I was willing to do to support the old, increasingly fragile technology powering the project. At that point, I had also become very disillusioned by React, Next.js and the general highly volatile Node ecosystem with its well-known dependency churn and increasing number of supply chain attacks.</p>



<p class="wp-block-paragraph">So, when I decided to finally completely rewrite The Old West, I chose to go in a fully different direction with the technology. I chose vanilla PHP.</p>



<p class="wp-block-paragraph">With so many shiny new programming languages and web frameworks on offer, why did I choose PHP? Well, I didn&#8217;t want dependencies. I wanted to write the code once, deploy it and be able to forget about it for years at a time even if it meant a little bit more work up front. My other requirement was that I still wanted to be able to update it at anytime without having to first spend hours trying to sort out dependency or runtime version incompatibilities just to get it to build again.</p>



<p class="wp-block-paragraph">Vanilla PHP is not only perfect for that, but really the only feasible way to do it. Combined with Apache, Linux and MariaDB (the truly classic LAMP stack), it is practically bulletproof. No supply chain attacks, no version incompatibilities, no dependency update hell &#8212; nothing but stability and peace of mind.</p>



<p class="wp-block-paragraph">PHP is incredibly backwards-compatible which means I can safely update the version on my server using my package manager and don&#8217;t have to worry about too many, if any updates to my code. The only &#8220;dependencies&#8221; I rely on are official PHP extensions such as PDO for database connectivity which I&#8217;m not concerned about because they are maintained and updated with PHP itself. </p>



<p class="wp-block-paragraph">I wrote my own autoloader for namespaces, a tiny, fully custom router module, as well as a very lightweight ORM layer with an inheritable Model class, a QueryBuilder class, and a migration class that processes raw <code>.sql</code> files for migrations. Templating is handled with pure PHP. There is hardly any JavaScript on the website and I use pure CSS without anything like SCSS or Less. I do use the Bootstrap CSS classes for responsiveness, but I copied the CSS files into my project by hand which means they will work forever unless I feel like updating them.</p>



<p class="wp-block-paragraph">That&#8217;s really all there is to it. The website is lightweight, lightning-fast, and entirely customized to my specific needs. It will run stably for as long as the server runs and even if the server craps out at some point, moving the project to another server requires minimal effort.</p>



<p class="wp-block-paragraph">Vanilla PHP may not be glamorous, but it has been the stablest way to create a website for decades and, since it still powers the majority of the web, I think we can safely say it will remain that way for decades to come.</p>



<p class="wp-block-paragraph">If you&#8217;d like to see the new design, you can find The Old West here: <a href="https://www.the-old-west.com" target="_blank" rel="noreferrer noopener">www.the-old-west.com</a>.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/06/02/the-relaunch-of-the-old-west-and-why-i-chose-vanilla-php/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12337</post-id>	</item>
		<item>
		<title>My New 14&#8243; MacBook Pro M5</title>
		<link>https://blog.alexseifert.com/2026/04/30/my-new-14-macbook-pro-m5/</link>
					<comments>https://blog.alexseifert.com/2026/04/30/my-new-14-macbook-pro-m5/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Thu, 30 Apr 2026 09:40:24 +0000</pubDate>
				<category><![CDATA[Hardware]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Mac]]></category>
		<category><![CDATA[MacBook]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12271</guid>

					<description><![CDATA[I finally got a new MacBook. It's my first brand new MacBook since 2014.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Well, I finally got a new MacBook Pro. It&#8217;s my first brand new MacBook since I bought a 15&#8243; MacBook Pro in 2014. I&#8217;ve been wanting to buy a new one for several months, but was unsure about whether I should wait for the rumored M6 MacBooks that will supposedly get a new lighter and thinner design later this year or just ignore the rumors and buy a new one now.</p>



<p class="wp-block-paragraph">But once I found out that <a href="https://www.cyberport.de" data-type="link" data-id="https://www.cyberport.de" target="_blank" rel="noreferrer noopener">cyberport.de</a> had MacBooks on sale (I&#8217;m not sponsored), I decided that I couldn&#8217;t wait anymore and bought one. The basic specs are:</p>



<ul class="wp-block-list">
<li>M5 chip</li>



<li>32 GB of RAM</li>



<li>2 TB SSD</li>



<li>14&#8243; display</li>
</ul>



<p class="wp-block-paragraph">I got that for €2500 which is about €500 less than directly from Apple and essentially made the 2 TB SSD upgrade free. I also could have gone with a 16&#8243; instead, but I decided I&#8217;d prefer a larger SSD. Most of the time I use it plugged into an external monitor anyway and when I&#8217;m traveling with it, I like the smaller, lighter form factor.</p>



<p class="wp-block-paragraph">I did play around with the idea of getting a 15&#8243; MacBook Air instead since the specs I care most about are  the same, but in the end I decided the fans might be nice to have even though I&#8217;ll probably only rarely, if ever, need them.</p>



<p class="wp-block-paragraph">Here are a few pictures:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7151/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro still sealed in the box" class="wp-image-12289" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro still sealed in the box</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7152/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro still unopened" class="wp-image-12290" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro still unopened</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7153/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro's first boot screen" class="wp-image-12292" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro&#8217;s first boot screen</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7154/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro installing macOS 26.4.1" class="wp-image-12291" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro installing macOS 26.4.1</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7155/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro transferring files from an external SSD with a Time Machine backup of my old 2019 MacBook Pro" class="wp-image-12293" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro transferring files from an external SSD with a Time Machine backup of my old 2019 MacBook Pro</figcaption></figure>
</div>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/04/30/my-new-14-macbook-pro-m5/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12271</post-id>	</item>
		<item>
		<title>ScratchBook 2.1 Released</title>
		<link>https://blog.alexseifert.com/2026/04/28/scratchbook-2-1-released/</link>
					<comments>https://blog.alexseifert.com/2026/04/28/scratchbook-2-1-released/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Tue, 28 Apr 2026 08:13:18 +0000</pubDate>
				<category><![CDATA[Haunted House Software]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[ScratchBook]]></category>
		<category><![CDATA[ScratchPad]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12257</guid>

					<description><![CDATA[ScratchBook 2.1 has just been released in the Mac App Store and now includes support for Sonoma and Sequoia.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">ScratchBook 2.1 has just been released in the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>!</p>



<p class="wp-block-paragraph">Changes include:</p>



<ul class="wp-block-list">
<li>Added support for macOS 14 (Sonoma) and macOS 15 (Sequoia)</li>



<li>Fixed an issue with truncated text on the import sheet</li>
</ul>



<p class="wp-block-paragraph">You can download ScratchBook for free on the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel=" noreferrer noopener"><img data-recalc-dims="1" loading="lazy" decoding="async" width="156" height="40" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=156%2C40&#038;ssl=1" alt="Download on the Mac App Store" class="wp-image-12140" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?w=156&amp;ssl=1 156w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=150%2C38&amp;ssl=1 150w" sizes="auto, (max-width: 156px) 100vw, 156px" /></a></figure>
</div>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/04/28/scratchbook-2-1-released/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12257</post-id>	</item>
		<item>
		<title>Examples of Server-Side Swift in the Wild</title>
		<link>https://blog.alexseifert.com/2026/04/04/examples-of-server-side-swift-in-the-wild/</link>
					<comments>https://blog.alexseifert.com/2026/04/04/examples-of-server-side-swift-in-the-wild/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Sat, 04 Apr 2026 08:38:41 +0000</pubDate>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[Notes]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Vapor]]></category>
		<category><![CDATA[Web Development]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12133</guid>

					<description><![CDATA[Ever since I experimented with the Swift server-side framework Vapor last year, I've been interested in actually seeing it in the wild. Here are a couple of examples.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="640" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=1024%2C640&#038;ssl=1" alt="Swift-based architecture for Things Cloud" class="wp-image-12224" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=1024%2C640&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=400%2C250&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=150%2C94&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=768%2C480&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=1536%2C960&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?w=1680&amp;ssl=1 1680w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Swift-based architecture for Things Cloud</figcaption></figure>
</div>


<p class="wp-block-paragraph">Ever since <a href="https://blog.alexseifert.com/2025/06/29/experimenting-with-the-swift-web-framework-vapor/" data-type="link" data-id="https://blog.alexseifert.com/2025/06/29/experimenting-with-the-swift-web-framework-vapor/" target="_blank" rel="noreferrer noopener">I experimented with the Swift server-side framework Vapor</a> last year, I&#8217;ve been interested in actually seeing it in operation in the wild. I know there have been a number of companies, including Apple itself, that have started using it for their server-side applications, but it&#8217;s nice to see some concrete examples of it.</p>



<p class="wp-block-paragraph">I&#8217;ve been following the <a href="https://www.swift.org/blog/" data-type="link" data-id="https://www.swift.org/blog/" target="_blank" rel="noreferrer noopener">official Swift blog</a> for a while now, but I just recently discovered two posts about companies that are using Swift and Vapor at large scale to run their server-side applications.</p>



<p class="wp-block-paragraph">The first of which is the syncing service for the very popular Mac/iOS productivity application, <a href="https://culturedcode.com/things/" data-type="link" data-id="https://culturedcode.com/things/" target="_blank" rel="noreferrer noopener">Things</a>. I&#8217;ve played around with Things before in the past, but it&#8217;s too expensive for my basic needs, so I don&#8217;t use it. Nonetheless, it is still a very popular application and the fact that Swift and Vapor power its syncing backend is great news for those who are questioning the framework&#8217;s reliability or scalability.</p>



<p class="wp-block-paragraph">You can read more about Things using Swift and Vapor <a href="https://www.swift.org/blog/how-swifts-server-support-powers-things-cloud/" target="_blank" rel="noreferrer noopener">here</a>. The post also includes more information about their infrastructure in general.</p>



<p class="wp-block-paragraph">The second usage I found recently was for the <a href="https://telemetrydeck.com" data-type="link" data-id="https://telemetrydeck.com" target="_blank" rel="noreferrer noopener">TelemetryDeck analytics service</a>. Unlike Things, I had never heard of them, so I can&#8217;t say a whole lot about them, but they apparently have a massive amount of traffic that Swift and Vapor handle flawlessly.</p>



<p class="wp-block-paragraph">You can read about it <a href="https://www.swift.org/blog/building-privacy-first-analytics-with-swift/" target="_blank" rel="noreferrer noopener">here</a>.</p>



<p class="wp-block-paragraph">Of course, something to keep in mind is that both of these posts are on the official Swift blog which is unlikely to report on any negative experiences that companies have had with server-side Swift. However, I still find it great that companies are having success with using Swift and Vapor in production environments with large traffic loads.</p>



<p class="wp-block-paragraph">If you know of any other examples of server-side Swift in the wild, let me know. I&#8217;d love to read about it &#8212; good or bad!</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/04/04/examples-of-server-side-swift-in-the-wild/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12133</post-id>	</item>
		<item>
		<title>Trying to Use My iPad as a Desktop Replacement</title>
		<link>https://blog.alexseifert.com/2026/03/26/trying-to-use-my-ipad-as-a-desktop-replacement/</link>
					<comments>https://blog.alexseifert.com/2026/03/26/trying-to-use-my-ipad-as-a-desktop-replacement/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Thu, 26 Mar 2026 15:53:44 +0000</pubDate>
				<category><![CDATA[Featured]]></category>
		<category><![CDATA[Operating Systems]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[iPad]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12163</guid>

					<description><![CDATA[I've already tried using my iPad as a laptop replacement while traveling, but this time, I try to use it as a desktop replacement. This is how it went.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=1024%2C768&#038;ssl=1" alt="My desk setup using my iPad Air M2 in desktop mode" class="wp-image-12184" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">My desk setup using my iPad Air M2 in desktop mode</figcaption></figure>
</div>


<p class="wp-block-paragraph">A couple of months ago, I took my 11&#8243; iPad Air M2 with me to the city of Regensburg to try <a href="https://blog.alexseifert.com/2026/01/26/trying-to-use-my-ipad-as-a-laptop-replacement/" data-type="link" data-id="https://blog.alexseifert.com/2026/01/26/trying-to-use-my-ipad-as-a-laptop-replacement/" target="_blank" rel="noreferrer noopener">working with it as a laptop replacement</a>. Unfortunately, the tiny 11&#8243; screen and the non-standard, smaller keyboard made it less than a stellar experience. However, I thought I could mitigate those problems while taking advantage of iPadOS 26&#8217;s new windowing feature by using a normal keyboard, mouse and a large external screen &#8212; essentially, turning it into a desktop replacement.</p>



<h2 class="wp-block-heading">Equipment</h2>



<p class="wp-block-paragraph">My equipment list for this experiment isn&#8217;t as exciting as it was for the laptop replacement experiment because I just used my usual desktop accessories:</p>



<ul class="wp-block-list">
<li>Logitech MX Keys keyboard</li>



<li>Apple mouse</li>



<li>LG widescreen monitor that supports USB-C</li>
</ul>



<p class="wp-block-paragraph">Normally, I use a Logitech MX Master 3S mouse instead of the Apple mouse, but I&#8217;ve already used up all three device connections that it supports, so I had dig out my old Apple mouse. Otherwise, the most important part was the monitor with USB-C support. With that, I could just plug my iPad in and it not only activated external display mode, but importantly, it also charged.</p>



<h2 class="wp-block-heading">My Experience</h2>



<p class="wp-block-paragraph">I know that the iPad is limited as to what it is capable of doing primarily due to the limitations of iPadOS. Development is nearly impossible on it which already means it&#8217;s not a viable desktop replacement for me full-time. However, I also write a lot, so I thought the best way to test it was to do some writing.</p>



<p class="wp-block-paragraph">On my Mac, I use Pages anyway and most of it gets saved to iCloud Drive. As such, writing in Pages on the iPad means there isn&#8217;t really much of a change of workflow. I already knew that from when I tried using my iPad as a laptop replacement, but what I didn&#8217;t count on what the severe limitations I ran into when using the mouse and keyboard.</p>



<p class="wp-block-paragraph">It turns out, unsurprisingly, that iPad apps are not optimized for them. That didn&#8217;t surprise me at all, but what did surprise me was just <em>how</em> limited it would be. Even first party-applications like Pages were severely restricted. For example, there is no way to correct a misspelled word with the mouse and keyboard. You have to use the touch screen to tap on the word that is underlined in red and then you get the popup menu with the spelling suggestions. There is absolutely not way to replicate that functionality with the mouse and keyboard which means I had to drag Pages from my big screen down to the iPad&#8217;s touch screen to use it &#8212; not a good experience at all.</p>



<p class="wp-block-paragraph">Also, for some reason, forward delete doesn&#8217;t seem to work at all on the iPad. A full-size Mac keyboard has its own forward delete key and on the smaller Mac keyboards (such as on the MacBooks), you use fn+delete. Neither of those worked on the iPad at all which made trying to edit text much more tedious.</p>



<p class="wp-block-paragraph">Even worse, though, were third-party apps. For example, I tried watching a YouTube video using the YouTube app. I could click on a video, but couldn&#8217;t exit it. There was literally no way to leave the video or access buttons like &#8220;Save&#8221; or &#8220;Like&#8221;. It just didn&#8217;t work at all. Again, I had to drag the app down to my iPad&#8217;s screen and use the touch interface.</p>



<p class="wp-block-paragraph">The worst issue I kept encountering, though, was that the iPad would regularly crash entirely. As in, both screens would go black and there would be a loading spinner on the iPad&#8217;s screen that lasted for several seconds before I could use it again&#8230; except all of the apps I had open were closed. That happened with startling frequency even with only three of my most-used apps open: Mail, Safari and Pages. So much for windowed multitasking.</p>



<p class="wp-block-paragraph">After this happened four times in a half an hour, I gave up on the experiment and went back to my Mac.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">Needless to say, I won&#8217;t be using my iPad as a desktop or laptop replacement anytime soon. After the experiment with trying to use the iPad as a laptop replacement, I went into this one less than optimistic. Unfortunately, my pessimism was confirmed by the experience.</p>



<p class="wp-block-paragraph">I find it really disappointing that, despite the improvements Apple made to desktop mode in iPadOS 26, it still doesn&#8217;t even come close to being a viable, reliable option. Frankly, my iPad with its M2 processor is several times more powerful than my old Intel MacBook Pro, but the software cripples it to the point of irrelevance.</p>



<p class="wp-block-paragraph">The idea of having a truly all-in-one, portable device is extremely appealing to me. I love using my iPad to read my RSS feeds, eBooks, articles online, magazines, etc which is what I primarily use it for. While I have an Apple Pencil, I rarely use it to take notes or draw anything. Unfortunately, unless Apple makes some major changes to iPadOS, it&#8217;s just going to remain a glorified electronic reader for me.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/03/26/trying-to-use-my-ipad-as-a-desktop-replacement/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12163</post-id>	</item>
	</channel>
</rss>
