H
iOS App · iPhone & Widgets

Retime

A jet lag app that turns one flight into a day-by-day plan for your body clock. The server does the circadian math; a language model only explains it.

Timeline Feb 2026 — Present
Role Product Designer & Developer
Team Solo Project
Tools SwiftUI, WidgetKit, Node.js, PostgreSQL, OpenRouter, Figma

Retime turns a flight into a day-by-day plan for your body clock. Every number is computed on the server; a language model only explains it. It did not start that way.

4

travel states

6

stages behind one card

1 / 6

stages is the model

45 min

largest shift in one night

3

widget sizes, one layout

5

models swapped, app untouched

A city at night from the air, its street grid picked out in light.
Part one

The problem, and the thesis I abandoned

Jet lag is a scheduling problem that most apps treat as a content problem. I spent the first month building the content version, then had to prove to myself it was wrong.

01

Twelve hours is not one instruction

Every jet lag app says go to bed three hours earlier tonight. Nobody does that. The body clock moves about an hour a day — so the problem was never what to do, it was how to cut one correction into steps.

Bedtime 11:00 pm 10:15 9:30 8:45 8:00 pm Every other app  ·  −180 min in one night Retime  ·  −45 min a night usual tonight +1 +2 departure Bedtime 11:00 pm 10:15 9:30 8:45 8:00 pm −180 min, one night −45 min a night usual tonight +1 +2 dep.

180 ÷ 4 nights = 45 min, snapped to the quarter hour, capped at 90.

02

Four states, four kinds of advice

One home screen for every day broke on the morning of my own flight. So the trip itself became the state — four of them, each licensing a different kind of advice.

No travel

No flight booked

Pre-flight

Up to 5 days out

Flight day

About 12 hours

Post-arrival

Days 1–7 on the ground

Hold the line. Consistency now makes the next shift cheap.

Walk the clock. Bedtime moves one step tonight, not the whole shift.

Stop shifting. Sleep windows go inside the flight.

Finish it. Light and caffeine timing, 60 min/day east or 90 west.

← Before the flight After landing →

Precedence is hard-coded: in-flight > post-arrival > pre-flight > no travel.

03

The reversal

I started out believing the opposite of what I ship: let the model reason about the whole trip. Four broken outputs took the arithmetic away from it, one move at a time.

Model decides the number Server decides the number
Original thesisLet it reason about the trip
100%

Reads well in a pitch.

Move 01 · Mar 6A stricter prompt
100%

"Try to sleep on the plane" — for a 14-hour haul and a 90-minute hop alike.

Move 02 · Mar 6Narrow what it sees
72%
28%

Shown 7 flights, it printed "Boston → LA" for a SFO→ORD trip.

Move 03 · Mar 7Pre-compute the step
38%
62%

Told to spread a shift over days, it spent it all on day one.

Move 04 · Mar 9Overwrite its answer
100%

It shifted a westbound traveller the wrong direction.

The model never lost its job — it lost the calculator.

An aircraft wing and winglet above a bank of cloud in low sun.
Part two

Where every number comes from

Each minute Retime shows you is computed or written, never both. Drawing that one line is the decision the rest of the app hangs from.

04

One suggestion, six stages

The reversal, made concrete.

01

Classify state

One of four states

02

Pick flight

The leg that matters

03

Compute step

Shift ÷ days, ≤ 90 min

04

Write advice

Four categories, prose

05

Validate output

Schema, then overwrite

06

Cache & schedule

Widget, notifications

Five of the six are pure functions with unit tests — so the model can be wrong without the numbers being wrong.

TypeScript · Zod · deepseek-v3.2 · PostgreSQL · APNs

05

Read the same card twice

One real card, split by who wrote each line. Numbers are structural — tappable into alarms. Prose only explains them. The user is never told which is which.

Retime suggestions screen, expanded, showing a sleep schedule card and an exercise card
1
2
3
4
1

12:30 am – 8:30 am

computed

The one thing a half-awake person reads, so the one thing the model can't touch. computePostArrivalPlan() stamps over its answer.

2

"You're in a layover: arrived in Los Angeles (from Vegas) 43h ago, with a Boston→Seattle flight in 37.9h (3h westward shift)."

written

Every fact came from the context block. The model's contribution is that it reads like someone who has seen your itinerary.

3

Sleep shift plan — 12:30 am (90m), then 2:00 am (180m)

computed

Section 01's staircase, rendered. Tomorrow next to tonight turns an instruction into a plan.

4

"Evening exercise (4–8 PM local) naturally pushes your clock later, aligning with Seattle time."

written

The mechanism, in one line. People stay with a schedule longer when they know which way it pushes.

Same principle in the banner: a plan that rewrites itself while you're following it isn't a plan. Recomputation is always something you ask for.

A gate sign reading 35 in a bright, near-empty airport terminal, flight numbers and times below it.
Part three

Making it legible at a gate

A plan is worthless if you cannot read it one-handed, at a gate, at six in the morning. So colour carries the schedule and text only confirms it.

06

Colour is the clock

Two clocks, both reading 11:01. It takes a beat to work out which one you're standing in — and UTC−4 / UTC+8 are correct but useless at a gate.

So the palette carries the information. Purple is night. Lime is day. You know which clock is yours before you have read either number.

The two accents only ever mean asleep and awake — in clocks, cards and every route gradient.

Boston

UTC−4 · EDT

11:01 pm

night

Hong Kong

UTC+8 · HKT

11:01 am

day

The header pair, rebuilt in HTML. Same numerals, opposite meaning.

Palette
#D1E561
Daytime / active
#A48AEE
Night / sleep
#F1C038
Sunlight
#7D6142
Caffeine
#141518
Background
#1F222A
Surface L1
#2B323B
Surface L2
Type scale
Hong Kong Poppins 800 · 20
12:30 am Poppins 700 · 24
Sleep Schedule Poppins 700 · 18
Caffeine Cutoff Poppins 600 · 16
56 / 40 MINUTES SF Pro Rounded 500 · 14
18pt card radius 44pt nav radius 22pt content inset

Hierarchy comes from weight, not size — so a 20pt city and a 24pt time share a row without competing.

07

Three tabs, one question each

Three tabs, because a traveller only has three questions: what do I do now, where am I going, who does this app think I am.

Home — what do I do now

Four cards in a fixed order — sleep, exercise, caffeine, sunlight — so the one you want is always in the same place.

Retime home screen with sleep summary and four collapsed suggestion cards
Collapsed — the whole day in one scroll.
Retime home screen with the sleep card expanded to show the shift plan and alarm actions
Expanded — two taps write real alarms.

Flights — where am I going

Entered once, read constantly — so it's drawn as a ticket.

Retime flights tab showing a ticket-shaped flight card with dual clocks
Purple origin, lime destination — the sleep and wake pair again.
Retime flight search screen
Timezones resolve against an airport table, never a city string.
Retime 3D globe showing a Seattle to Boston route
The globe is motivation, not information — looked at, not read.

Mine — who does this app think I am

Every input here changes a number in the pipeline. Nothing else is collected.

Retime profile tab with target schedule and weekly overview
"Preparing for a +3h shift" — the state machine in plain language.
Retime preferences screen with sleep duration slider and caffeine sensitivity
Three fields, each an input to the plan.
Retime weekly data screen with a sleep bar chart and averages
Measured against your own goal, never a population.

Widgets — the plan without the app

The plan matters when you reach for coffee, so it lives outside the app. Three depths: whenwherewhat to do.

Retime medium widget showing departure and arrival with four progress pills
Medium — both ends of the flight, plus four progress pills.
Retime small widget showing days remaining and the route
Small — days remaining and the route line, nothing else.
Retime large widget showing the flight, the bedtime shift and four category tiles
Large — all four targets. Same App Group cache as the app, so the two can never disagree.
Sunset and a rising moon in the same frame, seen past a wing from a cabin window.
Part four

What each decision cost

Nine decisions, what each one bought, and what I gave up to get it — including the ones still unpaid.

08

Decisions, with their costs

Six choices that shaped the product, each with what it cost.

Decision Why Gained Gave up

Compute every sleep time on the server

not trust the model's schedule

WhyA wrong direction deepens jet lag. Arithmetic shouldn't be probabilistic.

GainedTimes that are always right, and unit-testable

Gave upThe model can no longer surprise me with a better plan

Serve a stale plan over an error state

not "couldn't load — retry"

WhyYesterday's bedtime is off by 45 minutes. An error screen is off by everything.

GainedThe app always has an answer

Gave upA silently outdated plan is possible

Exactly four categories, always

not let the model choose what matters

WhyA fixed set buys muscle memory — caffeine is always third.

GainedA layout that never reflows

Gave upNo room for a fifth lever like meal timing

Two accents carry day and night

not labels and timezone offsets

WhyTwo clocks reading 11:01 are identical as text. Colour separates them pre-attentively.

GainedOrientation without reading

Gave upTwo hues spent — they can't be reused for anything else

The server owns the state machine

not duplicate the logic on device

WhyApp, widget and push must agree. Two implementations means two answers.

GainedOne truth across app, widget and push

Gave upThe client can't recompute while offline

A wheel picker for bedtime

not chevron steppers

WhyBuilt steppers first, replaced them 21 minutes later. An hour was eight taps.

GainedOne gesture instead of eight taps

Gave upA popover, so the value is hidden while editing

09

Where it stands

A working build on my own phone, not a shipped product. I've validated the engineering, not the premise.

Shipped & working

  • Four-state engine — 36 server tests, every transition covered
  • Three widget families on a shared App Group cache
  • Alarm write-through — real system alarms and scheduled push
  • Offline cache — last good plan instead of an error
  • Calendar import + airport-table timezone resolution

Cut or never built

  • Apple Watch app — scoped, never started. The biggest missing surface
  • Localization — the interface is English only
  • Onboarding in the launch path — built, but first launch drops you into an empty home
  • iOS test coverage — all tests live on the server

What I'd fix first

  • The feedback sheet lies. It collects a reason and sends it nowhere, implying the app learns from you. A defect, not a gap
  • No analytics. Both success metrics in my own PRD are unmeasurable
  • No users but me. Every claim here is that the plan is correct, not that people follow it
SwiftUI WidgetKit CoreData App Groups Node.js TypeScript Express Prisma PostgreSQL Zod OpenRouter · DeepSeek V3.2 APNs moment-timezone Figma

The model changed five times. When the thing behind the boundary is that replaceable, the boundary is the design.

Part photographs by alex°, Ross Parmly, Big Dodzy and Jack Cohen on Unsplash.

Back to Work Next Project