Posted on August 25, 2026 · Personal Projects
A few weeks ago I started an experiment. I wanted to see how far I could get building a real, production-ready iOS application using GitHub Copilot as my primary development partner, mostly guiding, reviewing, and occasionally correcting. The result is Outdoor Fun Hunt, a GPS-based treasure hunt app that lets you create and play location-based hunts outdoors with friends and family. I plan to use it this week at the camp site where I meet lot of my friends.
This post is about what I actually learned in the process. Not the polished narrative, but the honest one — including the part where I had to spin up my own backend server from scratch, which turned out to be one of the most educational pieces of the whole project.
The Idea: Vibe-Coding as a Real Workflow
Vibe-coding, for those who haven’t come across the term yet, is the practice of describing what you want to build in natural language and letting an AI coding assistant generate most of the implementation. You stay in the driver’s seat — you define requirements, review output, catch bugs, make architectural decisions — but you’re not writing every line yourself.
I was curious whether this approach could produce something genuinely usable, not just a toy prototype. The app needed a map, GPS-based clue unlocking, photo tasks, a real-time leaderboard, an organiser editor protected by real authentication, and full English and Czech localisation. Non-trivial.
The short answer: yes, it can. The longer answer is where things get interesting.
What Worked Surprisingly Well
SwiftUI and MVVM boilerplate
Copilot is excellent at generating SwiftUI views, view models, and the connective tissue between them. Give it a clear description of what a screen should do — „a map-dominated player view with a progress card at the bottom and zoom buttons that work after the user has panned“ — and it produces solid, idiomatic Swift. The Observation framework, async/await, actor isolation, proper environment injection — it gets all of that right most of the time.
Tests
I was impressed by the test coverage it generated. Every service and view model got a matching Swift Testing suite with proper given/when/then structure, mock implementations of all injected protocols, and coverage of edge cases I hadn’t even thought to ask for. Keeping tests passing became a reliable safety net as the codebase grew.
Localisation
Generating a full Czech translation including plural forms, diacritics, and grammatically correct phrasing for every string in the app — that would have taken me days. It took maybe an hour of prompting and correction. Czech has complex plural rules (1 task / 2–4 tasks / 5+ tasks are all grammatically different) and Copilot handled them correctly once I pointed it in the right direction.
What Required More Hands-On Work Than I Expected
MapKit’s gesture-stealing
This was my most stubborn bug. I built zoom in/out buttons over the map, and they worked fine — until the user actually panned or pinched. After any manual gesture, the buttons silently stopped responding. The fix turned out to be two separate issues layered on top of each other: MapKit’s internal MKMapView gesture recognisers intercept touches before SwiftUI’s overlay buttons can get them, and MapCameraPosition.region returns nil after a user gesture (the camera position internally switches representation). Neither of these is documented well. You learn them by running into them.
Appwrite’s query format changing between versions
The Appwrite backend I deployed used version 1.9. The query format Copilot generated was the old DSL-string format (equal("field", ["value"])) rather than the current JSON format ({"method":"equal","attribute":"field","values":["value"]}). Every data query returned a 400 „Syntax error“ until I diagnosed it by testing the API directly with curl. Lesson: always verify generated API integration code against the actual live version of the service you’re running, not whatever version is best-represented in the AI’s training data.
Spinning Up Appwrite on Linode: The Full Story
This deserves its own section because it was genuinely the steepest learning curve of the project — and also the most satisfying thing I figured out.
Why I needed a backend at all
The app started entirely local — hunt data stored in a JSON file bundled with the app, progress saved to the device’s Documents folder. That’s fine for a single player, but the feature I most wanted was a live leaderboard: multiple teams competing on the same hunt, seeing each other’s scores update in real time. That needs a shared backend.
Choosing Appwrite
I evaluated three options: PocketBase (single binary, minimal ops overhead), Appwrite (richer feature set, official Linode Marketplace listing, native Swift SDK), and Supabase self-hosted (Postgres, excellent queries, but a 10-container Docker composition that’s frankly overkill for a hunt app). I went with Appwrite — it was in the Linode Marketplace, had a first-class Swift SDK, and its Storage service has chunked resumable uploads built in, which matters for a future photo-upload feature.
The Linode Marketplace deploy
If you haven’t used Linode (now Akamai Compute), the Marketplace experience is not as smooth as I wish. You pick your app, choose a plan — I used a 4 GB Shared CPU instance, about €24/month — fill in a few parameters, and click Create. Ten minutes later Appwrite is running on a fresh Ubuntu 24.04 box, with Traefik handling TLS automatically once your DNS A record has propagated. Except it took me a while to figure out it was installed to a different location than where Copilot suggested.
The part they don’t tell you clearly in the documentation: the first-time setup requires you to find the installed location of the Docker Compose project, because the Marketplace StackScript doesn’t always put it where you’d expect. The most reliable way I found was asking Docker itself:
docker inspect $(docker ps -qf "name=appwrite") \
--format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' | sort -u
That prints the exact directory. cd there and you have everything: the .env file with all configuration, and docker compose commands to apply changes.
Anonymous authentication as the player login model
One design decision I’m particularly happy with: players don’t need to create an account. They just type a team name. Under the hood, each install creates an anonymous Appwrite session (persisted via cookie, valid for a year) and all progress is stored against that session’s identity plus the team name string. No sign-up friction, no passwords to forget. The leaderboard aggregates by team name, not by account. Clean and simple for the use case.
Organisers — the people building hunts — do have real email/password accounts, because they need write access to the hunts collection and that has to be properly gated. Switching between the anonymous gameplay session and the organiser session required a small but non-obvious trick: Appwrite refuses to create a new session while one is already active. You have to explicitly delete the current session first, then authenticate with email/password. Once I understood that, the implementation was straightforward.
The thing nobody mentions about Appwrite’s console sign-up
If you set _APP_CONSOLE_WHITELIST_ROOT=enabled in the .env (which the Marketplace script does by default), you can only create one admin account — ever — and it has to be the very first sign-up. If you don’t do this immediately, you get a confusing „sign-up is restricted“ error with no obvious path forward. The fix is to temporarily set _APP_CONSOLE_WHITELIST_ROOT=disabled, create your account, then re-enable it. Knowing this upfront would have saved me twenty minutes.
Infrastructure as code with Terraform/OpenTofu
I also wrote a complete Terraform configuration for the Linode deployment — variables, firewall rules, a persistent data volume that survives instance rebuilds, DNS management, and a cloud-init script that installs Appwrite non-interactively. If you want to tear down and rebuild the whole thing reproducibly, it takes one command.
Side note: HashiCorp changed Terraform’s licence to BUSL, and Homebrew removed the formula as a result. If you try brew install terraform today you’ll get an error. Use brew install hashicorp/tap/terraform from HashiCorp’s own tap, or switch to OpenTofu (brew install opentofu), the open-source fork that’s fully compatible and available in Homebrew core.
The Honest Take on Vibe-Coding
The workflow genuinely accelerates development. Features that would have taken me a day to implement took an hour. The architectural decisions — feature-based folder structure, protocol-first services, offline-first caching, proper Swift concurrency — were well-reasoned and consistent throughout the codebase because I could describe principles once and have them applied everywhere.
But it’s not magic, and it’s not a replacement for understanding what you’re building. The moments where things went wrong were always the moments where I accepted generated output without understanding it well enough to know what questions to ask. The MapKit zoom bug took longer to fix than it should have because I initially trusted the generated solution rather than digging into why the buttons stopped working after a pan gesture. The Appwrite query format issue was invisible until I tested against the real API.
The meta-skill that vibe-coding requires isn’t typing less — it’s knowing enough to recognise when something is subtly wrong, even when it looks plausible. That’s still on you.
What’s Next
The app is in review with Apple now. After launch I want to add photo upload — the schema and storage bucket are already provisioned on the backend, and the client code has a clean seam ready for it. I also want to move the backend from HTTP to HTTPS (currently running with an ATS exception, which is embarrassing), and add a simple in-app notification when you’re knocked off the top of the leaderboard.
If you want to follow along, or if you’re curious about using the app for your own events — a scout camp, a family reunion, a corporate team-building day — I’d love to hear from you.
Tags: iOS, SwiftUI, Vibe-Coding, GitHub Copilot, Appwrite, Linode, Swift, Treasure Hunt, Indie Dev