Tephra’s home screen on an iPhone: Good evening, Maria. A card for the project in the field today, Cedar Hollow Phase I Survey, with 24 collected and an Open project button. Below it a search, filters for archaeology, survey and monitoring, and this week’s six projects.

All work

Since Codifi I’ve used the time to go deep on native development: Android, the Apple Watch, a GNSS receiver over Bluetooth and ARKit. To have something real to build I started field capture over from what three years of Codifi taught me.

What it is
A field survey app on iPhone, Android, Apple Watch and the web. Not released.
Why
To sharpen my native skills: Android, a watch, connected accessories and ARKit.
My part
Every frontend: iPhone, iPad, Apple Watch, Android and the web app. A big hand in the backend’s design. I also carried the brand into each app.
When
Since October 2026
Built in
Swift, SwiftUI and UIKit, with ARKit and Object Capture. Kotlin and Jetpack Compose. MapLibre on both.

What three years of Codifi taught me

At Codifi we talked every day with people working on boats and in the heat and the bright sun. Its schema kept their data clean. Every record had its place and the relationships came from the schema. Nobody left the field with a mess. What it missed was speed. Crews often just need to grab something now and sort it out later.

Some of what they asked for most was out of reach too. Codifi is React Native and its JavaScript can’t keep running in the background. A track of the day would have needed a third-party service to keep location on. That cost too much battery. Tephra starts from those gaps, in native code.

Capture now, classify later

In Codifi a crew had to say what a thing was before they could record it. That kept the data clean. It also slowed down the moment someone just needed to grab something and move on.

So Tephra adds a faster path. A photo, a note, a voice note or a shape goes in with one tap and shows only what matters in that moment. The app helps sort it into a full feature later and offers one from the captures nearby. When you already know what a thing is you can still record it in full straight away.

A note dictated straight in, to be sorted later. No sound.

Controls for gloves and bright sun

Working with crews in the sun taught me what a phone is like outside. The screen washes out. Gloves miss small buttons. A soil color picked from a list of Munsell codes is easy to get wrong.

So the buttons are bigger and the form shows a shovel test at a glance. Soil color is picked from chips you can see, each with its notation and name.

A pit’s level and the Munsell picker. No sound.

Scanning what stays in the ground

Archaeologists sort what they find. Diagnostic artifacts go back with them. Non-diagnostic ones are only documented and left where they lie. The record made in the field is all there will ever be.

A 3D scan of each one keeps far more than a photo. It also gave me a way into augmented reality. I learned Apple’s Object Capture by scanning a brick outside with no signal. I learned ARKit by setting the model back down on a counter.

My edit of a scan: a brick outside, then the model on a counter in AR. No sound.

A voice note on the wrist

In the field the phone is the device most likely to die. It collects all day while it runs the GPS. Crews end up carrying spare batteries. Some notes are quicker said than typed anyway and some hands are full.

So on the Apple Watch a voice note is one tap. It stays on the watch until the phone is ready to take it, then plays on the phone and is turned into words there. Every note taken on the wrist is load the phone doesn’t carry.

Tephra on an Apple Watch Ultra recording a voice note: a timer at 0:03, a level meter, an ember Stop button and Cancel.
A voice note on the watch, three seconds in.

When the phone dies the watch keeps the day

Survey crews have to account for the ground they cover. A track of the day was one of the things they asked Codifi for most and React Native couldn’t keep it running.

Native code can. The day log is a light GPS track of the whole day kept in a pocket and saved as GPX, with a Live Activity on the Lock Screen and a Stop that works without opening the app.

  • A walker’s settings. iOS rests the GPS while the phone stands still and wakes it when it moves. People stop all the time on a site.
  • Fewer points. One every 10 meters. Nothing worse than 40 meters is kept.
  • Written as it goes. A phone that dies keeps everything up to that moment.

Battery is the problem every crew runs into. A phone that collects all day and runs the GPS is the first thing to die. So the Apple Watch can take the day over. At 15% the phone offers it the day. The phone can’t start the watch itself. Its notification shows on the wrist though. One tap there carries the day on. If the phone dies the watch keeps recording and hands the file over once it’s charged.

A crew member can switch between the phone and the watch through the day and the track stays whole. Some people would rather keep the GPS on the watch from the start. They can.

Tephra’s day log on an Apple Watch Ultra: 1.45 km so far, Recording, an ember Stop button and a Voice note button below it.
The day log on the watch, recording.

Cutting a drive out of the day

Handing the day between devices makes one mistake easy. Someone forgets to stop and drives off with the log still running.

So each day has a screen of its own, with a calendar and each track’s map and elevation and a time to drag along it. The drive can be cut out of the track without deleting the day or losing the rest of the GPX.

On my first field test the GPS dropped out during a six-minute phone call and the track drew 4.2 km (2.6 miles) of road as one straight line. Now the file marks where the GPS was lost and starts a new line when it’s back. A day on the watch alone used 10% of its battery in 1 hour 37 minutes.

Cutting 2.6 miles of a drive out of a day’s track. No sound.

A dock built the way Find My’s is

Crews work from the map all day. Nothing should take it away. Find My does this well. It keeps a sheet over its map that never goes away and the map behind it keeps working.

People keep asking on Apple’s forums how to build it. SwiftUI’s sheet covers the tab bar. So the dock is a UIKit tab bar controller presented as the sheet itself, with SwiftUI pages inside.

The hard part was the bar. UIKit melts a tab bar into a sheet’s glass only near a detent with one private name. So the dock’s resting height is a public detent that carries the same name. I found it by tracing UIKit itself.

The dock on a demo survey I invented. The map keeps working behind it. No sound.

The same features on iOS and Android

React Native gave Codifi one codebase. Going native means building each feature twice, the way each system wants it. Android in Kotlin and Compose was one of the skills I set out to sharpen. I test it on the emulator and on a Samsung tablet.

  • A recording you can see and stop without opening the app

    iOS
    A Live Activity on the Lock Screen and in the Dynamic Island. Its Stop button runs an App Intent.
    Android
    A foreground service with an ongoing notification and Stop. Android 16 promotes it with a chip in the status bar.
  • Recording all day in a pocket

    iOS
    A background activity session and Core Location’s fitness updates. iOS rests the GPS while the phone is still.
    Android
    A location foreground service. It rests the GPS itself, woken by the significant-motion sensor.
  • A capture in one tap from the Lock Screen

    iOS
    Capture and Dictate controls for the Lock Screen and Control Center.
    Android
    Quick Settings tiles for a photo and a voice note.
  • On the wrist

    iOS
    A watchOS app for the day log and voice notes. The Live Activity has a Smart Stack layout too.
    Android
    Not started.
  • The sheet over the map

    iOS
    A UIKit tab bar controller presented as a sheet, with SwiftUI pages inside.
    Android
    A Compose dock with three heights of its own.
  • Dictation with field words

    iOS
    On-device speech with a custom language model for the field’s own vocabulary.
    Android
    On-device speech without the custom vocabulary.
  • A Bluetooth GNSS receiver

    iOS
    AccessorySetupKit to pair and Core Bluetooth to listen.
    Android
    Not started.

Talking to a GNSS receiver

Survey work needs better than a phone’s few meters. Agencies set how close a point has to be and a phone’s own GPS often can’t get there.

So I built the iPhone side of a Bluetooth receiver, which was the time with connected accessories I was after: the system’s own pairing picker, then NMEA sentences read as they stream in, each checked before anything trusts it. I tested it with a SparkFun RTK Facet.

In the recording the phone’s own fix is good to 7 meters. The Facet’s is good to 0.81 meters before any corrections.

Pairing the Facet and reading its fix. No sound.
$GNGGA,152846.00,3505.06400,N,10639.02400,W,2,12,0.8,1512.3,M,-23.4,M,1.0,0000*5D
$GNGGA
GN: more than one satellite system. GGA: where you are.
152846.00
The time, 15:28:46 UTC.
3505.06400,N
Latitude, 35° 05.064′ north.
10639.02400,W
Longitude, 106° 39.024′ west.
2
The kind of fix. 2 is DGPS. 4 would be RTK fixed and 5 RTK float.
12
Satellites in use.
0.8
HDOP, how good the satellites’ spread is.
1512.3,M
Height above sea level, in meters.
-23.4,M
How far the geoid is from the ellipsoid there.
1.0,0000
How old the corrections are and the station they came from.
*5D
The checksum. Every character between $ and * run through XOR.
An example GGA sentence, taken apart.

The ground it stands on

I build every frontend and I had a big hand in the backend’s design. What a capture is and what it can become started in my product docs. So did one coordinate system under every app and the way voice notes and 3D scans are stored. The sync itself isn’t mine. One rule in the database is mine alone. It never works out a number a person reads.

Every frontend Mine

  • iPhone and iPadSwift and SwiftUI, with a database on the device
  • Apple WatchRecords on its own and syncs through the phone
  • AndroidKotlin and Compose, with a database on the device
  • Web appReact and MapLibre

The platform

  • One way in for changessync_push applies each once and in order
  • Rows back downPowerSync streams each project to its phones
  • Files photos, recordings and 3D scans
  • Sign-in one account in every app

Postgres with PostGISevery shape stored in WGS84

Saved on the device first. On the phones a capture never waits for a signal.

Each app sends its changes through one function and gets its projects back. Files follow once their record is in.

Repos built for agents

Every repo is set up so an AI agent finds its way in a few reads instead of a search.

  • A map in every repo. Each opens with a CLAUDE.md that says what lives where and which document decides what. One more above them maps all eight repos and says where to start.
  • The agents keep the map current. A change another repo depends on updates its guide in the same commit. The guides changed 106 times in their first week. Claude was a coauthor on 105 of them.
  • Tickets are plain files. Each is named in words and never by a number. Every commit in every repo starts with its ticket’s name. “What should I pick up?” is a question for Claude, not a board.
  • One worktree per ticket. Each ticket gets its own copy of the repo and its own agent, many at once. The web app and the iPhone app run one shared set of test cases. They can’t drift apart.

Claude is a coauthor on every commit to the apps and writes Swift faster than I do. Neither of us knew the GPS could drop out during a phone call until I lost 4.2 km of road to one.