tag:otavio.cc,2005:/posts_feed Otávio C. https://cdn.u.pika.page/rn2srD1AHUgpv5E4BWQX7WAE-Gc_YZ1MkAOyZx5iFtk/s:100:100/fn:otaviocc/plain/s3://pika-production/06ng6xtgjm8baeez9hu1gojk86e3 2025-12-17T11:20:56Z tag:otavio.cc,2005:Post/74126 2026-01-22T12:47:23Z 2026-03-11T08:05:36Z Triton <div class="trix-content"> <ol class="site-table-of-contents"> <li><a href="#statuslog">Statuslog</a></li> <li><a href="#purls">PURLs</a></li> <li><a href="#web-page">Web Page</a></li> <li><a href="#now-page">Now Page</a></li> <li><a href="#weblog">Weblog</a></li> <li><a href="#pics">Pics</a></li> <li><a href="#pastebin">Pastebin</a></li> </ol> <p>A couple of months ago, I wrote about <a href="https://github.com/otaviocc/Triton">Triton</a>, detailing some of the technical decisions I made during its development. <a href="https://otavio.cc/posts/lessons-from-building-triton">That post</a> covered architecture, design patterns, and lessons learned along the way. At the time, Triton was ready to launch, but I needed extra time to review its code one more time to ensure everything worked exactly as intended.</p> <p>A month has passed, and Triton is now available for <a href="https://github.com/otaviocc/Triton/releases">download</a>. Not only that, its source code is also available on <a href="https://github.com/otaviocc/Triton">GitHub</a>.</p> <p>For those unfamiliar, <a class="underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://home.omg.lol">omg.lol</a> is a web service<sup id="fnref:1"><a class="footnote-ref" data-id="7baec86d-82ff-4fab-8cd7-dbac05180852" href="#fn:1">1</a></sup> that provides a collection of fun, useful tools like status updates, permanent URLs, web pages, weblogs, pastebin, and more, all tied to a personal address. I built Triton to bring these to the desktop with a native macOS experience, allowing users to interact with all these services from a single, unified application.</p> <p>The app supports multiple <a href="https://omg.lol">omg.lol</a> addresses, letting users switch between them seamlessly. Beyond basic functionality, Triton includes quality-of-life features like local muting (hiding specific addresses or keywords from the statuslog timeline), sharing through native macOS share sheets, context menus for quickly copying URLs in various formats, and integration with Spotlight and Shortcuts.</p> <p>Triton is free and will always be free and open source. No in-app purchases, no prompts to rate or review. If you find it useful and want to support its development, I'd appreciate a <a class="underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://ko-fi.com/otaviocc">coffee to keep the code flowing</a> ☕️.</p> <p>Now, to the features!</p> <h2 id="statuslog"> <a href="#statuslog" class="anchor" title="Link to this heading" aria-hidden="true"></a>Statuslog</h2> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/swXYKVxP0gz3DIqo7uyzD7gbZh0HPHef6L0n2Ka9sFg/s:3840:3840/fn:social/plain/s3://pika-production/yw6vb5mo3conwilfzhunnjahixar" data-original-src="https://cdn.u.pika.page/MaiKDlYt6Xri2f9riCRcXEbVO0TLZudkhc3u8RI4RP4/fn:social/plain/s3://pika-production/yw6vb5mo3conwilfzhunnjahixar" alt="" src="https://cdn.u.pika.page/LBZzvZzTQ54tvXcl2cMhBbI38iZAydBPhzJaoxo4oy8/s:1800:1400/fn:social/plain/s3://pika-production/yw6vb5mo3conwilfzhunnjahixar"> <figcaption class="attachment__caption" aria-hidden="true"> Statuslog </figcaption> </figure><p><strong>Statuslog</strong> serves as the landing screen of the application. Even before logging in, you can browse the list of posts published by the omg.lol community.</p> <p>Interaction with posts happens through right-click actions. A contextual menu appears with options to open the post in a browser, copy the URL, or mute all posts from that user<sup id="fnref:2"><a class="footnote-ref" data-id="681f1dac-b207-4048-a94c-f7286625146b" href="#fn:2">2</a></sup>.</p> <p>After logging in, you can publish a status using the compose button in the top right corner of the app.</p> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/JEV1UaeERuhjpzc7btPZ8gFAMPGjuipbJmjqK3XXJfE/s:3840:3840/fn:composing/plain/s3://pika-production/t9qrapr2jiy7gm2n9qg9k39pmeeu" data-original-src="https://cdn.u.pika.page/dItXpqEx0hCaGnTu2xrj03CTEA1IQKcNbiGqrNSp_0s/fn:composing/plain/s3://pika-production/t9qrapr2jiy7gm2n9qg9k39pmeeu" alt="" src="https://cdn.u.pika.page/BQsQp2fHdQj6LXLioVUjnwsIBLyAgIxp3mULv9ZbOYM/s:1800:1400/fn:composing/plain/s3://pika-production/t9qrapr2jiy7gm2n9qg9k39pmeeu"> <figcaption class="attachment__caption" aria-hidden="true"> Composing a new Status </figcaption> </figure><p>The compose window is resizable, and I built an emoji picker from scratch for this application. macOS doesn't provide an emoji picker that can be used programmatically in the way I needed, so creating a custom solution was necessary.</p> <p>If you have multiple addresses, you can use the address picker at the top of the compose window to select which address to post from. This pattern appears throughout other features of the application as well.</p> <h2 id="purls"> <a href="#purls" class="anchor" title="Link to this heading" aria-hidden="true"></a>PURLs</h2> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/WcGzGy4vnU33SgZdlPLl0HGFRRmh8x-2Eku2XaFA2sQ/s:3840:3840/fn:purls/plain/s3://pika-production/l3lrqnmwi1emsu143heelygsfp0t" data-original-src="https://cdn.u.pika.page/OE0Z5XN1wOvWw8VKKOt2_ZSgwDUgwtgF9Y5nt_E0fns/fn:purls/plain/s3://pika-production/l3lrqnmwi1emsu143heelygsfp0t" alt="" src="https://cdn.u.pika.page/lVrZCsgSpSrmDRxWin51GyceRjaSMlAMK7lW7PuU7Ko/s:1800:1400/fn:purls/plain/s3://pika-production/l3lrqnmwi1emsu143heelygsfp0t"> <figcaption class="attachment__caption" aria-hidden="true"> PURLs: Permanent URLs </figcaption> </figure><p>Permanent URLs, or simply <strong>PURLs</strong>, can be created from the plus button in the top right corner of the application. Once a PURL is saved, right-clicking reveals several options: copy the URL, copy it as a Markdown link, open in browser, share using macOS' native sharing menu, or share directly on Statuslog, which opens a compose window with the PURL and its name already populated.</p> <h2 id="web-page"> <a href="#web-page" class="anchor" title="Link to this heading" aria-hidden="true"></a>Web Page</h2> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/vzRBvEk455TMvI7_OlafmnF0zMGoOYxf09YOihY0_Hg/s:3840:3840/fn:webpage/plain/s3://pika-production/gye6mvzrjsvg18snihczekn4127x" data-original-src="https://cdn.u.pika.page/SigjuoULEvmwLEnWiiLxxe2bpkvvRMqH_2R4F_ot9t8/fn:webpage/plain/s3://pika-production/gye6mvzrjsvg18snihczekn4127x" alt="" src="https://cdn.u.pika.page/zcM_DffzycxVMTQOykaAs1ZCXxtBMa5h72sYwEx_q6g/s:1800:1400/fn:webpage/plain/s3://pika-production/gye6mvzrjsvg18snihczekn4127x"> <figcaption class="attachment__caption" aria-hidden="true"> Web Page </figcaption> </figure><p>Every omg.lol address has a landing page called<strong> Web Page</strong>. This page works as an index for all the services, websites, and links a person might want to share.</p> <p>In Triton, you can edit the content of your Web Page. The application maintains a local copy of previous versions, allowing you to revert changes if needed.</p> <p>Double-clicking a version opens the editor, while right-clicking also provides access to editing options.</p> <p>From the top bar, you can open the current Web Page version in a browser and share a link via Statuslog.</p> <h2 id="now-page"> <a href="#now-page" class="anchor" title="Link to this heading" aria-hidden="true"></a>Now Page</h2> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/lM61LOyktbbqyK8jyeGHqlbc_CPgsedxagxrgXneYc8/s:3840:3840/fn:nowpage/plain/s3://pika-production/1rgc8fw234x4v8fuzyxjjwa041eb" data-original-src="https://cdn.u.pika.page/X58OiqYCzDQe7SYm-ArRsaNfJYXEllZl_S7x0qsCiYw/fn:nowpage/plain/s3://pika-production/1rgc8fw234x4v8fuzyxjjwa041eb" alt="" src="https://cdn.u.pika.page/6fYMYyere4WrfvWCmKu2ws4gnjKzI97ierOnopGkTv0/s:1800:1400/fn:nowpage/plain/s3://pika-production/1rgc8fw234x4v8fuzyxjjwa041eb"> <figcaption class="attachment__caption" aria-hidden="true"> Now Page </figcaption> </figure><p><strong>Now Page</strong> is a simple page where you can <a href="https://nownownow.com/about">share information about what you're up to;</a> an easy way to consolidate what you're doing, reading, watching, playing, and more to share with friends and family.</p> <p>On Triton, this feature mirrors the Web Page implementation, with a local copy of recent changes and buttons to open in a browser and share directly via Statuslog. There’s also a button to open the garden with other Now Pages from the <a href="https://omg.lol">omg.lol</a> community.</p> <h2 id="weblog"> <a href="#weblog" class="anchor" title="Link to this heading" aria-hidden="true"></a>Weblog</h2> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/S_wamkGaLiS9y23mqPd9CYwVz58oWdjIzyASEUiUQWs/s:3840:3840/fn:Screenshot%202025-12-30%20at%2011.45.02/plain/s3://pika-production/78n9mxef3s7a0xzdq1uc6v40jnr3" data-original-src="https://cdn.u.pika.page/tfuhf1K9m9oKaX_DKdcmlZ3usp8IFf6nG3suDnBpLYI/fn:Screenshot%202025-12-30%20at%2011.45.02/plain/s3://pika-production/78n9mxef3s7a0xzdq1uc6v40jnr3" alt="" src="https://cdn.u.pika.page/U1dVRkOxPBN6dtyc5n1qy_4nonV7-17kXHWd-kPXTg0/s:1800:1400/fn:Screenshot%202025-12-30%20at%2011.45.02/plain/s3://pika-production/78n9mxef3s7a0xzdq1uc6v40jnr3"> <figcaption class="attachment__caption" aria-hidden="true"> Weblog </figcaption> </figure><p><strong>Weblog</strong> is where new blog entries can be created and managed. New entries can be created using the plus button in the top bar, which opens a composer window. From the composer, you can set the date and time of publishing, as well as the post status or visibility.</p> <p>For those using tags to group entries together, the Tags field autocompletes previously used tags. Pressing TAB selects the first matching tag, streamlining the tagging workflow.</p> <p>And <strong>Weblog</strong> is where other features meet. You can upload photos to <a href="https://some.pics">some.pics</a> via <strong>Pics</strong> and add them to blog posts as Markdown images. Similarly, code snippets from <strong>Pastebin</strong> can be copied as code blocks and inserted directly into blog entries. Blog posts can also be shared directly to <strong>Statuslog</strong>. This integration makes the workflow seamless; everything needed to publish a complete blog post is accessible from within the application.</p> <h2 id="pics"> <a href="#pics" class="anchor" title="Link to this heading" aria-hidden="true"></a>Pics</h2> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/jIrY4MSt-eB8JiSSM5_z4ysSR-yTB5eJ3504B4UwKwc/s:3840:3840/fn:Screenshot%202025-12-30%20at%2011.45.29/plain/s3://pika-production/bgdmeitzw7gbi6dwuuhao42xn2uy" data-original-src="https://cdn.u.pika.page/W-eC3dE1953kQCNMQ47PccLAAo5fyh-ItyazDGbihx0/fn:Screenshot%202025-12-30%20at%2011.45.29/plain/s3://pika-production/bgdmeitzw7gbi6dwuuhao42xn2uy" alt="" src="https://cdn.u.pika.page/utbFjHeQ1fzvaiCzDzcPdvDAfRCb9r6UZUf-xqXvdlU/s:1800:1400/fn:Screenshot%202025-12-30%20at%2011.45.29/plain/s3://pika-production/bgdmeitzw7gbi6dwuuhao42xn2uy"> <figcaption class="attachment__caption" aria-hidden="true"> Pics </figcaption> </figure><p><strong>Pics</strong> is my favorite feature. It handles image uploads to <a href="https://some.pics/">some.pics</a>, allowing you to add captions, alt text, and tags to images during the upload process.</p> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/IUd0Ldv52wmq-ukJsfXzklCM84v_M5M67ajkysAnaGk/s:3840:3840/fn:Screenshot%202025-12-30%20at%2011.45.39/plain/s3://pika-production/n3d2qri3093e1murbr0u3t9sr0r9" data-original-src="https://cdn.u.pika.page/MOndCDFzj7kPAP9Ov5W6RM8Z-QvXyQPWW4LiYykuADo/fn:Screenshot%202025-12-30%20at%2011.45.39/plain/s3://pika-production/n3d2qri3093e1murbr0u3t9sr0r9" alt="" src="https://cdn.u.pika.page/AemvNxG0xeih4zbhJJ7HhvKkscattEs_O0uJQtkeC-Y/s:1800:1400/fn:Screenshot%202025-12-30%20at%2011.45.39/plain/s3://pika-production/n3d2qri3093e1murbr0u3t9sr0r9"> <figcaption class="attachment__caption" aria-hidden="true"> Photo Uploader </figcaption> </figure><p>Photos can be added from the Photos app library or by dragging and dropping images directly into the photo uploader. This flexibility makes it easy to work with images regardless of where they're stored.</p> <p>Already uploaded pictures can be edited to update their captions, alt text, and tags. This makes it easy to refine how images are presented without needing to re-upload them.</p> <p>The contextual menu provides several options for working with images. You can copy the some.pics link to a photo, copy it as a Markdown link, copy it as a Markdown image, and much more. Images can also be shared directly to Statuslog. These options simplify the process of incorporating images into blog posts, status updates, or any other content being created.</p> <h2 id="pastebin"> <a href="#pastebin" class="anchor" title="Link to this heading" aria-hidden="true"></a>Pastebin</h2> <figure class="attachment attachment--preview attachment--png"> <img height="1596" width="2422" data-zoom-src="https://cdn.u.pika.page/Ov7HZb9x0EPibb5ObEQ1WyJxXaBTpP8z-vHGG79iZmI/s:3840:3840/fn:Screenshot%202025-12-30%20at%2011.46.19/plain/s3://pika-production/7va1crp9v572p7ovhf9ws0sjrydk" data-original-src="https://cdn.u.pika.page/gZ8OTYjL0GCQEEklUPYm-Txr4I8zgzm13iPlFynCm98/fn:Screenshot%202025-12-30%20at%2011.46.19/plain/s3://pika-production/7va1crp9v572p7ovhf9ws0sjrydk" alt="" src="https://cdn.u.pika.page/3ObbtD8xMxP5BRrhVB_A5YX-Iz_mhRhBmXCqCckTnCg/s:1800:1400/fn:Screenshot%202025-12-30%20at%2011.46.19/plain/s3://pika-production/7va1crp9v572p7ovhf9ws0sjrydk"> <figcaption class="attachment__caption" aria-hidden="true"> Pastebin </figcaption> </figure><p><strong>Pastebin</strong> is a place for short text and code snippets to be shared with others online. Like other features in Triton, it provides a contextual menu with several options, including the ability to copy snippets as Markdown code blocks for use in blog posts or entries, and to share directly to Statuslog. This makes it straightforward to share code examples or text snippets while maintaining proper formatting.</p> <p>That’s Triton. If you have feature requests or run into any issues, the <a class="underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://github.com/otaviocc/Triton">GitHub repository</a> is the place to go. I'm looking forward to seeing how people use the app.</p> <ol class="footnotes"> <li id="fn:1" data-id="7baec86d-82ff-4fab-8cd7-dbac05180852"><p>And better yet, omg.lol was created and is maintained by <a href="https://adam.omg.lol/">Adam</a>, a genuinely good person who stands for the right causes and created one of the most welcoming communities on the web.</p></li> <li id="fn:2" data-id="681f1dac-b207-4048-a94c-f7286625146b"><p>Muting users and keywords can also be configured from Triton Settings.</p></li> </ol> </div> A couple of months ago, I wrote about Triton, detailing some of the technical decisions I made during its development. That post covered architecture, design patterns, and lessons learned along... tag:otavio.cc,2005:Post/74057 2026-01-08T10:49:34Z 2026-01-08T10:49:34Z Defaults in December 2025 <div class="trix-content"> <p>Continuing the <em>tradition</em> from the last two years (<a href="https://otavio.cc/posts/defaults-in-december-2023">2023</a>, <a href="https://otavio.cc/posts/defaults-in-december-2024">2024</a>), it's time for my annual Defaults post.</p> <p>As I always do at some point during the year, I migrated back to <a href="https://en.wikipedia.org/wiki/Reminders_(Apple)">Apple Reminders</a> for my <em>personal</em> to-dos. The reason is simple: it's well integrated with the system, easy to share with family members, and Apple keeps adding new features every time there's a new version of its operating systems. It's not as beautiful as <a href="https://culturedcode.com/things/">Things</a>, but it gets the job done.</p> <p>Without going through the photos in my library, I can't pinpoint when I started using the <a href="https://leica-camera.com/en-US/photography/leica-apps/leica-lux">Leica LUX</a> app. But during the course of the year, it took <a href="https://halide.cam/">Halide</a>'s place on my phone and is one of the icons on my Lock Screen (the other being Apple's Camera app). Leica LUX is not cheap, but the quality of the photos is phenomenal. For portrait photos, there's nothing like it. Best of all, the subscription can be shared with family members, which is  great.</p> <p>For about four years, I worked at a VPN company, but earlier this year I switched companies and stopped using their product. I'm using <a href="https://protonvpn.com">ProtonVPN</a> now and am pretty happy with their service. I just wish their Apple TV app was a little bit faster at fetching and updating the list of countries and servers.</p> <p>In the past, I canceled my <a href="https://www.git-tower.com/mac">Git Tower</a> subscription, but this year it's back on the list of apps I use daily. It's one of the applications that's always open on my computer, so it's definitely worth its price.</p> <p>Two other apps that are always open on my computer are <a href="https://proxyman.com/">Proxyman</a> and <a href="https://kaleidoscope.app/">Kaleidoscope</a>. The combo of Tower, Proxyman, and Kaleidoscope works as a productivity booster. They're always open, next to Terminal and Xcode.</p> <p>Finally, the complete list of defaults apps in December 2025:</p> <ul> <li><p>📨 <strong>Mail Client</strong>: Mail.app, Proton Mail</p></li> <li><p>📮 <strong>Mail Server</strong>: iCloud, Proton Mail</p></li> <li><p>📝 <strong>Notes</strong>: Notes.app, iA Writer</p></li> <li><p>✅ <strong>To-Do</strong>: Reminders.app</p></li> <li><p>📷 <strong>iPhone Photo Shooting</strong>: Camera.app, Leica LUX</p></li> <li><p>🟦 <strong>Photo Management</strong>: Photos.app</p></li> <li><p>📆 <strong>Calendar</strong>: Calendar.app</p></li> <li><p>📁 <strong>Cloud File Storage</strong>: iCloud Drive</p></li> <li><p>📖 <strong>RSS</strong>: Feedbin with Reeder</p></li> <li><p>🙍🏻‍♂️ <strong>Contacts</strong>: Contacts.app</p></li> <li><p>🌐 <strong>Browser</strong>: Safari.app, Orion</p></li> <li><p>💬 <strong>Chat</strong>: Messages.app, Signal</p></li> <li><p>🔖 <strong>Bookmarks</strong>: Anybox</p></li> <li><p>📑 <strong>Read It Later</strong>: GoodLinks</p></li> <li><p>📰 <strong>News</strong>: Feedbin, Mastodon</p></li> <li><p>🎵 <strong>Music</strong>: Music.app</p></li> <li><p>🎤 <strong>Podcasts</strong>: Podcasts.app</p></li> <li><p>🔐 <strong>Password Management</strong>: 1Password, iCloud Keychain</p></li> </ul> <p>Other:</p> <ul> <li><p>🔎 <strong>Search</strong>: Kagi Extension (in Safari.app)</p></li> <li><p>💬 <strong>Social</strong>: Ivory (Mastodon), Tapestry</p></li> <li><p>📝 <strong>Journaling</strong>: Journal.app</p></li> <li><p>📚 <strong>Reading</strong>: Kindle, Books.app</p></li> <li><p>🔑 <strong>VPN</strong>: ProtonVPN</p></li> <li><p>🔢 <strong>MFA</strong>: Yubico Authenticator</p></li> </ul> <p>Coding:</p> <ul> <li><p>🧑‍💻 <strong>Code Editor</strong>: NeoVim, Xcode (macOS), Nova</p></li> <li><p>👨‍💻 <strong>Git</strong>: Tower</p></li> <li><p>👨‍💻 <strong>Proxy</strong>: Proxyman</p></li> <li><p>👨‍💻 <strong>Diff</strong>: Kaleidoscope</p></li> <li><p>👨‍💻 <strong>Shell</strong>: Zsh</p></li> <li><p>👨‍💻 <strong>Multiplexer</strong>: tmux</p></li> </ul> </div> Continuing the tradition from the last two years (2023, 2024), it's time for my annual Defaults post. As I always do at some point during the year, I migrated back... tag:otavio.cc,2005:Post/74695 2026-01-08T10:49:00Z 2026-01-08T10:49:57Z Subscriptions in December 2025 <div class="trix-content"> <p>Similar to the <em>tradition</em> of publishing the annual list of Defaults, this is the review of my current subscriptions. I’ve also published the lists in <a href="https://otavio.cc/posts/subscriptions-in-december-2023">2023</a> and <a href="https://otavio.cc/posts/subscriptions-in-december-2024">2024</a>.</p> <p>Event though I have a <a href="https://bearblog.dev/">Bear</a> lifetime subscription, I moved my blog to <a href="https://pika.page">Pika</a>. I don’t know exactly why, but I like it. Also, the folks from <a href="https://goodenough.us/">Good Enough</a>, the makers of Pika, are super transparent and responsive on Mastodon.</p> <p>I’ve also moved to a lifetime subscription in <a href="https://omg.lol">omg.lol</a> because that’s the best corner of the web. I even made my own application, <a href="https://github.com/otaviocc/Triton/">Triton</a><sup id="fnref:1"><a class="footnote-ref" data-id="756fba5b-bdbf-4d1c-9afe-d317c777a225" href="#fn:1">1</a></sup>, to use <a href="https://omg.lol">omg.lol</a> services natively from my computer.</p> <p>And to simplify my life and cross-post everything to Mastodon, I’ve added <a href="https://echofeed.app/">EchoFeed</a> to my list of subscriptions. It’s the kinda of service that works so well you don’t even remember it exists.</p> <p>Finally, the complete list of subscriptions in December 2025:</p> <h2 id="streaming"> <a href="#streaming" class="anchor" title="Link to this heading"></a>Streaming</h2> <ul> <li><p>Amazon Prime - <code>90 €/year</code></p></li> <li><p>Apple TV+ (as part of the Apple One subscription)</p></li> <li><p>Disney Plus - <code>160 €/year</code></p></li> </ul> <h2 id="podcasts"> <a href="#podcasts" class="anchor" title="Link to this heading"></a>Podcasts</h2> <ul> <li><p><a href="https://99percentinvisible.org/"><u>99% Invisible</u></a> + other SiriusXM Podcasts - <code>50.00 €/year</code></p></li> <li><p><a href="https://www.20k.org/"><u>Twenty Thousand Hertz+</u></a> - <code>45.00 €/year</code></p></li> </ul> <h2 id="services"> <a href="#services" class="anchor" title="Link to this heading"></a>Services</h2> <h3 id="must-have"> <a href="#must-have" class="anchor" title="Link to this heading"></a>Must-Have</h3> <ul> <li><p><a href="https://1password.com/"><u>1Password</u></a> - <code>$60/year</code></p></li> <li><p><a href="https://www.apple.com/apple-one/"><u>Apple One</u></a> - <code>35 €/month</code></p></li> <li><p><a href="https://arqbackup.com/"><u>Arq</u></a> - <code>$59.99/year</code></p></li> <li><p><a href="https://relay.firefox.com/"><u>Firefox Relay</u></a> - <code>12 €/year</code></p></li> <li><p><a href="https://kagi.com/"><u>Kagi</u></a> - <code>$108/year</code></p></li> <li><p><a href="https://proton.me/"><u>Proton</u></a> - <code>95 €/year</code> (190 € every two years, actually).</p></li> </ul> <p>These are indispensable to me.</p> <h3 id="blogsocial"> <a href="#blogsocial" class="anchor" title="Link to this heading"></a>Blog/Social</h3> <ul> <li><p><a href="https://pika.page/"><u>Pika</u></a> - <code>$60/year</code></p></li> <li><p><a href="http://omg.lol/"><u>omg.lol</u></a> - <code>$0/year</code> (lifetime)</p></li> <li><p><a href="https://echofeed.app/"><u>EchoFeed</u></a> - <code>$20/year</code> (<code>$25/year</code>, but the <a href="http://omg.lol/"><u>omg.lol</u></a> community gets a discount)</p></li> </ul> <p>These are the best online communities one could hope for.</p> <h3 id="reading"> <a href="#reading" class="anchor" title="Link to this heading"></a>Reading</h3> <ul> <li><p><a href="https://feedbin.com/"><u>Feedbin</u></a> - <code>$50/year</code></p></li> <li><p><a href="https://goodlinks.app/"><u>GoodLinks</u></a> - <code>6 €/year</code></p></li> </ul> <h3 id="fun-to-have"> <a href="#fun-to-have" class="anchor" title="Link to this heading"></a>Fun to Have</h3> <ul><li><p><a href="https://www.nintendo.com/us/switch/online/nintendo-switch-online/"><u>Nintendo Switch Online + Erweiterungspaket</u></a> - <code>40 €/year</code></p></li></ul> <h2 id="apps"> <a href="#apps" class="anchor" title="Link to this heading"></a>Apps</h2> <ul> <li><p><a href="https://tapbots.com/ivory/"><u>Ivory</u></a> - <code>30 €/year</code></p></li> <li><p><a href="https://www.git-tower.com/mac"><u>Git Tower</u></a> - <code>99 €/year</code></p></li> <li><p><a href="https://kaleidoscope.app/"><u>Kaleidoscope</u></a> - <code>$8/month</code></p></li> <li><p><a href="https://proxyman.com/"><u>Proxyman</u></a> - <code>$99</code> (Perpetual License with 1 year of updates)</p></li> </ul> <ol class="footnotes"><li id="fn:1" data-id="756fba5b-bdbf-4d1c-9afe-d317c777a225"><p> Hey, <a href="https://ko-fi.com/otaviocc">send me a coffee</a> if you like it 😉</p></li></ol> </div> Similar to the tradition of publishing the annual list of Defaults, this is the review of my current subscriptions. I’ve also published the lists in 2023 and 2024. Event though... tag:otavio.cc,2005:Post/68078 2025-11-15T21:01:00Z 2026-03-10T22:25:37Z Lessons from Building Triton <div class="trix-content"> <ol class="site-table-of-contents"> <li><a href="#modularity-with-swift-package-manager">Modularity with Swift Package Manager</a></li> <li><a href="#one-way-dependencies">One-Way Dependencies</a></li> <li><a href="#vertical-slices">Vertical Slices</a></li> <li><a href="#protocol-first-boundaries">Protocol-First Boundaries</a></li> <li><a href="#modern-swift-throughout">Modern Swift Throughout</a></li> <li><a href="#data-flow-dtos-to-domain-models">Data Flow: DTOs to Domain Models</a></li> <li><a href="#streaming-based-caching">Streaming-Based Caching</a></li> <li><a href="#dependency-injection">Dependency Injection</a></li> <li><a href="#pure-swiftui">Pure SwiftUI</a></li> <li><a href="#features-built-on-this-architecture">Features Built on This Architecture</a></li> <li><a href="#consequences-and-tradeoffs">Consequences and Tradeoffs</a></li> <li><a href="#conclusion">Conclusion</a></li> </ol> <p>I've been building Triton, a native macOS client for <a href="https://omg.lol">omg.lol</a>. For those unfamiliar, <a href="https://omg.lol">omg.lol</a> is an amazing web service<sup id="fnref:1"><a class="footnote-ref" data-id="134a505c-d05f-4260-bb4c-4a59be9562d7" href="#fn:1">1</a></sup> that provides a collection of fun, useful tools like status updates, permanent URLs, web pages, weblogs, pastebin, and more, all tied to a personal address.</p> <p>Triton brings these to the desktop with a native macOS experience. Users can post status updates, manage their PURLs (permanent URLs), edit their web pages and now pages, write weblog entries, upload pictures to <a href="https://some.pics">some.pics</a>, and manage code snippets in the pastebin; all from a single, unified application. The app supports multiple <a href="https://omg.lol">omg.lol</a> addresses, letting users switch between them.</p> <div class="attachment-gallery"><figure class="attachment attachment--preview attachment--png"> <img height="1804" width="1444" data-zoom-src="https://cdn.u.pika.page/FvjT9ZqUj7Y2miwFWxdjz6LmZD0N7OK2BKtM6Tc8I7M/s:3840:3840/fn:image/plain/s3://pika-production/gbcxxhqu23ralwqkvtmvxub2q2t4" data-original-src="https://cdn.u.pika.page/ph1j-doWsQCvns1Ifrjk3sSBxUZU_qPLCcqa519Jmbo/fn:image/plain/s3://pika-production/gbcxxhqu23ralwqkvtmvxub2q2t4" alt="Screenshot of a native macOS application called Triton. It shows a timeline of status messages from different users on the omg.lol platform." src="https://cdn.u.pika.page/14y1oAkNwv-hV_ZSDnI_YvVkNvOXjaark5Z_CvstfYg/s:1800:1400/fn:image/plain/s3://pika-production/gbcxxhqu23ralwqkvtmvxub2q2t4"> <figcaption class="attachment__caption" aria-hidden="true"> Triton </figcaption> </figure></div> <p>Beyond basic functionality, Triton includes quality-of-life features like local muting (hide specific addresses or keywords from your statuslog timeline), sharing through native macOS share sheets and statuslog posts, context menus for quickly copying URLs in various formats (plain URLs, Markdown links, or Markdown code blocks), and Spotlight and Shortcuts integration.</p> <p>Triton isn't available for download yet, but I'm planning to release it soon. When it launches, it will be open source and available on GitHub as well, allowing others to read its code, contribute to, or adapt it for their own needs.</p> <p>Once Triton is released, I'll write a proper introduction covering the app from a user perspective; its features and workflows. For now, though, I want to focus on how the application was built and the architectural decisions behind it. Like the source code itself, these decisions are documented as Architecture Decision Records (ADRs) and will be available on GitHub, providing context for anyone interested in understanding or adapting the architecture.</p> <p>Building a macOS application from scratch, or any application to be honest, presents an opportunity to define architectural patterns that will serve the project for years to come. Rather than starting with a monolithic structure and refactoring later, I chose to implement a modular, feature-isolated, layered architecture from day one using <a href="https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/">Swift Package Manager</a>.</p> <h2 id="modularity-with-swift-package-manager"> <a href="#modularity-with-swift-package-manager" class="anchor" title="Link to this heading" aria-hidden="true"></a>Modularity with Swift Package Manager</h2> <p>The architecture centers on isolated packages within a <code>Packages/</code> directory. Each package represents either a feature domain or an infrastructure concern, creating boundaries that are enforced by the compiler.</p> <pre class="hljs-highlight"><code class="language-bash">Packages/ ├── Status/ <span class="hljs-comment"># Feature: Status updates</span> ├── PURLs/ <span class="hljs-comment"># Feature: URL management</span> ├── Account/ <span class="hljs-comment"># Feature: User account</span> ├── ... <span class="hljs-comment"># Feature: ...</span> ├── OMGAPI/ <span class="hljs-comment"># Infrastructure: API client</span> ├── DesignSystem/ <span class="hljs-comment"># Infrastructure: UI components</span> ├── SessionService/ <span class="hljs-comment"># Infrastructure: Session management</span> ├── ... <span class="hljs-comment"># Infrastructure: ...</span> └── FoundationExtensions/ <span class="hljs-comment"># Utilities</span></code></pre> <p>Swift Package Manager enforces dependency relationships at compile time. A feature package cannot accidentally import another feature's internal implementation. It's impossible to violate this without explicitly modifying <code>Package.swift</code> files.</p> <p>Currently, the application has 19 packages in the <code>Packages/</code> directory, not counting external dependencies like <code>MicroClient</code> and <code>MicroContainer</code> (which I created as well). These 19 packages contain more than 50 modules (targets), with each feature typically splitting into targets representing different architectural layers. This might seem excessive at first, but the granular structure provides precise control over dependencies and enables  isolation between concerns.</p> <p>The impact is felt immediately. Xcode rebuilds only changed packages and their dependents, improving incremental build times. More importantly, the architecture  accommodates growth without major changes.</p> <h2 id="one-way-dependencies"> <a href="#one-way-dependencies" class="anchor" title="Link to this heading" aria-hidden="true"></a>One-Way Dependencies</h2> <p>Within this modular foundation, I established strict architectural layers with enforced one-way dependency flow:</p> <pre class="hljs-highlight"><code class="language-bash"> Views | View Models | Repositories | Network &amp; Persistence Services | Shared Services (OMGAPI, SessionService) | Foundation Modules (Utilities)</code></pre> <p>These layers are enforced through Swift Package Manager's dependency system. Feature packages implement their layers as separate targets, while infrastructure and utility packages occupy the lower layers. The compiler prevents any upward dependencies.</p> <p>Views and View Models depend on Repository protocols, never directly on Network Service or Persistence Service. This isolation enables testing at each layer independently. A View Model test uses a mock repository without touching network or persistence code. A Repository test uses mock services without real API calls or database operations.</p> <p>The Repository layer coordinates between Network Service and Persistence Service, implementing domain logic and caching strategies. This is where the application decides when to fetch from the network versus reading from local storage, how to handle conflicts, and when to invalidate caches.</p> <h2 id="vertical-slices"> <a href="#vertical-slices" class="anchor" title="Link to this heading" aria-hidden="true"></a>Vertical Slices</h2> <p>Each feature package implements a complete vertical slice through all architectural layers:</p> <pre class="hljs-highlight"><code class="language-bash">Status/ ├── Package.swift ├── Sources/ │ ├── Status/ <span class="hljs-comment"># Main UI and integration</span> │ │ ├── Views/ │ │ ├── Scenes/ │ │ └── Factories/ │ ├── StatusRepository/ <span class="hljs-comment"># Data coordination</span> │ ├── StatusNetworkService/ <span class="hljs-comment"># API communication</span> │ └── StatusPersistenceService/ <span class="hljs-comment"># Local storage</span> └── Tests/</code></pre> <p>Features expose themselves through a standardized App Factory pattern:</p> <pre class="hljs-highlight"><code class="language-swift"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">StatusAppFactory</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> environment: <span class="hljs-type">StatusEnvironment</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">init</span>( <span class="hljs-params">sessionService</span>: <span class="hljs-keyword">any</span> <span class="hljs-type">SessionServiceProtocol</span>, <span class="hljs-params">authSessionService</span>: <span class="hljs-keyword">any</span> <span class="hljs-type">AuthSessionServiceProtocol</span>, <span class="hljs-params">networkClient</span>: <span class="hljs-type">NetworkClientProtocol</span> ) { environment <span class="hljs-operator">=</span> .<span class="hljs-keyword">init</span>( sessionService: sessionService, authSessionService: authSessionService, networkClient: networkClient ) } <span class="hljs-meta">@MainActor</span> <span class="hljs-meta">@ViewBuilder</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">makeAppView</span>() -&gt; <span class="hljs-keyword">some</span> <span class="hljs-type">View</span> { <span class="hljs-keyword">let</span> viewModel <span class="hljs-operator">=</span> environment.viewModelFactory .makeStatusAppViewModel() <span class="hljs-type">StatusApp</span>(viewModel: viewModel) .environment(\.viewModelFactory, environment.viewModelFactory) .modelContainer(environment.modelContainer) } <span class="hljs-meta">@MainActor</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">makeScene</span>() -&gt; <span class="hljs-keyword">some</span> <span class="hljs-type">Scene</span> { <span class="hljs-type">ComposeStatusScene</span>(environment: environment) } }</code></pre> <p>Everything else, Views, View Models, Repositories, Services, remains internal to the package. Features cannot access each other's internals, enforcing true isolation.</p> <p>I built Triton alone, but on a team, this structure promotes shared ownership. Any engineer can work on any feature without needing to understand the entire codebase. The boundaries are clear, the contracts are explicit, and the architecture naturally guides correct implementation.</p> <h2 id="protocol-first-boundaries"> <a href="#protocol-first-boundaries" class="anchor" title="Link to this heading" aria-hidden="true"></a>Protocol-First Boundaries</h2> <p>At layer boundaries, I use protocols to define contracts. Each repository and service has a public protocol marked <code>Sendable</code> for concurrency safety:</p> <pre class="hljs-highlight"><code class="language-swift"><span class="hljs-keyword">public</span> <span class="hljs-keyword">protocol</span> <span class="hljs-title class_">PURLsRepositoryProtocol</span>: <span class="hljs-title class_ inherited__">Sendable</span> { <span class="hljs-keyword">var</span> purlsContainer: <span class="hljs-type">ModelContainer</span> { <span class="hljs-keyword">get</span> } <span class="hljs-keyword">func</span> <span class="hljs-title function_">fetchPURLs</span>() <span class="hljs-keyword">async</span> <span class="hljs-keyword">throws</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">addPURL</span>(<span class="hljs-params">address</span>: <span class="hljs-type">String</span>, <span class="hljs-params">name</span>: <span class="hljs-type">String</span>, <span class="hljs-params">url</span>: <span class="hljs-type">String</span>) <span class="hljs-keyword">async</span> <span class="hljs-keyword">throws</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">deletePURL</span>(<span class="hljs-params">address</span>: <span class="hljs-type">String</span>, <span class="hljs-params">name</span>: <span class="hljs-type">String</span>) <span class="hljs-keyword">async</span> <span class="hljs-keyword">throws</span> }</code></pre> <p>Implementations are <code>actor</code> types, providing automatic serialization and thread safety:</p> <pre class="hljs-highlight"><code class="language-swift"><span class="hljs-keyword">actor</span> <span class="hljs-title class_">PURLsRepository</span>: <span class="hljs-title class_ inherited__">PURLsRepositoryProtocol</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> networkService: <span class="hljs-keyword">any</span> <span class="hljs-type">PURLsNetworkServiceProtocol</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> persistenceService: <span class="hljs-keyword">any</span> <span class="hljs-type">PURLsPersistenceServiceProtocol</span> <span class="hljs-comment">// Implementation details...</span> }</code></pre> <p>One feature I like in Xcode is that I only need to document protocols. When opening the Quick Help to read a method's documentation, Xcode displays the protocol's documentation if that method comes from a protocol the type implements. This means I document public protocols, the contracts defining what each layer provides. while implementation details remain undocumented and internal, reducing maintenance burden when internals change. When the code is released on GitHub, all public protocols will be fully documented.</p> <p>Since protocols are public but implementations are internal, concrete types are built and made available to other layers through factory methods. Each service and repository module includes factories that create and return instances:</p> <pre class="hljs-highlight"><code class="language-swift"><span class="hljs-keyword">func</span> <span class="hljs-title function_">makeRepository</span>() -&gt; <span class="hljs-keyword">any</span> <span class="hljs-type">PURLsRepositoryProtocol</span> { <span class="hljs-type">PURLsRepository</span>( networkService: makeNetworkService(), persistenceService: makePersistenceService(), authSessionService: authSessionService, sessionService: sessionService ) }</code></pre> <p>This pattern ensures consumers depend on protocol contracts while factories handle the creation of concrete implementations, maintaining encapsulation throughout the architecture.</p> <h2 id="modern-swift-throughout"> <a href="#modern-swift-throughout" class="anchor" title="Link to this heading" aria-hidden="true"></a>Modern Swift Throughout</h2> <p>The architecture leverages modern Swift features at every layer:</p> <ul> <li><p><a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/"><strong>Concurrency</strong></a><strong>:</strong> <a href="https://developer.apple.com/documentation/swift/actor">Actors</a> for services and repositories ensure thread safety without manual locks. All async operations use async/await.</p></li> <li><p><a href="https://developer.apple.com/documentation/Observation"><strong>Observation</strong></a><strong>:</strong> View Models use <code>@Observable</code> instead of <code>ObservableObject</code>, eliminating boilerplate and improving performance through fine-grained observation.</p></li> <li><p><a href="https://developer.apple.com/documentation/swiftdata"><strong>Swift Data</strong></a><strong>:</strong> All persistence uses Swift Data with <code>@Model</code> types, avoiding Core Data's complexity for straightforward data models.</p></li> <li><p><strong>Type Safety: </strong>Type safety everywhere. E.g., in the network layer, <code>NetworkRequest&lt;Request, Response&gt;</code> provides compile-time safety for API calls. DTOs in the OMGAPI package define API contracts explicitly.</p></li> </ul> <h2 id="data-flow-dtos-to-domain-models"> <a href="#data-flow-dtos-to-domain-models" class="anchor" title="Link to this heading" aria-hidden="true"></a>Data Flow: DTOs to Domain Models</h2> <p>Data flows through distinct representations at each layer:</p> <pre class="hljs-highlight"><code class="language-bash">Network API ↓ DTOs (OMGAPI package) ← Codable, API-shaped ↓ Repository ← Maps to domain/persistence models ↓ Domain Models (@Model or structs) ↓ ViewModels / Views</code></pre> <p>The OMGAPI package contains all request and response DTOs, matching the API structure exactly. Network Services return these DTOs unchanged. Repositories map DTOs to domain models or persistence models, applying business logic during transformation.</p> <p>This decoupling means API changes don't ripple through all layers. The repository acts as an anti-corruption layer, translating between the external API contract and internal domain representation.</p> <h2 id="streaming-based-caching"> <a href="#streaming-based-caching" class="anchor" title="Link to this heading" aria-hidden="true"></a>Streaming-Based Caching</h2> <p>Repositories coordinate data synchronization through <code>AsyncStream</code> patterns:</p> <pre class="hljs-highlight"><code class="language-swift"><span class="hljs-keyword">actor</span> <span class="hljs-title class_">PURLsRepository</span>: <span class="hljs-title class_ inherited__">PURLsRepositoryProtocol</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">startPURLsSync</span>() { streamTask <span class="hljs-operator">=</span> <span class="hljs-type">Task</span> { [<span class="hljs-keyword">weak</span> <span class="hljs-keyword">self</span>] <span class="hljs-keyword">in</span> <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> <span class="hljs-keyword">self</span> <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> } <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> purls <span class="hljs-keyword">in</span> networkService.purlsStream() { <span class="hljs-keyword">let</span> storablePurls <span class="hljs-operator">=</span> purls.map { purlResponse <span class="hljs-keyword">in</span> <span class="hljs-type">StorablePURL</span>(address: current, purlResponse: purlResponse) } <span class="hljs-keyword">try</span> <span class="hljs-keyword">await</span> persistenceService.storePURLs(purls: storablePurls) } } } }</code></pre> <p>Network operations emit data through streams. Repositories listen and automatically persist incoming data. Views query Swift Data directly using <code>@Query</code>, receiving updates automatically through SwiftData's observation.</p> <p>This pattern provides automatic synchronization with minimal manual coordination. Data flows naturally from network through repositories to local storage and UI, with the repository managing the stream lifecycle.</p> <h2 id="dependency-injection"> <a href="#dependency-injection" class="anchor" title="Link to this heading" aria-hidden="true"></a>Dependency Injection</h2> <p>Feature integration happens through a lightweight dependency injection container. The main application's <code>TritonEnvironment</code> registers infrastructure and App Factory factories:</p> <pre class="hljs-highlight"><code class="language-swift"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">TritonEnvironment</span>: <span class="hljs-title class_ inherited__">TritonEnvironmentProtocol</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> container <span class="hljs-operator">=</span> <span class="hljs-type">DependencyContainer</span>() <span class="hljs-keyword">var</span> statusAppFactory: <span class="hljs-type">StatusAppFactory</span> { container.resolve() } <span class="hljs-keyword">init</span>() { container.register( type: <span class="hljs-type">StatusAppFactory</span>.<span class="hljs-keyword">self</span>, allocation: .static ) { container <span class="hljs-keyword">in</span> <span class="hljs-type">StatusAppFactory</span>( sessionService: container.resolve(), authSessionService: container.resolve(), networkClient: container.resolve() ) } } }</code></pre> <p>The container supports <code>.static</code> allocation for singletons and passes itself to factory closures for resolving dependencies. Type-safe resolution through <code>container.resolve()</code> catches missing registrations at compile time.</p> <p>Internally, each feature has its own environment which registers the services and repositories that belong to that feature. The feature's <code>AppFactory</code> receives infrastructure dependencies from the main application's container and uses them to initialize the feature's environment.</p> <p>It's worth noting that infrastructure services like Session Service, Auth Session Service, and Network Client follow a specific module pattern. Features depend only on protocol-defining interface modules; for example, a feature imports <code>SessionServiceInterface</code> but never <code>SessionService</code> directly. Only the main application links against modules containing concrete implementations. This separation ensures features remain decoupled from infrastructure implementation details, depending solely on contracts.</p> <h2 id="pure-swiftui"> <a href="#pure-swiftui" class="anchor" title="Link to this heading" aria-hidden="true"></a>Pure SwiftUI</h2> <p>The entire UI is SwiftUI. No AppKit or UIKit<sup id="fnref:2"><a class="footnote-ref" data-id="e851db9f-af17-43bd-bc20-70bebbfc1ce9" href="#fn:2">2</a></sup> anywhere. A Design System package provides shared components: view modifiers, toolbar items, colors, and spacing constants. Features compose from these building blocks, ensuring consistency across the application.</p> <p>SwiftUI Previews enable rapid development. I use the <a href="https://martinfowler.com/bliki/ObjectMother.html">Mother Object</a> pattern to create fixtures:</p> <pre class="hljs-highlight"><code class="language-swift"><span class="hljs-keyword">enum</span> <span class="hljs-title class_">StatusViewModelMother</span> { <span class="hljs-meta">@MainActor</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">func</span> <span class="hljs-title function_">makeStatusViewModel</span>( <span class="hljs-params">statuses</span>: [<span class="hljs-type">Status</span>] <span class="hljs-operator">=</span> <span class="hljs-type">StatusMother</span>.makeStatuses(), <span class="hljs-params">isLoading</span>: <span class="hljs-type">Bool</span> <span class="hljs-operator">=</span> <span class="hljs-literal">false</span> ) -&gt; <span class="hljs-type">StatusViewModel</span> { .<span class="hljs-keyword">init</span>( statuses: statuses, isLoading: isLoading, repository: <span class="hljs-type">StatusRepositoryMother</span>.makeRepository() ) } }</code></pre> <p>These Mother Objects serve double duty: SwiftUI Previews during development and test fixtures for unit tests. Ninety percent of views have multiple preview states, catching UI issues immediately.</p> <pre class="hljs-highlight"><code class="language-swift">#<span class="hljs-type">Preview</span>(<span class="hljs-string">"Not Loading"</span>) { <span class="hljs-type">StatusView</span>( viewModel: <span class="hljs-type">StatusViewModelMother</span>.makeStatusViewModel() ) .frame(width: <span class="hljs-number">420</span>) } #<span class="hljs-type">Preview</span>(<span class="hljs-string">"Loading"</span>) { <span class="hljs-type">StatusView</span>( viewModel: <span class="hljs-type">StatusViewModelMother</span>.makeStatusViewModel( isLoading: <span class="hljs-literal">true</span> ) ) .frame(width: <span class="hljs-number">420</span>) }</code></pre> <h2 id="features-built-on-this-architecture"> <a href="#features-built-on-this-architecture" class="anchor" title="Link to this heading" aria-hidden="true"></a>Features Built on This Architecture</h2> <p>The architecture's true test comes from the features it supports. Triton implements a complete suite of <a href="https://omg.lol">omg.lol</a> services, each as an isolated feature package:</p> <ul> <li><p><strong>Statuslog</strong> provides a timeline of status updates with local muting capabilities. Users can mute specific addresses or keywords, useful for avoiding spoilers or filtering unwanted content.</p></li> <li><p><strong>PURLs</strong> manages permanent URLs. Users create, edit, and delete PURLs, with context menus for copying URLs in various formats (plain URLs or Markdown links) and sharing via the native share sheet or directly to Statuslog.</p></li> <li><p><strong>Web Page and Now Page</strong> features let users view and edit their <a href="https://omg.lol">omg.lol</a> pages directly from the app. Changes publish instantly, eliminating the need to use a web browser for content updates.</p></li> <li><p><strong>Weblog</strong> offers full-featured blog management. Users write new posts, edit existing entries, and delete old content. Each entry has a context menu with sharing options and the ability to open posts in a browser.</p></li> <li><p><strong>Pics</strong> integrates with <a href="https://some.pics">some.pics</a> for image management. Users upload new pictures, delete existing ones, and share photos through various formats—Some Pics URLs, direct image URLs, or Markdown image syntax.</p></li> <li><p><strong>Pastebin</strong> handles code snippets and text pastes. Users create new pastes with syntax highlighting support, edit existing snippets, and share public pastes through context menus. Private pastes remain accessible only within the app.</p></li> </ul> <p>The architecture supports multiple <a href="https://omg.lol">omg.lol</a> addresses. Users switch between addresses without friction, with each feature  respecting the currently selected address. Session management and authentication happen  through the Auth Session infrastructure package.</p> <h2 id="consequences-and-tradeoffs"> <a href="#consequences-and-tradeoffs" class="anchor" title="Link to this heading" aria-hidden="true"></a>Consequences and Tradeoffs</h2> <p>This architecture requires upfront planning before writing feature code. Creating a new feature means setting up package structure, defining protocols, and implementing layers sequentially. There's more initial overhead than throwing everything in a single target.</p> <p>But the benefits justify this cost. The architecture prevents common problems:</p> <ul> <li><p><strong>Circular dependencies:</strong> Impossible due to SPM's acyclic dependency enforcement</p></li> <li><p><strong>Tight coupling:</strong> Layer boundaries and feature isolation prevent accidental coupling</p></li> <li><p><strong>Difficult testing:</strong> Protocol boundaries enable mock implementations at every layer</p></li> <li><p><strong>Architectural erosion:</strong> Compiler enforces the architecture continuously</p></li> </ul> <p>Parallel development becomes straightforward. As I mentioned above, I built Triton alone, but in a larger codebase with multiple engineers, they can work on different features without conflicts because features don't share code. When features need common functionality, it must be explicitly extracted to infrastructure packages, making shared dependencies visible and intentional.</p> <p>And the modular structure scales naturally. Adding a new feature follows established patterns. The architecture accommodates growth without major restructuring because the boundaries were established from the beginning.</p> <h2 id="conclusion"> <a href="#conclusion" class="anchor" title="Link to this heading" aria-hidden="true"></a>Conclusion</h2> <p>Building a modular, feature-isolated, layered architecture from day one establishes patterns that serve the project long-term. Swift Package Manager enforces boundaries at compile time. Strict layering with one-way dependencies prevents coupling. </p> <p>Modern Swift features like actors, async/await, <code>@Observable</code>, and Swift Data work naturally within this structure. The protocol-first approach at boundaries enables testing while keeping implementation details private. Streaming-based caching provides automatic synchronization with minimal coordination code.</p> <p>The architecture isn't <em>free</em>. It requires initial setup and ongoing discipline. But it prevents common problems before they occur and scales naturally as the codebase grows. For a project intended to last years, this investment pays off.</p> <p>I'm looking forward to releasing the application. I'm currently reviewing the documentation and the ADRs, and have to prepare, sign, and notarize a build to publish on GitHub. Hopefully soon.</p> <ol class="footnotes"> <li id="fn:1" data-id="134a505c-d05f-4260-bb4c-4a59be9562d7"><p>And better yet, <a href="https://omg.lol">omg.lol</a> was created and is maintained by <a href="https://adam.omg.lol/">Adam</a>, a genuinely good person who stands for the right causes and created one of the most welcoming communities on the web.</p></li> <li id="fn:2" data-id="e851db9f-af17-43bd-bc20-70bebbfc1ce9"><p>The application compiles for iOS and iPadOS as well, but I still have to refactor the UI to look better on these systems.</p></li> </ol> </div> I've been building Triton, a native macOS client for omg.lol. For those unfamiliar, omg.lol is an amazing web service1 that provides a collection of fun, useful tools like status updates, permanent...