Strategy Document

Clarity — PWA to Native iOS

A phased approach to perfecting the mobile experience, from PWA fixes through Capacitor wrapping to an optional native Swift client.

Current State

Clarity is a Next.js 16 PWA deployed on Vercel with Turso/Drizzle, shadcn/ui, and Tailwind v4. It works well as a web app but has persistent iOS Safari tab bar issues.

The Problem

iOS Safari's bottom toolbar height is not reflected in env(safe-area-inset-bottom). This causes the mobile navigation dock to either float too high (Safari browser) or get clipped (PWA standalone). The toolbar dynamically expands/collapses and no CSS value tracks it.

  • Safari tabbed mode: toolbar overlaps content, insets don't account for it
  • Safari scrolled (toolbar collapsed): just the 34px home indicator — insets correct
  • PWA standalone: no Safari toolbar, insets correct — easiest to fix

01 Perfect the PWA Now

Fix the tab bar with the correct CSS pattern and add JavaScript-based Safari toolbar detection.

Separated Padding Pattern (CSS)

Never combine fixed height with safe-area padding on the same element. Separate them:

CSS /* Outer container handles safe area */ .bottom-nav { position: fixed; bottom: 0; width: 100%; padding-bottom: env(safe-area-inset-bottom, 0px); } /* Inner container handles nav height */ .bottom-nav-content { height: 56px; display: flex; align-items: center; } /* Body clearance */ body { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }

Safari Toolbar Detection (JavaScript)

The visualViewport API is the only reliable way to track Safari's dynamic toolbar:

JS // Detect standalone PWA vs Safari browser const isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone; // In Safari browser, track the dynamic toolbar if (!isStandalone && window.visualViewport) { window.visualViewport.addEventListener('resize', () => { const offset = window.innerHeight - window.visualViewport.height; document.documentElement.style .setProperty('--safari-toolbar-h', `${offset}px`); }); }

Viewport Meta Tag

Required in <head> to enable safe area access:

HTML <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />

02 Capacitor Wrapper When Ready

Wrap the existing Next.js app with Capacitor for App Store distribution without rewriting the UI.

What You Get

  • Real App Store listing
  • Native push notifications
  • Haptic feedback
  • Controlled WKWebView (no Safari toolbar)
  • Badge counts on app icon

What to Watch For

  • env(safe-area-inset-*) reports 0px — use capacitor-plugin-safe-area
  • Apple may reject thin wrappers — add 2-3 native features
  • Debugging happens inside Xcode, not browser DevTools
  • iOS updates can break WKWebView behavior

03 Native Swift App If It Grows

A SwiftUI frontend talking to the same backend. Full native experience, zero WebView compromises.

Architecture

Turso DBlibsql://clarity-aventerica89
Next.js API Routes/api/* — Vercel
PWANext.js + shadcn/ui
Web • Home Screen
Swift AppSwiftUI client
App Store • Ad Hoc

Both frontends share the same API, auth (Better Auth), and data model. No duplication of business logic.

Tradeoff: Maintaining two frontends means every new feature ships twice. Only worth it if the app has real traction and WebView limitations frustrate users.

iOS Distribution Options

All options for getting the app on your phone (and your girlfriend's).

MethodCostExpiryDevicesReview?Best For
Xcode SideloadFree7 days3 apps max NoneQuick testing
Ad Hoc$99/yr1 year100 devices NonePersonal use (recommended)
TestFlight$99/yr90 days10,000 testers LightBeta testing
Unlisted App Store$99/yrNoneUnlimited FullPrivate but discoverable by link
Public App Store$99/yrNoneUnlimited FullPublic launch

Apple's App Removal Policy

If you publish to the App Store, here's when Apple would flag your app.

Removal Triggers

  • Not updated in 3+ years
  • Zero or near-zero downloads in a rolling 12-month period
  • Crashes on launch (removed immediately)

What Happens

  • You get a 90-day warning email
  • A simple version bump + resubmit keeps it alive
  • Existing users keep the app even if it's pulled
  • You can resubmit anytime — no permanent ban
For Ad Hoc distribution, none of this applies. Your app isn't on the store, so there's nothing to remove. This is why Ad Hoc is the best option for personal use.

Decision Matrix

Which approach to use based on what you need.

NeedPWACapacitorNative Swift
Web + mobile from one codebase BestGoodTwo codebases
Perfect iOS tab bars / gestures WorkaroundBetterPerfect
App Store presence NoYesYes
Push notifications Web Push onlyNativeNative
Development speed FastestFastSlower
Maintenance burden LowestLow-MedHigh (2 UIs)
Offline / background sync Service WorkerBetterBest
Widgets / Shortcuts NoNoYes

Recommended Sequence

Phase 1 — Now

Fix the PWA tab bar with visualViewport API + separated padding. This is the primary interface and should work flawlessly on iOS Safari and standalone mode.

Phase 2 — When Ready for App Store

Wrap with Capacitor. Add push notifications + haptics + one more native feature to pass App Store review. Use capacitor-plugin-safe-area for inset values. Distribute via Ad Hoc ($99/yr) for personal use or submit to the store.

Phase 3 — If It Takes Off

Build a SwiftUI client against the same API. Full native experience. Only justified if the product has traction and WebView limitations frustrate users.