Back to Projects

Retext

A native macOS text expansion app with AI-powered snippets. Type a short trigger like ;sig anywhere on your Mac and it expands to the full text—or describe what you need and let Claude write it.

October 1, 2024
SwiftSwiftUINext.jsPostgreSQLClaude AIVercel

What Is Retext?

Retext is a native macOS app I designed, built, and ship on my own. The idea is simple: type less, do more. You save a snippet once under a short trigger like ;sig, and from then on typing that trigger anywhere on your Mac expands it into the full text. It works system-wide, so the same snippets show up in Mail, Slack, VS Code, your browser, and anywhere else you type.

The part I'm most proud of is the AI side. If you don't feel like writing a snippet from scratch, you describe what you want and Claude drafts it for you, streaming the text into the editor as it writes. It's a small feature that feels a little magical the first time you use it.

Retext is free to start (5 snippets plus your first AI generation on the house), with a Pro tier for unlimited snippets and AI. I run it as a real product: paying customers, a marketing site, analytics, and a release pipeline. Building something end to end like this has taught me more about software than almost anything else I've worked on.

Key Features

  • System-wide expansion — Works in any macOS app with instant, popup-free replacement.
  • AI-powered snippets — Describe what you want and Claude (Sonnet 4) writes it, streaming in real time.
  • Teams — Shared snippet libraries with Google sign-in, role-based access, and sync that keeps working offline.
  • Global hotkey — Control+Space (or whatever you prefer) brings Retext up from anywhere.
  • Tag organization — Group and filter snippets with a clean three-column layout.
  • Smart validation — Catches duplicates, overlapping triggers, and the infinite loops that text expanders are famous for.
  • Auto-updates — New versions install themselves; no trips to a download page.

See It In Action

Type a trigger and it expands instantly, anywhere on your Mac:

And when you'd rather not write a snippet yourself, describe what you need and let Claude draft it:

You can try it yourself at retext.io.

Architecture

Retext spans a native macOS client, a backend proxy, and a small marketing site:

Retext/                    # macOS app (Swift / SwiftUI)
├── App/                   # Entry point, AppDelegate, Sparkle updates
├── Models/                # State, license manager, hotkey manager
├── Views/                 # Three-column NavigationSplitView layout
├── Features/
│   ├── AIGeneration/      # Proxy client, SSE stream parsing
│   └── Teams/             # Offline cache, sync engine, conflict resolution
└── TextReplacer.swift     # The keyboard interception engine

retext-api/                # Backend (Next.js on Vercel)
├── app/api/generate/      # AI generation endpoint, SSE streaming
└── lib/                   # Licensing, rate limits, free-trial tracking

The Mac UI is a SwiftUI NavigationSplitView with tags on the left, snippets in the middle, and the editor on the right. The backend is a Next.js app on Vercel backed by Postgres (Neon) through Prisma, with Upstash Redis for caching and rate limiting. Licensing runs through Polar.sh, updates through Sparkle, and product analytics through PostHog.

The Hard Problems

This is where the project got interesting. A text expander looks trivial from the outside, but the moment you intercept every keystroke on someone's machine, the margin for error disappears.

Intercepting keystrokes without slowing typing down

The core engine uses a global event tap (CGEvent.tapCreate()) to watch keystrokes system-wide. The catch is that macOS will silently disable a tap if its callback takes too long, so the callback does almost nothing: it grabs the key code and hands off to a background processing queue immediately. (An early version also leaked memory on every keystroke until I switched from passRetained to passUnretained—a fun one to track down.)

A 50-character rolling buffer, guarded by a serial queue for thread safety, tracks recent typing. Matching runs against a pre-computed trigger cache so it stays cheap on the hot path instead of touching storage on every key.

Replacing text reliably

Once a trigger matches, Retext deletes it with simulated backspaces and pastes the replacement through the clipboard. I went with the clipboard instead of synthesizing keystrokes because long snippets get truncated when an event tap blocks for too long. To avoid clobbering whatever you'd copied, it saves your clipboard, does the paste, and restores it about 300ms later—unless it notices you copied something new in the meantime, in which case it leaves your clipboard alone.

Catching infinite loops before they happen

Picture a snippet called addr whose content is "My address is ;addr." It expands, types out ;addr, which expands again, forever. Retext validates against this before you can save, alongside checks for duplicate and overlapping triggers. It's the kind of edge case nobody thinks about until it eats their afternoon.

Enforcing free limits where users can't reset them

The free tier originally tracked AI usage on the client, which anyone could reset by clearing app data. I moved enforcement to the server: Redis with atomic counters and a UTC daily reset, plus device-keyed tracking for the one free trial generation so it survives reinstalls. A surprising number of releases went into making that hold up across machines with old installs and stale credentials.

Keeping team snippets in sync, even offline

Teams share a snippet library that has to work on a flaky connection. The client keeps a local SQLite cache, queues changes while offline, and reconciles with the server when it reconnects, resolving conflicts by holding onto both the local and server versions rather than guessing.

Shipping It Solo

Because Retext needs Accessibility permissions to read keystrokes, it can't be sandboxed, which means it can't live on the Mac App Store. That opened up everything the App Store normally handles for you:

  • Code signing with a Developer ID certificate.
  • Notarization with Apple so users never see the scary "unidentified developer" warning.
  • Stapling the notarization ticket to the DMG for offline verification.
  • Sparkle for auto-updates, with an EdDSA-signed appcast.

I wired all of it into a GitHub Actions pipeline. Push a version tag like v1.5.0 and it builds, signs, notarizes, staples, packages a DMG, publishes the GitHub release, and updates the Sparkle appcast. Beta and internal test builds run through the same path. The backend handles the rest of the product loop: Polar.sh for licensing and checkout, a Resend-powered feedback form, Google OAuth and JWTs for team accounts, and PostHog for understanding how people actually use the app.

Business Model

Retext is freemium:

  • Free — 5 snippets and 1 AI generation, no account needed.
  • Pro — Unlimited snippets and AI for $4.99/month, $29.99/year, or $49.99 once for a lifetime license.
  • Teams — Seat-based plans for shared libraries and sync.

Licensing runs through Polar.sh, with validation cached in Redis for five minutes so the app stays snappy without hammering the licensing API on every request.

What I Took Away

Retext was my first real Swift project, and going from "I've never written SwiftUI" to "people are paying for this" stretched me in every direction:

  • SwiftUI and AppKitNavigationSplitView, observable state, and the spots where you still have to reach down into AppKit.
  • Low-level macOS APIs — event taps, thread-safe buffers, and simulating input without breaking the host app.
  • Apple distribution — signing, notarization, and shipping outside the App Store.
  • Full-stack streaming — relaying Claude's SSE stream through a Next.js proxy to a native client without leaking API keys.
  • Running a product — pricing, licensing, analytics, support, and a release pipeline I trust enough to push on a Friday.

The biggest lesson was how much lives between "the feature works on my machine" and "a stranger paid for it and it works on theirs." That gap is where most of the real engineering happens.