<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>twocentstudios</title>
    <description>A coding blog covering iOS, Swift, and other programming topics.</description>
    <link>https://twocentstudios.com/blog/tags/flyio/index.html</link>
    <atom:link href="https://twocentstudios.com/blog/tags/flyio/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sun, 01 Feb 2026 00:12:37 -0600</pubDate>
    <lastBuildDate>Sun, 01 Feb 2026 00:12:37 -0600</lastBuildDate>
    <generator>Jekyll v3.9.3</generator>
    
      <item>
        <title>Vibe Coding a Rental Apartment Search Management App</title>
        <description>&lt;p&gt;I’ve been apartment hunting here in the Tokyo-area with my girlfriend. We’ve been sending links to various rental property listings back and forth in LINE (messaging app) and emailing with brokers. In a chat interface, it was hard keeping up with the status of each of the properties we’d seen, we wanted to see, we’d inquired about, etc. Classic project management problem.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-suumo-listing-example.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Example SUUMO property listing page slightly edited for clarity&quot; title=&quot;Example SUUMO property listing page slightly edited for clarity&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Example SUUMO property listing page slightly edited for clarity&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I decided to skip the step of putting each property into a shared spreadsheet and jump straight to vibe coding a web app with Claude Code. I’ve never worked on a full-stack TypeScript app before, and my impression is that LLMs are most proficient at it, so that’s what I went with.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-finished-desktop-interface.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Finished desktop interface for Bukkenlist&quot; title=&quot;Finished desktop interface for Bukkenlist&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Finished desktop interface for Bukkenlist&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;My goal was to create a shared space where we could keep track of the status of listings, add new ones easily, do calculations like 2-year amortized cost, keep a notes and ratings field for each of us, see all the salient points of a property at a glance, and archive properties that we decide against or are already taken.&lt;/p&gt;

&lt;p&gt;After a day of work, it supported scraping SUUMO listings and worked on mobile and desktop web. Another 2 half-days of work and it supports 4 listing sites, maps, expired listings, and English/Japanese localization. I called it Bukkenlist 物件リスト.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-day1-and-final-comparison.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;App progression from end of day 1 (left) to final polished version (right) - there&apos;s not much visual difference&quot; title=&quot;App progression from end of day 1 (left) to final polished version (right) - there&apos;s not much visual difference&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;App progression from end of day 1 (left) to final polished version (right) - there&apos;s not much visual difference&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This was “vibe coding” in the customary definition of “not looking at the generated code at all”. I see the code scrolling past in the terminal window but I’m letting Claude commit it after I check that the rendered result looks and works as intended in the browser window. For this project, I’m playing the role of product manager and QA engineer. However, I did make the decisions about using SQLite for storage, the schema, and the deployment strategy. And I helped Claude dig itself out of holes in the way only an engineer can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The TL;DR:&lt;/strong&gt; In 2 working days I produced a completely functional web app with much better usability than the spreadsheet it would compete with. Using an AI tool like Claude Code aimed at professionals, it’s hard for me to imagine someone with no coding background being able to get to the same finish line I did. But with the existing cadre of no-code AI tools, perhaps this would be a perfectly scoped project.&lt;/p&gt;

&lt;h2 id=&quot;the-full-development-process&quot;&gt;The full development process&lt;/h2&gt;

&lt;p&gt;I have a Claude Code $100/mo Max subscription. I used the pattern of using “plan mode” with Opus aggressively to ensure proper context gathering and then “accept edits” mode with Sonnet to execute the plan. These were long sessions, so I actually blew through my usage limits once or twice with 1 or 2 hours remaining (with my usual Swift projects I hadn’t hit the Max limit for Sonnet before). At those times, I switched over to the nascent OpenAI Codex CLI (with a $20/mo Pro plan) to see how it did. Everything about Codex still feels months behind Claude Code, but it did handle some of the tasks I threw at it well enough.&lt;/p&gt;

&lt;h3 id=&quot;day-1&quot;&gt;Day 1&lt;/h3&gt;

&lt;p&gt;Learning from some &lt;a href=&quot;/2025/06/22/vinylogue-swift-rewrite/&quot;&gt;past experiments&lt;/a&gt;, I decided this time to be more intentional with my initial getting-started prompts. I didn’t dump my entire vision for the app onto the model and have it create a full product requirements doc and phase-by-phase development plan. I thought staying in the loop would ensure the best chance of success and even minor scalability.&lt;/p&gt;

&lt;p&gt;SUUMO listing scraping was the most risky part, so I had it start by creating some infrastructure around fetching and parsing the HTML for a few example listings and comparing the results with the values I’d plucked out by hand.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-console-parsing-results.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Console showing results from the initial SUUMO listing parsing&quot; title=&quot;Console showing results from the initial SUUMO listing parsing&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Console showing results from the initial SUUMO listing parsing&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Only after the parsing seemed relatively robust did I have Claude create the initial structure of the Express.js backend and React frontend. It used VITE but I only sort of know what role that plays. The first renderable version was a text field for the SUUMO URL, a submit button, and then a list of the keys and values parsed out.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-first-working-interface.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;First working interface with URL input field and parsed listing key/values&quot; title=&quot;First working interface with URL input field and parsed listing key/values&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;First working interface with URL input field and parsed listing key/values&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then it was time to add persistence. This was the part where I &lt;em&gt;should&lt;/em&gt; have first decided on hosting, got that set up, &lt;em&gt;then&lt;/em&gt; decided on the most low maintenance storage solution. Instead, I chose SQLite, which I’ve been interested in lately and have &lt;a href=&quot;/2025/07/02/swift-vapor-fly-io-sqlite-config/&quot;&gt;already deployed&lt;/a&gt; successfully on Fly.io.&lt;/p&gt;

&lt;p&gt;With my engineer hat on, I made the initial decision to have Claude go with a mixed schema-less approach, storing a generous amount of metadata about each property in named columns, but then having a dumping ground JSON column with all the parsed key/value data. Hard to say whether that’s made my life easier or harder while adding new listing sources. For a personal project with 2 users, I think it was a fine decision. For a real production site, I can already tell it would be a nightmare to maintain.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Final schema of the `scrapes` table
     cid  name           type     notnull  dflt_value  pk
     ---  -------------  -------  -------  ----------  --
     0    id             INTEGER  0                    1
     1    url            TEXT     1                    0
     2    property_name  TEXT     0                    0
     3    scraped_data   TEXT     1                    0
     4    created_at     INTEGER  1                    0
     5    status         TEXT     0                    0
     6    archived       INTEGER  1        0           0
     7    kiyoko_notes   TEXT     0                    0
     8    chris_notes    TEXT     0                    0
     9    kiyoko_rating  INTEGER  0                    0
     10   chris_rating   INTEGER  0                    0
     11   source_site    TEXT     1        &apos;suumo&apos;     0
     12   color_id       TEXT     0                    0
     13   latitude       REAL     0                    0
     14   longitude      REAL     0                    0
     15   expired        INTEGER  1        0           0
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;From there I had Claude build out the master/detail list in desktop mode. It had little trouble putting together a passible design.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-initial-master-detail-view.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Initial master-detail interface showing property list and selected property detail&quot; title=&quot;Initial master-detail interface showing property list and selected property detail&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Initial master-detail interface showing property list and selected property detail&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I added delete and refresh support since those were helpful in manual testing. Refresh should re-run the scraping and parsing and replace all the fields with the freshly parsed content.&lt;/p&gt;

&lt;p&gt;Then it was kind of the fun part: pushing around the fields in the UI to make it more pretty and readable.&lt;/p&gt;

&lt;p&gt;Next I had to add image carousel support which was surprisingly easy. I prompted Claude to do some extra research for best practices before deciding on a solution.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-ui-reorganizing-carousel.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Reorganized UI with image carousel&quot; title=&quot;Reorganized UI with image carousel&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Reorganized UI with image carousel&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I added deterministic unique color generation for each property based on its unique ID mapped to a hue value 0-359 in HSV. I use this technique often in projects as a nice touch to make resources easier to identify.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-color-id-support.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Properties with unique color IDs for intuitive identification&quot; title=&quot;Properties with unique color IDs for intuitive identification&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Properties with unique color IDs for intuitive identification&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can’t sightread Japanese as fast as I can English, so I had Claude add full UI localization in both English and Japanese to the entire app and have it save the preference in local storage. This helped speed up QA of parser errors going forward.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-localization-support.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;English/Japanese localization toggle&quot; title=&quot;English/Japanese localization toggle&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;English/Japanese localization toggle&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I wanted to highlight the at-a-glance parts each property that were especially important to the two of us, so I added those in big font next to the image carousel.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-at-a-glance-properties.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;First version of the at-a-glance property details&quot; title=&quot;First version of the at-a-glance property details&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;First version of the at-a-glance property details&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;From here it was a lot of polish. I felt like I was in full product manager flow-state, just picking off the next obvious change in the UI and prompting Claude to have a go at it.&lt;/p&gt;

&lt;p&gt;The whole point of the app was to facilitate our apartment search process, which ultimately meant appending our own information to listings. I added an open-ended status field to track things like “requested viewing” or “viewing on 8/24”. I added an open-ended notes field for each of us, then a 4-level rating system. In the notes field, we’ve been adding merits/demerits. The rating system is an easy way to clearly communicate our enthusiasm towards each property and see it at a glance in the sidebar.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-notes-and-ratings.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;First version of the notes and rating system for each (hardcoded) user&quot; title=&quot;First version of the notes and rating system for each (hardcoded) user&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;First version of the notes and rating system for each (hardcoded) user&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A cool feature I’d very loosely prototyped with a single ChatGPT query a few days previously was an “amortized cost” field, calculated from several fields. There are so many disparate fees for each listing (monthly rent, management fees, security deposit, key money, parking fee, etc.) that it’s hard to do an apples-to-apples comparison of how expensive properties actually are. It’s elementary school math, but just annoying to do.&lt;/p&gt;

&lt;p&gt;It was pretty simple to add this field: parse out the semantic values, multiply the monthly costs by the lease term, add the one-time costs, then divide by the lease term to get the overall monthly cost.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-all-in-cost.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Amortized cost calculation for true monthly expense comparison&quot; title=&quot;Amortized cost calculation for true monthly expense comparison&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Amortized cost calculation for true monthly expense comparison&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I was on the fence about whether to build out a full user table and authentication system. It may have been worth seeing whether Claude could have one-shotted multi-user support. Instead, I opted for a simple password auth and full editing support for any field. I’m pretty happy with this solution and proud of myself for not going overboard on the spec. It’s much easier to share a single password than deal with a create account flow on multiple devices or while on the go. I set some strict rate limits for password attempts and page requests in general and know that if the site gets hacked and trashed somehow it’s not a huge deal.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-login-screen.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Simple password login screen to gate the whole app&quot; title=&quot;Simple password login screen to gate the whole app&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Simple password login screen to gate the whole app&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It was finally time for deployment! I was definitely procrastinating on this, but I wanted to get it online before I went to bed.&lt;/p&gt;

&lt;p&gt;Looking into some of the common free hosting services that target JS, I realized Vercel was serverless and I’d need a different solution for the SQLite storage. I could have tried Turso for SQLite hosting, but signing up for 2 services felt like too much complexity. I went back to Fly.io since I have some experience with them and an existing account and all the CLI stuff installed.&lt;/p&gt;

&lt;p&gt;Claude was happy to set up all the deployment stuff and mostly one-shotted it. The big issue came with my underlying scraping implementation. Scraping was based on &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt; which needs to spin up a full Chromium instance and that takes 10+ seconds on a 2 GB machine. I have aggressive suspension set for my Fly.io instances which means this heavy startup cost needs to be paid every time a new listing is added. I also didn’t want to pay for a full 2 GB machine on Fly.&lt;/p&gt;

&lt;p&gt;I started another vibe spike to replace Playwright with Puppeteer and a lighter Chromium fork based on &lt;a href=&quot;https://vercel.com/guides/deploying-puppeteer-with-nextjs-on-vercel&quot;&gt;this guide&lt;/a&gt; from Vercel. With a lot of trial and error (including rewriting the parser), I got the memory requirement down to 512 MB at the cost of 30+ second scraping.&lt;/p&gt;

&lt;p&gt;At this point, I took a step back and thought about whether I actually needed a full Chromium-based scraper. After all, I’d never actually verified whether these sites were doing enough JS rendering to require it. I don’t have a lot of experience with scrapers and this project was an attempt to fix that. I had Claude do yet another spike with some initial research as to what the most common tools were for low-resource scraping and it chose &lt;a href=&quot;https://github.com/jsdom/jsdom&quot;&gt;JSDOM&lt;/a&gt;. After rewriting the parser yet again, it turned out this worked fine and was super fast and easily deployable to a tiny 256 MB machine.&lt;/p&gt;

&lt;p&gt;If I’d have tried deploying immediately after finishing the very first version of the scraper, I’d have had a much easier time. But I also realized I wouldn’t have had much invested at this point, and my motivation to continue may not have survived this deployment slog. An interesting paradox! In theory I would have saved hours, but in practice I may not have shipped anything. It’s also possible I would have chosen a different deployment service that affected my choice of persistence solution, etc. Decision ordering really matters, and I’m trying to get better at it. But also, LLMs make spikes, backtracking, and rewrites so low-cost/low-effort that as long as you’re willing to ignore sunk costs and your motivation survives you can end up with much more optimal solutions in the long run.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-deployed-production-end-day1.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;App successfully deployed to production at the end of day 1 (Japanese interface)&quot; title=&quot;App successfully deployed to production at the end of day 1 (Japanese interface)&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;App successfully deployed to production at the end of day 1 (Japanese interface)&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;My first commit was 4pm on Saturday and my last commit before going to sleep was 6am Sunday. I’d (re)watched 3 seasons of Silicon Valley in the background. I sent my girlfriend a link and the password and went to bed.&lt;/p&gt;

&lt;h2 id=&quot;day-2--3&quot;&gt;Day 2 &amp;amp; 3&lt;/h2&gt;

&lt;p&gt;I woke up a couple hours later and made pancakes and got a message from my girlfriend with links to listings from 2 other services. So it was time to add support for more listing sources!&lt;/p&gt;

&lt;p&gt;I had Claude do a refactor of the scraper in preparation for adding multi-service support. Again, this was vibe coding so I had no idea how well it did, but I trusted it. This took about an hour. I gave it a link to a new listing and had it run its scraping parsing iterative procedure to write an initial version of the parser. According to the git logs it took about an hour to write the two new scrapers and a guide for itself for writing future scrapers.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-multi-source-support.jpg&quot; width=&quot;&quot; height=&quot;300&quot; alt=&quot;Multi-source support showing listings from different rental websites&quot; title=&quot;Multi-source support showing listings from different rental websites&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Multi-source support showing listings from different rental websites&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I realized the two of us would need to use my new site from our iPhones, so I added mobile support. This was way way faster than I expected. It took a bit more Claude coercing the next day to get it fully optimized, but the first attempt was definitely usable.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-initial-mobile-interface.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Initial mobile interface with separate property list screen (left) and property detail screen (right)&quot; title=&quot;Initial mobile interface with separate property list screen (left) and property detail screen (right)&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Initial mobile interface with separate property list screen (left) and property detail screen (right)&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Sunday was a half-day. On Monday, I put in another half-day optimizing the mobile layout, adding another source, adding listing expiry support, maps support, and deep linking support.&lt;/p&gt;

&lt;p&gt;Maps support was actually the most difficult single-feature I’d vibe coded for the whole project. Scraping the coordinates for a listing wasn’t too bad, but deciding on the maps provider and implementation was difficult.&lt;/p&gt;

&lt;p&gt;At first, I was planning on rendering out a static map image on the backend during property add because it seemed simplest and lowest cost. But since I already have an Apple Developer account, using the MapKit JS API was free so I went with that.&lt;/p&gt;

&lt;p&gt;Turns out that Claude had a pretty awful time integrating the MapKit JS library. This is where I ran into a lot of frustration with vibe coding and not having any idea how React works, how JS library loading should work, how environment variables work on the client side, how JS library token authorization should work, and more. I was in thrashing mode with Claude, watching it implement “fixes” that seemed dubious even to me, a JS novice, and inevitably did not work at all.&lt;/p&gt;

&lt;p&gt;I had to get a lot more hands on and spent a long dev cycle restarting the dev server, deploying to production over and over, copying and pasting browser console logs, and adding and removing secrets from the Fly.io admin page.&lt;/p&gt;

&lt;p&gt;In the end, we got it working, but it’s hard for me to say &lt;em&gt;why&lt;/em&gt; Claude struggled so hard with this particular task and how I could have approached it differently.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/bukkenlist-maps-support.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Map showing the property location and its relationship to closest station&quot; title=&quot;Map showing the property location and its relationship to closest station&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Map showing the property location and its relationship to closest station&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;iteration-loop-workflow&quot;&gt;Iteration loop workflow&lt;/h2&gt;

&lt;p&gt;For my overall development experience, the local build and serve process was more effortless than iOS, but I found Claude’s new background Bash processes feature frustrating.&lt;/p&gt;

&lt;p&gt;Claude would write some code, start a background server, test the code by making some calls to the server, make some code changes, then not realize that the server needed to be restarted and get stuck in a “why isn’t the output changing” loop. I’d need to keep an eye out for this and intervene.&lt;/p&gt;

&lt;p&gt;After a while I took control of starting/stopping the dev server in a separate terminal window, but Claude would ignore this and keep trying to do its own thing. If I was working on this full time I would certainly spend some time making this flow more efficient. I dealt with the paper cuts.&lt;/p&gt;

&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h2&gt;

&lt;p&gt;We still have at least a few weeks left in the apartment hunting process. The less we need to use this app the happier I will be.&lt;/p&gt;

&lt;p&gt;For the time investment, I’d consider this app overkill. It’s useful individually, but for a production app it wouldn’t work as scraping is presumably against the TOSes. The parsing is sloppy. There’s no user account system. There’s no sharing system. There’s no base SaaS functionality. Even individually, it would have taken a lot less time to simply enter the key fields into a spreadsheet manually.&lt;/p&gt;

&lt;p&gt;But this was a good experience seeing how feasible vibe coding is for someone with my background. There were several points in the process where I hit that beautiful flow state and really loved it. But there were also stretches where Claude was thrashing and I was losing my patience. Or when I was the tool of the LLM clicking boxes in admin panels or doing visual QA while it was doing the interesting architecture and coding tasks.&lt;/p&gt;

&lt;p&gt;One thing’s for certain: I never would have attempted a project like this without an LLM agent. If I did, I probably would have lost motivation after finishing the first scraper. I probably would have used a technology I already knew even if it was not the most prudent choice in 2025.&lt;/p&gt;

&lt;p&gt;I’d like to try some of the more “batteries included” vibe coding environments (e.g. Lovable, Bolt, Replit, V0) to do a similarly scoped project in the near future. I’m most comfortable with Claude Code at the moment because I’m used to the freedom + sharp edges combination. But it’s hard for me to imagine a non-programmer or even a junior programmer being able to dig themselves out of the holes I found myself in a few times. There’s just a &lt;em&gt;lot&lt;/em&gt; to know still to get an MVP designed, developed, and deployed.&lt;/p&gt;

&lt;p&gt;I can see how using a vibe coding environment with less freedom but more well-paved integrations could prevent dead-ends and thrashing and bad developer experience. Maybe within the year both Claude Code and the vibe coding platforms will have converged into providing decent enough support for users of any background.&lt;/p&gt;

</description>
        <pubDate>Mon, 18 Aug 2025 12:46:25 -0500</pubDate>
        <link>https://twocentstudios.com/2025/08/18/vibe-coding-a-rental-apartment-search-management-app/</link>
        <guid isPermaLink="true">https://twocentstudios.com/2025/08/18/vibe-coding-a-rental-apartment-search-management-app/</guid>
        
        <category>claude</category>
        
        <category>web</category>
        
        <category>react</category>
        
        <category>typescript</category>
        
        <category>vibecoding</category>
        
        <category>app</category>
        
        <category>javascript</category>
        
        <category>nodejs</category>
        
        <category>flyio</category>
        
        <category>sqlite</category>
        
        <category>debugging</category>
        
        
      </item>
    
      <item>
        <title>Configuring Swift Vapor on Fly.io with SQLite</title>
        <description>&lt;p&gt;This post is a guide for getting a &lt;a href=&quot;https://vapor.codes/&quot;&gt;Swift Vapor&lt;/a&gt; server-side app up and running on &lt;a href=&quot;https://fly.io/&quot;&gt;Fly.io&lt;/a&gt; with SQLite as the database provider. The target audience is Swift developers who are inexperienced with servers and deployment.&lt;/p&gt;

&lt;p&gt;I’m assuming you’ve already chosen &lt;strong&gt;Vapor&lt;/strong&gt;, &lt;strong&gt;SQL&lt;/strong&gt;, and &lt;strong&gt;Fly.io&lt;/strong&gt; as your tools of choice and therefore will not discuss any of their tradeoffs.&lt;/p&gt;

&lt;p&gt;The below setup using SQLite avoids the operational complexity of maintaining a full Postgres server. Especially as a beginner that does not need the full breadth of functionality Postgres offers beyond SQLite. This is a worthwhile tradeoff for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Toy apps that still need 24/7 network access&lt;/li&gt;
  &lt;li&gt;Prototypes and proof-of-concepts intended for a limited audience&lt;/li&gt;
  &lt;li&gt;Bespoke apps for you and your friends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the primary constraint of using SQLite (in the strategy described in this post) is that you must only have one server instance (since that server will be hosting both the app and the database). When just starting out this is arguably for the best; your server app implementation can also be simplified by assuming no parallelization.&lt;/p&gt;

&lt;p&gt;Fly.io’s &lt;a href=&quot;https://fly.io/docs/about/pricing&quot;&gt;pricing&lt;/a&gt; is pay-as-you-go so it’s hard predict exactly how much you, the reader, will be on the hook for. As of this writing, provisioning a system described in this post &lt;em&gt;that is stopped, serving zero requests&lt;/em&gt; would be $0.30 USD per month ($0.15/GB for the Machine and $0.15/GB for the Volume). Then you will pay usage based on how long your machine is awake for and how much bandwidth you use. You should monitor your usage closely. Going along with the intended use cases, this post will assume &lt;strong&gt;you want the absolute cheapest of everything&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’re looking for a more robust database solution in the same vein, my &lt;a href=&quot;/2025/06/30/swift-vapor-fly-managed-postgres/&quot;&gt;previous post&lt;/a&gt; discusses &lt;a href=&quot;https://fly.io/docs/mpg/overview/&quot;&gt;Fly.io Managed Postgres Service&lt;/a&gt; but is not as thorough a walkthrough as this post. Note: there’s &lt;em&gt;another&lt;/em&gt; Fly.io Postgres-related service called &lt;a href=&quot;https://fly.io/docs/postgres/&quot;&gt;Fly Postgres&lt;/a&gt; which is more like Postgres configured into a separate Fly.io App instance (make sure not to get tripped up when reading the docs and forum posts).&lt;/p&gt;

&lt;p&gt;Strategies for automated backups, automatic failovers, high availability, or basically anything you need for a production deployment are mentioned briefly at the end of the post. Note also that the particular setup described in this post specifically disallows multiple machines; you are locked into one machine running in one region (great for limiting complexity, awful for production-quality customer service).&lt;/p&gt;

&lt;p&gt;On successful deployment, you’ll have an app accessible via the public interface at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myapp.fly.dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;

&lt;p&gt;This guide assumes you have:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A working Vapor app (we’ll use “myapp” as an example)&lt;/li&gt;
  &lt;li&gt;Basic familiarity with the &lt;a href=&quot;https://fly.io/docs/flyctl/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly&lt;/code&gt; CLI&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Your app already builds and runs locally with or without an existing Postgres integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will not cover any sort of data migration.&lt;/p&gt;

&lt;h2 id=&quot;step-1-project-structure&quot;&gt;Step 1: Project Structure&lt;/h2&gt;

&lt;p&gt;Your Vapor project should look like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;myapp/
├── Package.swift
├── Dockerfile
├── fly.toml
├── Sources/
│   └── App/
│       ├── configure.swift
│       ├── routes.swift
│       └── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2 id=&quot;step-2-configure-package-dependencies&quot;&gt;Step 2: Configure Package Dependencies&lt;/h2&gt;

&lt;p&gt;First, update your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Package.swift&lt;/code&gt; to use SQLite (instead of PostgreSQL):&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Package.swift&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;myapp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;platforms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;macOS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;v12&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://github.com/vapor/vapor.git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;4.0.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://github.com/vapor/fluent.git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;4.0.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://github.com/vapor/fluent-sqlite-driver.git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;4.0.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &amp;lt;- SQLite driver&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;targets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;App&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Fluent&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;fluent&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;FluentSQLiteDriver&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;fluent-sqlite-driver&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &amp;lt;- SQLite driver&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Vapor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;vapor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executableTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Run&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;App&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2 id=&quot;step-3-configure-database-in-vapor&quot;&gt;Step 3: Configure Database in Vapor&lt;/h2&gt;

&lt;p&gt;Update your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;configure.swift&lt;/code&gt; to use environment-based database paths:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Sources/App/configure.swift&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Fluent&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;FluentSQLiteDriver&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ← Import SQLite driver&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Vapor&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;configure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Configure SQLite database with environment-based paths&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;databasePath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;production&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Production: Use volume-mounted path&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;databasePath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/data/db.sqlite&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Local development: Use project root&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;databasePath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;./db.sqlite&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;databases&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sqlite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;databasePath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sqlite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// Add your migrations here&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// app.migrations.add(CreateMyAppModels())&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;routes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;configure.swift concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Environment detection&lt;/strong&gt;: Vapor sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.environment&lt;/code&gt; based on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--env production&lt;/code&gt; flag we pass to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;serve&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;migrate&lt;/code&gt; commands&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Volume mount&lt;/strong&gt;: Production SQLite file lives on persistent storage at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Local development&lt;/strong&gt;: Local/Test SQLite file is created in your project directory&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;step-4-update-dockerfile&quot;&gt;Step 4: Update Dockerfile&lt;/h2&gt;

&lt;p&gt;If you do not know what you’re doing, you should use the latest &lt;a href=&quot;https://github.com/vapor/template/blob/0330dd9f4d1314ea122c90f3f3db3a24a2d97761/Dockerfile&quot;&gt;Vapor Dockerfile template&lt;/a&gt;. Then make the following modifications to include SQLite3 client tools and create the data directory:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# ...

# Install system packages including sqlite3 for database access
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true &amp;amp;&amp;amp; \
    apt-get -q update &amp;amp;&amp;amp; apt-get -q dist-upgrade -y &amp;amp;&amp;amp; \
    apt-get -q install -y ca-certificates tzdata sqlite3 &amp;amp;&amp;amp; \
    rm -r /var/lib/apt/lists/*

# ...

# Copy built executable
# ...

# Create data directory for SQLite database with proper ownership
RUN mkdir -p /data &amp;amp;&amp;amp; chown -R vapor:vapor /data

# ...

# Start the Vapor service when the image is run, running db migrations if necessary, and default to listening on 8080 in production environment
ENTRYPOINT [&quot;./&quot;]
CMD [&quot;serve&quot;, &quot;--auto-migrate&quot;, &quot;--env&quot;, &quot;production&quot;, &quot;--hostname&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8080&quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;SQLite-specific additions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqlite3&lt;/code&gt; (optional) inspect your db via the remote server console&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data&lt;/code&gt; directory creation with proper ownership&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--auto-migrate&lt;/code&gt; flag runs database migrations on startup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;step-5-configure-flytoml&quot;&gt;Step 5: Configure fly.toml&lt;/h2&gt;

&lt;p&gt;Create (via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly launch&lt;/code&gt;) or update your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly.toml&lt;/code&gt; configuration selectively:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# fly.toml&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;app&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;myapp&quot;&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;primary_region&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ord&quot;&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Choose a single region close to your users&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;kill_signal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;SIGINT&quot;&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;kill_timeout&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;5s&quot;&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[[vm]]&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;memory&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;256mb&quot;&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Lowest available memory &amp;amp; cpus&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;cpus&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[mounts]&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Volume configuration for persistent SQLite storage&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;myapp_db&quot;&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Matches the volume name you create next&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;destination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/data&quot;&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Matches the directory you created in Dockerfile&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[http_service]&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;internal_port&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8080&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;force_https&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;auto_stop_machines&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;stop&quot;&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Automatically stops machines when idle (save $$$)&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;auto_start_machines&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Automatically starts machines on first request&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;min_machines_running&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;      &lt;span class=&quot;c&quot;&gt;# &amp;lt;- Allow zero running machines when idle&lt;/span&gt;

  &lt;span class=&quot;nn&quot;&gt;[http_service.concurrency]&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;requests&quot;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;hard_limit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;soft_limit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;

  &lt;span class=&quot;nn&quot;&gt;[[http_service.checks]]&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;10s&quot;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;timeout&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;2s&quot;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;grace_period&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;5s&quot;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;method&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;get&quot;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/&quot;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;http&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Fly.io/fly.toml concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;App&lt;/strong&gt;: Holistic settings describing your application. If you had a production and staging, you’d have two Apps total with similar &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly.toml&lt;/code&gt; files. In our setup, one &lt;strong&gt;App&lt;/strong&gt; will &lt;em&gt;always&lt;/em&gt; contain one &lt;strong&gt;Machine&lt;/strong&gt; and one &lt;strong&gt;Volume&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;VM&lt;/strong&gt;: Virtual machine specifications (RAM, CPU).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Machine&lt;/strong&gt;: The actual running instance of your app. Pairs 1-to-1 with a &lt;strong&gt;Volume&lt;/strong&gt;. Is recreated fresh on each deploy.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Volume&lt;/strong&gt;: Persistent disk storage that survives deployments. This is where you can keep your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqlite.db&lt;/code&gt; file.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Auto-scaling&lt;/strong&gt;: Automatically stops/starts machines based on traffic. We set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto_stop_machines = &quot;stop&quot;&lt;/code&gt; to save money assuming that our app has significant idle periods. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto_start_machines = true&lt;/code&gt; automatically boots a machine when a request comes in. Startup takes about ~2s, so the compromise is that some user requests will be slower than if we kept the server cooking 24/7.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;step-6-create-flyio-app&quot;&gt;Step 6: Create Fly.io App&lt;/h2&gt;

&lt;p&gt;Initialize your Fly.io app. Do it from the web interface or using the CLI commands below:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create new Fly.io app&lt;/span&gt;
fly apps create myapp

&lt;span class=&quot;c&quot;&gt;# Or if app already exists, verify it&lt;/span&gt;
fly status -a myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/fly-io-apps-dashboard.png&quot; width=&quot;&quot; height=&quot;&quot; alt=&quot;Fly.io Apps dashboard showing your newly created app&quot; title=&quot;Fly.io Apps dashboard showing your newly created app&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Fly.io Apps dashboard showing your newly created app&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The Fly.io dashboard will show your app in the “Apps” section with a status indicator.&lt;/p&gt;

&lt;h2 id=&quot;step-7-create-storage-volume&quot;&gt;Step 7: Create Storage Volume&lt;/h2&gt;

&lt;p&gt;SQLite needs persistent storage that survives deployments.&lt;/p&gt;

&lt;p&gt;Create a &lt;strong&gt;Volume&lt;/strong&gt; via the web interface or the CLI command below:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create 1GB volume for SQLite database&lt;/span&gt;
fly volume create myapp_db --region ord --size 1 -a myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Volume concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Persistent storage&lt;/strong&gt;: Data survives app deployments and restarts.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Region-specific&lt;/strong&gt;: Must be in same region as your machine.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Size&lt;/strong&gt;: Start small with 1GB (you can expand later if needed).&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/fly-io-volumes-dashboard.png&quot; width=&quot;&quot; height=&quot;&quot; alt=&quot;Fly.io Volumes dashboard showing the newly created volume&quot; title=&quot;Fly.io Volumes dashboard showing the newly created volume&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Fly.io Volumes dashboard showing the newly created volume&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Verify the volume was created:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;fly volumes list -a myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;You should see output like:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ID                      STATE   NAME     SIZE REGION ZONE ENCRYPTED ATTACHED VM CREATED AT     
vol_abc123xyz           created myapp_db 1GB  ord    df19 true                  2 minutes ago
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2 id=&quot;step-8-deploy-to-flyio&quot;&gt;Step 8: Deploy to Fly.io&lt;/h2&gt;

&lt;p&gt;Deploy your app:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Deploy from your project root directory&lt;/span&gt;
fly deploy . -a myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;This will:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Build your Docker image (including compiling the Swift binary)&lt;/li&gt;
  &lt;li&gt;Create/update the app’s machine with the new image&lt;/li&gt;
  &lt;li&gt;Mount the volume to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data&lt;/code&gt; on the app machine&lt;/li&gt;
  &lt;li&gt;Run migrations automatically when the app starts (via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--auto-migrate&lt;/code&gt; flag)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a small app, the whole deployment process will take about 5 minutes. If you’re redeploying with no code changes, it’s less than 30 seconds (if the previous docker image can be reused).&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/fly-io-deployment-logs.png&quot; width=&quot;&quot; height=&quot;&quot; alt=&quot;Fly.io deployment logs showing successful deployment&quot; title=&quot;Fly.io deployment logs showing successful deployment&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Fly.io deployment logs showing successful deployment&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;During deployment, watch for these log messages:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine ... update succeeded&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Starting prepare [database-id: sqlite, migration: ...]&lt;/code&gt; (during app startup)&lt;/li&gt;
  &lt;li&gt;Server startup logs indicating successful migration and binding to port 8080&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;step-9-verify-deployment&quot;&gt;Step 9: Verify Deployment&lt;/h2&gt;

&lt;p&gt;Check that everything is working:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Check app status&lt;/span&gt;
fly status -a myapp

&lt;span class=&quot;c&quot;&gt;# Test your app&lt;/span&gt;
curl https://myapp.fly.dev/

&lt;span class=&quot;c&quot;&gt;# View recent logs&lt;/span&gt;
fly logs -a myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The status should show:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;started&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Health Checks&lt;/strong&gt;: Passing&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Volume&lt;/strong&gt;: Attached to your machine&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/fly-io-app-status.png&quot; width=&quot;&quot; height=&quot;&quot; alt=&quot;Fly.io app status showing running machine with mounted 1GB volume&quot; title=&quot;Fly.io app status showing running machine with mounted 1GB volume&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Fly.io app status showing running machine with mounted 1GB volume&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;step-10-access-your-database&quot;&gt;Step 10: Access Your Database&lt;/h2&gt;

&lt;p&gt;To inspect your SQLite database in production:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Wake up your machine (if auto-stopped)&lt;/span&gt;
curl https://myapp.fly.dev/

&lt;span class=&quot;c&quot;&gt;# SSH into the machine&lt;/span&gt;
fly ssh console -a myapp

&lt;span class=&quot;c&quot;&gt;# Access SQLite database (sqlite3 is pre-installed)&lt;/span&gt;
sqlite3 /data/db.sqlite
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Inside SQLite:&lt;/p&gt;
&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tables&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;-- List all tables
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;-- Show table structure  
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;-- Query your data
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;-- Exit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/fly-io-sqlite-terminal.png&quot; width=&quot;&quot; height=&quot;&quot; alt=&quot;Terminal showing SQLite3 session inside Fly.io machine&quot; title=&quot;Terminal showing SQLite3 session inside Fly.io machine&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Terminal showing SQLite3 session inside Fly.io machine&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;local-development&quot;&gt;Local Development&lt;/h2&gt;

&lt;p&gt;For local development, your SQLite database will be created as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./db.sqlite&lt;/code&gt; in your project root:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Run locally&lt;/span&gt;
swift run Run serve --hostname 0.0.0.0 --port 8080

&lt;span class=&quot;c&quot;&gt;# Access local database&lt;/span&gt;
sqlite3 db.sqlite
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;

&lt;h4 id=&quot;machine-wont-start&quot;&gt;Machine won’t start&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Check machine status&lt;/span&gt;
fly machine list -a myapp

&lt;span class=&quot;c&quot;&gt;# View detailed logs&lt;/span&gt;
fly logs -a myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h4 id=&quot;database-connection-errors&quot;&gt;Database connection errors&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Verify volume is mounted: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly ssh console -a myapp&lt;/code&gt; then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls -la /data/&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Check file permissions: SQLite file should be owned by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vapor:vapor&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;volume-not-attaching&quot;&gt;Volume not attaching&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Ensure volume and machine are in the same region: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly volumes list&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly machine list&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Volume names in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly.toml&lt;/code&gt; must match created volume name exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;backups-and-where-to-go-from-here&quot;&gt;Backups, and where to go from here&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Unlike managed databases, you’re responsible for SQLite backups.&lt;/p&gt;

&lt;p&gt;Fly.io automatically creates Volume snapshots with 5-day retention, but these aren’t easily accessible for restore.&lt;/p&gt;

&lt;p&gt;Below are some strategies for improving the robustness of your system in approximate order of complexity/reliability:&lt;/p&gt;

&lt;h4 id=&quot;irregular-manual-backup&quot;&gt;Irregular manual backup&lt;/h4&gt;

&lt;p&gt;The most low tech backup solution: copy the database from the server to your local machine whenever you remember to do so.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Download production database&lt;/span&gt;
curl https://myapp.fly.dev/  &lt;span class=&quot;c&quot;&gt;# wake machine&lt;/span&gt;
fly ssh sftp get /data/db.sqlite ./backup-&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;.sqlite -a myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h4 id=&quot;regular-manual-backup&quot;&gt;Regular manual backup&lt;/h4&gt;

&lt;p&gt;Set a repeating calendar entry or reminder to remind you to run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sftp&lt;/code&gt; command.&lt;/p&gt;

&lt;h4 id=&quot;automated-manual-backup&quot;&gt;Automated manual backup&lt;/h4&gt;

&lt;p&gt;Use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cron&lt;/code&gt; job or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launchd&lt;/code&gt; on macOS to automatically run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sftp&lt;/code&gt; command,&lt;/p&gt;

&lt;h4 id=&quot;s3-object-storage-backup&quot;&gt;S3 object storage backup&lt;/h4&gt;

&lt;p&gt;Set up an AWS S3 account (or equivalent) with a dedicated bucket to store &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqlite&lt;/code&gt; backups. Then add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cron&lt;/code&gt; job to a GitHub Action to perform the backup from Fly.io to the S3 bucket.&lt;/p&gt;

&lt;h4 id=&quot;add-litestream-for-sqlite-backups&quot;&gt;Add Litestream for SQLite backups&lt;/h4&gt;

&lt;p&gt;See &lt;a href=&quot;https://litestream.io/&quot;&gt;litestream.io&lt;/a&gt;.&lt;/p&gt;

&lt;h4 id=&quot;add-litefs-to-replicate-sqlite-to-multiple-machines&quot;&gt;Add LiteFS to replicate SQLite to multiple machines&lt;/h4&gt;

&lt;p&gt;See &lt;a href=&quot;https://fly.io/docs/litefs/&quot;&gt;this docs page&lt;/a&gt;.&lt;/p&gt;

&lt;h4 id=&quot;use-postgres&quot;&gt;Use Postgres&lt;/h4&gt;

&lt;p&gt;Congratulations, your app is successful enough to need Postgres.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Although I’ve tried to cover the most important SQLite bits in the post, each project is unique. Be sure to consult all the latest docs from Fly.io, Vapor, and SQLite.&lt;/p&gt;

</description>
        <pubDate>Wed, 02 Jul 2025 09:00:00 -0500</pubDate>
        <link>https://twocentstudios.com/2025/07/02/swift-vapor-fly-io-sqlite-config/</link>
        <guid isPermaLink="true">https://twocentstudios.com/2025/07/02/swift-vapor-fly-io-sqlite-config/</guid>
        
        <category>vapor</category>
        
        <category>sqlite</category>
        
        <category>hosting</category>
        
        <category>flyio</category>
        
        <category>apple</category>
        
        
      </item>
    
  </channel>
</rss>
