AI 资讯
Every Third-Party iOS Keyboard Is a Graveyard. So I Built a Voice Keyboard From Scratch in C++.
If you've searched the App Store for a reliable third-party QWERTY keyboard on iOS, you know how that ends. Some are abandoned. Some are ad-riddled. Some feel like they were ported from Android and never touched again. The good ones are the ones that don't ship features so much as they don't crash. The system keyboard is fine. It's fine because Apple has been iterating on it for fifteen years. Nobody else has. Third-party keyboards on iOS are a graveyard. I'm building one that isn't. It's called Diction. Most people know it as a voice keyboard, and that's what I lead with, but under the hood it's a serious low-level QWERTY project too. This post is about that half of it, because that's the half nobody talks about. Why third-party QWERTY on iOS is a graveyard Building a good keyboard extension on iOS is hard. There's a strict memory ceiling. There's a permission dance the user has to opt into. There's no keychain access, no meaningful background work, and the extension can be killed the moment iOS decides it needs the RAM. Most keyboard makers ship a first version to check the box, then abandon it once they see how much work maintaining it takes. The result is what you see on the App Store today. Every third-party keyboard I've tried on iOS fails on at least one of these: Speed. You type a letter, the letter arrives. That's it. If there's a hitch you can feel, the keyboard is broken. Half the ones I've tried have a visible delay on every keystroke. Predictability. If you correct the same word back three times, that word should be yours. The keyboard should stop fighting you. Most never do. You fight the same wrong correction for a year. Recovery. iOS keyboard extensions are memory-constrained. Bad ones freeze under pressure. When you rapid-switch between apps, half the third-party keyboards on the store will lock up until you kill and reopen the host app. Autocorrect that isn't from 2014. Fix the obvious typos. Split words that ran together. Complete contractions. Ge
AI 资讯
What 74 ADRs in 70 days actually buy a solo dev (no hire, no clients, just the file)
The question you don't dare ask out loud It's 10:40 PM on a Tuesday, I just closed an ADR — the seventy-fourth in this setup, written conscientiously, dated, cross-referenced with its migration, its contract test, and the commit that triggered it. And the question rises, the way it always rises at that hour when you've been coding alone for ten hours: who did I just write this for . No tech lead to convince, no PR review that'll catch it, no hypothetical acquirer to reassure, no architecture committee to brief tomorrow. Just the file, just me, just the doubt. It's the question of a solo dev at 70 days of serious practice. It has an honest answer, and that answer is neither "it'll pay when you sell" nor "it'll pay when you hire". Those two ROIs belong to other trajectories. The ROI of the solo dev who documents is an ROI he buys himself — deferred, intangible at moments, but materially countable if you force yourself to measure it in the first person. Here's mine, over 74 ADRs and 18 doctrine rules accumulated in 70 days, with no external observer to validate the grid. The false economy of "I'll remember" First trap, the one that cost me three weeks before I learned the lesson. The solo dev believes he doesn't need to write down what he decided because he decided it himself — his memory is worth an ADR. False at 14 days, systematically false at six weeks. Not because general memory fails, but because technical memory has a deceptive shape: you remember perfectly that you decided , you no longer remember why you decided that way. Three weeks after the May 5 session where I wrote ADR-0051 (FK ON DELETE SET NULL + CHECK NOT NULL incompatible, DELETE failing silently), I reopen the migration to add a column. I reread the diff, I don't understand why a certain CHECK constraint is phrased like this — the alternative I mentally dismiss today seems simpler, and I'm two clicks from refactoring. I go check the ADR. The answer is there, dated, sourced, in three lines. The simpl
AI 资讯
Building AR Hide and Seek — Shipping a Solo Indie LiDAR Game to the App Store
The idea came from an extremely serious game of hide and seek with my cousins. We were adults, which made it ridiculous, but also strangely perfect. Someone was hiding behind a couch in plain sight, surviving only because the seeker did not look carefully enough. That made me wonder: what if looking carefully was not enough? What if the seeker could not freely look around the room? What if they could only see the world through their phone screen, while virtual obstacles blocked parts of their view? That became the core idea behind AR Hide and Seek: a local multiplayer hide and seek game where 2-5 players use the space they are already in. The hiders physically hide somewhere in the room, while the seeker views the environment through an iPhone. The phone fills the space with digital clutter, making familiar rooms harder to read. One phone. One seeker. Real hiding places. Virtual obstacles. Why LiDAR? LiDAR on iPhone Pro models gives the phone a real-time depth map of the environment, with centimeter-level understanding of the space around it. That means virtual objects can be placed in ways that respect real-world geometry: a crate can sit on the floor, a wall can align with an actual wall, and obstacles can feel like they belong in the room rather than floating on top of it. For a game where the virtual environment needs to feel like it genuinely fills the space, that difference matters immediately. Without reliable depth information, objects can drift, clip, or hover in ways that break the illusion. The tradeoff is device requirement. LiDAR is only available on iPhone Pro models, which narrows the audience. But for this game, the better AR experience was worth it. The seeker sees a version of the room cluttered with virtual obstacles. The hiders are still physically hiding behind real furniture; the phone does not make them disappear. It simply makes finding them harder. Designing the Core Loop The mechanic is simple on paper, but it took a surprising amount of tu
AI 资讯
Why I Built My Own Licensing SDK Instead of Using Paddle
Originally published on the Keylight blog . A short founder note on why Keylight exists. Every product starts as somebody's unsolved problem; this is mine, and if you are shipping a paid app you have probably run into the same one. The problem I kept hitting I wanted to sell a desktop app directly. Not through the App Store — directly, to customers I could actually talk to. The payment side was easy: Stripe is excellent and the decision took an afternoon. Then I got to licensing, and everything slowed down. Stripe takes the money. It does not give you a license key. It does not sign anything your app can verify. It does not know what a device activation is. The moment a customer has paid, you are on your own: you need to mint a key, sign it so it cannot be forged, deliver it, let the app check it, track devices, and revoke it on a refund. None of that is payment processing, so none of it is in Stripe. So I looked at the platforms that do bundle licensing. Why the merchant-of-record platforms did not fit Paddle, Gumroad, and Lemon Squeezy all advertise license keys. I looked hard at each, and the same three problems came up. The fee. As merchants of record they charge around 5%, against Stripe's ~2.9%. On every sale, forever. Reasonable if it solved my problem well — but it did not. Offline validation. This was the dealbreaker. Their licensing is built around an online validation API: to check a key, the app calls the platform's server. My app is a desktop app, and desktop apps run on planes, behind firewalls, and offline. An online-only check leaves no good option. Fail closed — refuse to run without a server response — and a paying customer who is simply offline cannot use what they bought. Fail open — keep running when the server is unreachable — and the check is trivially bypassed: block the app's network access and it can never re-check the license or learn it was revoked. The app never actually verifies anything itself; it only knows what the server last told i
AI 资讯
Migrate License Keys Without Breaking Existing Customers
Originally published on the Keylight blog . The thing that stops developers from moving their licensing isn't the work. It's the fear of one specific moment: a paying customer opens the app after you've switched, and it tells them they're unlicensed. That's the nightmare — you reach for lower fees and customer ownership, and the bill comes due as a wave of "I already paid for this" support tickets. It's a reasonable fear, and it's also avoidable. Migrating onto Keylight doesn't require invalidating anything, re-issuing anything, or asking customers to do anything. This post is about the one rule that keeps everyone working, the two situations you might be in, and why a scary-sounding "major version" jump changes none of it. When you're ready for the click-by-click mechanics, the companion piece covers them: How to Import an Existing Customer Base into Keylight . Why migrating licensing feels risky A license check is binary in the moment a customer experiences it: the app either lets them in or it doesn't. So any change to the system behind that check feels like it's playing with a live wire. Switch the layer that answers "is this person allowed in," the thinking goes, and you risk every existing customer getting the wrong answer at once. That instinct is right about the stakes and wrong about the mechanism. The wave of lockouts people picture comes from one specific mistake: treating migration as a cutover , where the old keys stop being recognized the instant the new system goes live. If your migration invalidates the old keys, yes — everyone breaks. The entire trick is to not do that. The one rule: old keys stay valid Here's the rule the whole migration hangs on: you bring your customers' keys in as they are, and nothing gets invalidated. When you import an existing customer, their license is a live, active record from the first second. If you include the key string they already have, that key is what Keylight stores — not a replacement. So when your new build ask
AI 资讯
One-Time vs Subscription Licensing: Which to Use?
Originally published on the Keylight blog . "Should I charge once or charge monthly?" is one of the first real decisions an indie app faces, and it is usually answered by copying whoever the founder admires rather than by what fits the product. Both models are legitimate. This post lays out when each one actually makes sense, the honest tradeoffs, and how Keylight models perpetual keys and renewing subscriptions so the licensing follows your pricing instead of constraining it. The two models, defined A one-time (perpetual) license is a single payment for a license that does not expire. The customer owns that version — and usually some agreed window of updates — forever. Think of the classic "buy version 3, use it as long as you like" desktop app. A subscription license is a recurring payment for continued access. The license is valid while the customer keeps paying; stop paying and access ends or degrades. The recurring revenue funds ongoing development and any server-side costs the app carries. The distinction is not about the dollar amount — it is about what the customer is buying: ownership of a thing, or ongoing access to a service. Get that framing right and the model usually picks itself. When a one-time license is the right call A perpetual license fits when your app is a tool the customer owns and runs locally , with low ongoing cost to you per user. A focused Mac utility, an audio plugin, a developer tool that does its job on the user's machine — these have little marginal server cost, so charging rent for access is hard to justify and customers feel it. One-time pricing also builds trust. There is no metering, no "what happens if I stop paying," no fear of being locked out of work they already did. For tools people depend on, that ownership feeling is a genuine selling point, and it is exactly the kind of no-value-extraction stance that earns goodwill with developers and power users. The tradeoff is honest: revenue is lumpy and front-loaded. You get paid o
AI 资讯
How to ship and sell a paid desktop app outside the app stores (2026)
You built a desktop app — macOS, Windows, Linux, native or Tauri/Electron — and you want to sell it directly instead of handing 15–30% to Apple or Microsoft. Selling outside the stores means you keep the margin and own the customer relationship. It also means the plumbing the stores quietly handled is now yours: distribution, payments, licensing, updates, support. Here's the whole path, in roughly the order you'll hit it — with the licensing part (the one most people underestimate) covered properly. Why sell outside the app stores Margin. You keep 85–100% instead of giving up the store's cut. Control. Your own pricing, trials, upgrades, and refund policy — no review gatekeeping, no waiting on approval to ship a fix. The relationship. You get the customer's email and can actually support and re-sell to them. The tradeoff is that the things the store did invisibly — vouching for your binary, taking payment, enforcing the purchase — are now your job. This isn't a Mac thing. Windows devs sell direct constantly, Linux too, and a Tauri or Electron app ships to all three from one codebase. The work below applies across the board. 1. Distribution and updates Before anyone pays, they have to trust and install the thing. macOS: sign with a Developer ID certificate and notarize with Apple, or Gatekeeper will scare users off. Windows: an Authenticode code-signing certificate, ideally EV to build SmartScreen reputation faster. Linux: package as AppImage, .deb / .rpm , or Flatpak depending on your audience. Then updates, because the store won't push them for you: Sparkle (macOS), Squirrel/electron-updater (Electron), the Tauri updater , or your own endpoint. Decide this early — retrofitting auto-update onto a shipped app is miserable. 2. Getting paid Two real models: Stripe (you're the merchant). Lower fees, full control, your brand on the receipt. The catch: sales tax and EU VAT are your responsibility (handle it yourself or bolt on a tax service). Merchant of Record (Lemon Sque
AI 资讯
How to add license keys to a SwiftUI macOS app (in under an hour)
You built a Mac app, you want to sell it outside the App Store, and now you need licensing: a key the customer enters, an activation that sticks, and feature gates that hold up offline. Here's how to do it in an afternoon without standing up a backend. Note: this is cross-posted from the Keylight blog . I build Keylight, so this uses it as the worked example — the shape of the solution applies whatever SDK you choose. The three things licensing actually has to do Strip away the marketing and every licensing system does exactly three jobs: Activate — turn a key the user pastes in into proof-of-purchase bound to this device. Verify — on every launch, confirm that proof is still valid, including offline . Gate — unlock features based on the tier/entitlements the license carries. If you build this by hand you're writing a server, a crypto layer, and a state machine. The point of an SDK is to skip all three. 1. Add the SDK Add the Swift package in Xcode (File ▸ Add Package Dependencies) pointing at the Keylight Swift SDK, then configure it once with your tenant key at app launch: import Keylight let keylight = Keylight ( tenant : "your_tenant_key" ) 2. Activate a key Give the user a text field and call activate . This is the one online step — it exchanges the key for a signed, device-bound lease that's stored locally: do { try await keylight . activate ( key : enteredKey ) // lease stored — the app is now licensed on this device } catch { // show the user why: invalid key, device limit reached, etc. } 3. Verify on launch (offline-safe) On every subsequent launch you don't hit the network. The SDK verifies the stored lease's Ed25519 signature locally and hands you a state: switch keylight . checkOnLaunch () { case . licensed ( let lease ): unlockApp ( entitlements : lease . entitlements ) case . trial ( let daysLeft ): runTrial ( daysLeft : daysLeft ) case . expired , . invalid : showActivationScreen () } No server call, so the app opens instantly and works on a plane. Th
AI 资讯
I compared the licensing tools for my indie Mac app — the honest breakdown
I needed to license a macOS app I sell outside the App Store. I went down the rabbit hole so you don't have to. Here's the honest breakdown — what each tool is genuinely good at, and where it stops. No tool is "best"; they're good at different things. The two questions that decide everything Before the tools, answer these: Do you need real offline verification? (Desktop apps usually do — see firewalls, planes, air-gapped machines.) This eliminates the "license key is just a string you check over HTTP" options for serious use. Do you want payments handled too, or do you already have Stripe? Some of these are licensing-only; some are merchant-of-record that also do keys. The licensing-first tools Keygen — the one most people name first. Language-agnostic API, deep policy engine, open-source, self-hostable. Genuinely powerful. The cost is that it's primitives : you bring your own payments, wire the webhooks, and write the client code. Pick it when you want maximum control and don't mind assembling the flow. Cryptolens — classic license-key system with offline verification via signed responses. Strong .NET heritage. Solid if you're on Windows/.NET and want the traditional key + activation-count model. LicenseSpring — enterprise-leaning. Floating licenses, air-gapped activation, node-locking. Overkill for a solo indie app, right at home if you're selling into companies with offline/dark-site requirements. The payments-first tools (keys as a feature) Lemon Squeezy / Polar — merchant of record, so they handle sales tax for you, with a license-key API bolted on (activate / validate / deactivate). Great for getting paid fast across borders. The licensing side is basic — keys are essentially strings with an activation limit; offline verification isn't really their thing. Gumroad — the simplest possible "sell a thing, get a license key, verify over one endpoint." Fine for a cheap utility where piracy isn't worth fighting. Not infrastructure. StoreKit — only relevant if you shi
AI 资讯
How offline license activation actually works
If you ship a desktop app outside an app store, you eventually hit the same wall: how do you check a license when the user is on a plane, behind a corporate firewall, or just offline? Calling your server on every launch isn't an option. Here's how offline activation actually works, without the hand-waving. The naive version, and why it breaks The first thing everyone reaches for is "call home on launch, get back yes/no." It works in the demo and fails in the wild: No network = no app. Fail-closed locks out paying customers. Fail-open means anyone who blocks your domain runs free. Both are bad. A boolean is forgeable. If your app trusts a {"valid": true} response, a proxy or a patched DNS entry returns that for free. The fix isn't a better endpoint. It's moving the trust off the network and onto cryptography. The model that works: signed leases The durable pattern is a cryptographically signed lease (Keygen calls these license files, Keylight calls them leases — same idea): On first activation, the device talks to the server once . The server returns a small signed document: the license state, an expiry, the device binding, and any entitlements (which features/tiers are unlocked). The document is signed with the server's private key (Ed25519 is the modern choice — small, fast, boring in the good way). Your app ships the matching public key and verifies the signature locally on every launch. No network needed. Because the app only ever verifies with a public key, there's nothing secret in the binary to steal, and a forged lease fails the signature check. That's the whole trick: the server vouches once, math vouches forever after. first launch ──► server signs lease (Ed25519, private key) ──► stored on device every launch ──► app verifies signature (public key) ──► no network Device binding (so one key isn't infinite installs) A lease is bound to a device so a single license can't be pasted onto a thousand machines. The lease embeds a device fingerprint, and the SDK ch
AI 资讯
How I Built an Adversarial AI Council in React (and Why It Argues With You)
A local-first, single-file SPA where multiple agents debate your decision and hand you a verdict. The problem: every AI I asked just agreed with me I almost named this project wrong. I'd picked a name that sounded powerful. I asked ChatGPT, and it loved it. I asked Claude, and it nodded along. Nobody warned me about the trademark conflict, the wrong search intent, or the SEO fight I'd pick with the BBC. That was the moment I realized the problem wasn't the name. It was the feedback loop. Most AI assistants are tuned to please, so they hide your blind spots instead of showing them. When you need to make a consequential decision, "sounds great" is the most expensive answer you can get. So I built the opposite: a council of AI agents that disagree on purpose. What NoFlattery does NoFlattery puts 2–4 agents in a room, gives them different reasoning biases, and makes them debate your decision. The output isn't another chat transcript. It's a Decision Record: a clear verdict, the reasoning behind it, the main risk, what would change the call, and a next step. Use it for product decisions, pricing, tech stack, hiring, or any call where one perspective isn't enough. Key product choices: Local-first: your chats and API keys stay in your browser. BYOK: bring your own OpenAI, Anthropic, OpenRouter, or Ollama key. One-time price: no subscription, no account, no data harvesting. The stack The whole app is a single-file SPA built with: React 19 + TypeScript Zustand for state Dexie over IndexedDB for local-first storage Vite + vite-plugin-singlefile for a single index.html deploy An OpenAI-compatible provider runtime so users can plug in their own keys Why single-file? Because the deploy becomes dead simple. One HTML file. No server for the data. No build orchestration. I can ship the app to Cloudflare Pages and forget about it. The turn engine: deterministic, not magical The heart of NoFlattery is a turn-based multi-agent engine. One user message triggers one round. Each agent sp
AI 资讯
My analysis engine has two brains now
The thing I'm building, App Store Analyzer, is a website that does one thing: it reads an iOS niche and writes a deep market analysis for indie devs. For a long time that analysis had one brain — and it spoke German. That made sense at the start. German is my home market and my own language, so I built the analysis logic in German first. I could actually feel whether the output was good or garbage, section by section, because I was reading it in the language I think in. It got deep. Reliable. I trusted it. Then it started to hurt. Every time I wanted the analysis in another language, I was basically running the whole expensive thinking step again from scratch. German code, German slugs, German routes, German everything — and a goal of serving 14 languages. The whole thing fought itself. So I rebuilt the brain in English. Not "translated the code" — rebuilt the canonical brain so English is the one source of truth. Now the engine thinks once in native English, and that single analysis gets translated and cached into 13 other languages . Generate once, translate many. It was not a clean ride. The lows. A refactor left a pile of undefined names and quietly 500'd my detail pages — live, in production, while I thought everything was fine. I misread a normal cache warm-up window as a dead backend more than once and "fixed" things that were never broken. I spent an embarrassing stretch hammering an endpoint with a wrong key, watching 403 scroll by, before realizing my terminal had eaten the line that set the key. Small things. Hours each. The highs. Two of them I didn't expect: It got cheaper , not just cleaner. I'm not paying for a full deep analysis per language anymore — one real generation, then lightweight translations. For a solo dev watching every API cent, that's the whole game. And the English brain was actually sharper . I ran the old German output against the new English one side by side, fully expecting English to be the weaker copy. It wasn't. In a few section
AI 资讯
What Does Google Actually Look For During the 14-Day Closed Test?
You’ve spent weeks, maybe months, tracking down bugs, optimizing your user interface, and wrestling with backend security rules. You compile your native release build or run your final production compilations, thinking the hardest part of the journey is officially behind you. Then you open the Google Play Console, and you’re hit with the ultimate indie developer roadblock: the mandatory 12-tester and 14-day closed testing requirement . Many independent creators view this process as a simple download checklist. You might think, "I'll just find 12 people to download the app, leave it on their phones for two weeks, and wait it out." However, treating the testing phase as a static metric is the fastest way to get rejected during the final production access review. So, what is Google actually tracking in the background during these two weeks? Let’s take a deep dive into the core algorithmic requirement that determines your success: Continuous Engagement . 🔄 Decoding "Continuous Engagement" Google Play policies are not designed as a simple box-checking exercise. The underlying goal of the algorithm is to verify if your application is genuinely functional, stable, and being tested by an organic user base before it reaches millions of production users. To enforce this, Google's advanced systems actively monitor the devices connected to your closed test track over the 14-day timeline: Background Device Pings: Google Play Services regularly collects background automated signals (ping logs) from the devices where your test build is active. Real User Interaction: Leaving an app to rot in an application drawer without ever opening it is instantly flagged by the algorithm. Google measures whether the app is actively opened daily and tracks active interaction metrics within the build. Feedback Loops: The system monitors whether your test community is utilizing the internal testing channel on the Play Store to send private developer feedback and crash reports. 📉 The Illusion of "Ju
AI 资讯
A real bug you can't see - and one that fixed itself (Devlog #4)
Hey. No new feature this time - just a pass through the corners before the next one. We had a list of nine bugs we'd written down and kept walking past. Most were small. One wasn't, and it was hiding behind a button. When you import a file the studio already has - same bytes - we ask whether to share the existing file or make an independent copy you can edit on its own. Pick "independent copy" and you expect exactly that: your own file, safe to change or delete without touching anything else. It mostly worked. But the new copy's internal name was built from how many copies already existed - copy 2, copy 3, and so on. The problem shows up after a delete. Say you had three, removed the middle one, then made another. The new one counted "two exist, so I'm number three" - but number three was already taken. The studio saw the clash, quietly kept the old file, and pointed your new scene at it. You thought you'd made a clean copy; you were sharing the original, and the real copy you just made was orphaned on disk with nothing pointing at it. Edit "your" copy later and you'd be editing the original too. Nothing crashed. Nothing warned you. That's the worst kind. The fix: stop counting, and instead look at which names are actually taken and pick the first free one - so a copy made after a delete always gets its own identity. We also made the studio shout in the logs if two files ever collide again, instead of silently dropping one. Better a loud bug than a quiet one. The rest were smaller. A menu element could jump for a single frame when you grabbed it (the drag started from where the element was saved , not where it was shown ). A countdown number sat blank for one frame before popping in. And the end screen had a leftover timing delay we fixed - which you'll never see, because that screen is solid black either way. Real bug, just invisible. The one we'd marked most important? We went to fix it and found a rebuild from two weeks ago had already solved it. We checked three