TAL Tech Blog: Software Architecture

In this series of technical blog posts, we will write about the interesting (we think) technology behind the Time Atlas app.

This post discusses the overall architecture of the app, which is quite unconventional. When we started working on the app in late 2024, we had two main requirements in mind:

  • User’s device (phone) should do all the processing that is possible, and all the data should remain local, so that the app can work offline. Offline functionality means that it can be used even beyond mobile networks (I like hiking in the Finnish Lapland), but also makes operating the app economically sustainable. The processing of motion and location data to infer your activities and routes is costly, but modern phones have a huge amount of processing power, so if we can do it on the device, we don’t need to rent a fleet of servers to do the compute. And, importantly, Time Atlas has been designed privacy-first, and doing all processing locally means we do not ever need to process the data unencrypted in the backend.
    • It also helps save battery to do the computation on the phone instead of sending over the mobile network.
  • We want to support both iOS and Android (in the future), so we needed a cross-platform technology for the core of the app.

Choosing the Language: Go

Many teams choose to use cross-platform frameworks such as React Native or Flutter. We rejected these because we did not have experience ourselves on using them, and had heard from friends that they become often difficult to maintain in complex projects. We also knew that we would need to do a lot of compute, which we would need to write in native code, and would need some platform-dependent solutions (such as location and motion data capture) in any case.

We decided to take a strategy, where most of the app functionality, which we call the “core” is written in a cross-platform language, and the thin UI layers is written with a native language (Swift for iOS and Java or Kotlin for Android). The UI layer then will integrate with the core with a similar contract as you would integrate with a backend API.

We had three main choices for the core language: C++, Go and Rust. We knew C++ to work well for this purpose, but wanted to use a more modern language because C++ code tends to get “heavy” and is slow to compile. We then evaluated Go and Rust, and after some debate chose Go, and the gomobile library for integrating with Swift/Android. Our evaluation can be summarized as follows:

  • Go is a simple, modern language, which is fast to compile and easy to learn. Although strongly typed, it uses nullable pointers extensively, which means that that you need to be careful in writing clean and simple code with a comprehensive unit test coverage. The gomobile bridge seemed well maintained, as it is used by Google to develop their apps.
  • Rust has a steeper learning curve, and is slow to compile and had a bigger file footprint. Rust is a beautiful language with a powerful type system and compiler, which would help writing error-free code.

In the team, we had one who knew Go and another who knew Rust. I knew neither. The fast learning curve and speed of development of Go, with mature Swift integration (or so we thought), were the features that made us choose Go. This was a risky decision, C++ would had been the safe choice, but in hindsight, it has served us well.

High-level Architecture

Time Atlas app architecture is shown on a high level in the diagram below. For storage, we adopted Sqlite, which is extremely well battle tested on mobile, very fast and has integrations for all languages. As all the processing is done on device, we do not need advanced database features such as support for multiple users, concurrency or transactions.

We use SQlite in a bit quirky way, similar to how key-value databases like MongoDB are used: we store data in protobuffer messages keyed b a primary key. Some fields of the protobuffers (such as timestamps) are also exposed as table columns so that they can be indexed and selected quickly. We have developed a code-generation based “CRUDgen” layer that allows one to read and update those messages in the database using a simple object-oriented interfacec. We decidedly did not try to make a full-blown object-relational-mapping (ORM) layer, as we all shared a common hatred on those!

Time Atlas app high-level architecture.

We call our technology for processing location and motion data “MoLoc”. It takes about 3-5 seconds usually to process a few hours of data from the device, and we found Go performance easily sufficient. On the Swift side, we have MoLoc “tracker” which uses adaptive algorithms to drive sensor data collection using iOS SDKs.

Sync service takes of synchronizing user’s data to the cloud. It is end-to-end encrypted, so only the user’s app is able to read the data. That is, Time Atlas team does not have any access to your data. In addition, in the cloud we have proxies for AI APIs (OpenAI currently, but we have also tested Fireworks), and some other services such as logging and place-of-interest (POI) database.

Interfacing Swift and Go

We use protobuffers for all objects, so we can pass them easily between Go and Swift (or Java/Kotlin). Protobuffers are also easy to develop with, as you can add new fields without breaking existing stored data. Thrift from Facebook would had worked as well, but as we adopted Go (a Google project), it made sense to use Google’s protobuffers as well for assumed better future support.

Early on, we noticed that the Gomobile interface between Go and Swift was quite limited. Particularly, it does not support passing slices (arrays) or complex structs between. It basically only supports primitive types. Thus, we basically use the Gomobile bindings directly only on a couple of cases where primitive types are sufficient, and elsewhere we use gRPC (google’s remote procedure call framework) for passing protobuffers between the layers.

It is kind of dumb to use a remote produce call for a local interface, but since the performance was good enough, and we could then use the gRPC tooling, particularly code generation (which we are fans of), it was still a reasonable call. And although we have had some quirks with this solution (particularly a very nasty bug), overall it has served us well. At some point, we might replace the gRPC over networking with a “gRPC over gomobile” solution where we just pass byte arrays to gomobile dispatcher functions. But that’s when we have a business.

Learning Swift and SwiftUI (with the help of AI)

None of the team had any prior experience with Swift development. We had some experience of iOS development using Objective-C, but it was – to put it mildly – quite outdated. Actually, the team did not have much expertise in UX coding in general, so we got started with help of a consultant. Soon, though, we decided to take over also the UX coding.

It was scary for us backend rats to figure out how to do a Figma design and translate it into code. And how to structure an app so that it is convenient to develop? For the first problem, we found a tool builder.io, which provides a Figma plugin. I would say 80% of the time it did a good job in translating a design to a code, and usually it was easy to fix manually when there were flaws. The main benefit was that with this tool, we learned quickly how the designs are converted to code. Figma also itself produces code, but only per layer, but that is also helpful in adjusting the details.

However, we did not continue using builder.io for long, as we quickly figured out the logic ourselves, and it did not often do a very good job for more complex views. It was just easier to do it yourself.

Swift has been a joy to develop. It is a very solid language with a strong type system. Build times could be faster, but it pays off compared to the old times with Objective-C, when it was much harder to write correct code.

SwiftUI Experience

The app UI is almost fully implemented using the SwiftUI framework by Apple (with the exception of some special views, such as maps, which are not well supported by SwiftUI). Before deciding to adopt it, we consulted some friends, and their message was that it is now production quality and the preferred way to go. SwiftUI is a declarative, which makes the view code easy to understand (usually). Our experience has been generally very positive, but when you hit a problem, it can be extremely difficult to debug. For example, we recently encountered an infinite loop in SwiftUI view generation, which took a very long time to figure out (it was likely caused by a state changing during view construction, but we are still not sure, we made a bigger change causing the problem to disappear).

SwiftUI depends a lot on annotations and is not a first-class citizen in the compiler. Some problems that could be figured out statically at compile time are thus not caught before running the app. This is one of the big weaknesses of SwiftUI currently. Also, every SwiftUI developer has surely encountered the scary “the code takes too long time to type-check” error, which is very misleading: this error may arise if you have any type of mistake in your code, even a syntax error. Compiler is unable to pinpoint the problem, and you end up either splitting up the code or doing trial-and-error code replacement to see what is breaking it.

SwiftUI is certainly not ready, but it is already so much better than what iOS UX programming used to be.

Use of AI

Time Atlas certainly cannot be put together by vibe coding. At least not yet. But the team does use a lot of different AI tools (such as Perplexity, Le Chat, Google Gemini or Claude Code) to make development more productive and fun. The experience is mixed.

I personally used AI a lot to help me learn Go and Swift. It was very useful to ask AI to write a certain function, and then continue in tweaking it. Over time, as I started to get more familiar with the languages, I used AI less and less.

Recently I have decided to train my AI muscle more, and started using Claude Code. It is often a fantastic tool to do changes that need to change several files. I used it especially to do boring stuff like refactoring or basic “plumbing” such as adding new fields or functions. Claude Code is designed for interactive and iterative co-programming, and is a joy to use.

I would estimate AI tools give us maybe 10-20% productivity boost. It is a lot, but not a game changer. The bigger impact has been in helping us to ramp up on new languages and technologies. They have almost completely replaced googling and Stack Overflow in figuring out problems. One problem currently is that the LLMs are not always caught up with the latest versions of libraries, which is a challenge especially with the quickly developing SwiftUI.

For code reviews, we are also using an AI tool CodeRabbit, which is sometimes astonishingly good at spotting bugs in code logic, especially mismatches between comments and code. Downside of the rabbit is that it also produces a lot of bad quality reviews, such as clearly incorrect “this does not compile” comments, when the code had been compiling fine. It also suffers from not having up-to-date knowledge of latest SDKs and language features. That said, catching bugs automatically has a huge value.

That said, I find it important to use AI in moderation. If you stop using your brain yourself, it is going to weaken over time, like a muscle which is not exercised. I use AI to move faster, but I reserve the joy of solving the interesting problems myself. And of course, AI still does a lot of mistakes as well, and reviewing code by AI is quite a dull job.

Thanks for reading! We are happy to receive feedback ([email protected]), or in our Discord. Please try the Time Atlas app as well, and use the Feedback button to send us any comments you have.

Share this post:
Facebook
Twitter
LinkedIn

Download the Time Atlas App

Get a full view of your active life — not just your steps.

About Us

We are a Finnish-US startup backed by VCs and angels. Incorporated in October 2024, after plenty of early tinkering.

If you have any questions or would like to learn more about us, please reach out at [email protected].

Got feedback? Drop it in our Discord.