Daniel Eden, Designer
Deep Dive


Zeitgeist is an iOS and iPadOS app for monitoring Vercel deployments. Unlike my other apps, Zeitgeist serves a very specific audience: web developers who deploy their work to Vercel. I wanted to build a companion app that was powerful while maintaining simplicity and feeling lightweight. It’s also the only app in my portfolio that extensively relies on an internet connection, and this presented its own set of design challenges. In this deep dive, I wanted to share some of the architectural details behind Zeitgeist’s design.


Zeitgeist was built before Vercel sent their own email notifications about failed builds, so part of the goal for the app from its conception was to offer timely information about build status. Notifications were added in v3 of the app, and are more powerful than they first appear.

The first step in getting notifications set up is registering a device with Apple’s Push Notification Service (APNS). This happens across several steps:

A diagram illustrating how devices are registered for push notifications in Zeitgeist. The diagram is explained in detail below.
  1. iOS registers the device with APNS.
  2. APNS returns a unique device token that will be reused to send notifications later.
  3. This token is then sent to Zeitgeist’s “Postal Service” (ZPS) server, one of a few components used to manage notifications for Zeitgeist. Along with the token, the Vercel account IDs for the user’s signed in accounts are sent to the ZPS server.
  4. The ZPS server saves the device token and associated account ID into a database.

Once this registration is complete, the app can respond to webhook events sent by Vercel’s server. These events are sent when deployments are created or complete with failure or success, as well as other events such as project changes.

A diagram illustrating how webhooks trigger push notifications for Zeitgeist. The diagram is explained in detail below.
  1. An event on Vercel triggers a webhook, which is sent to the ZPS server. The payload includes information such as the event type and user ID.
  2. ZPS processes the webhook payload. If the event type is supported, the user’s ID is used to fetch registered device tokens from the register.
  3. The register returns all device tokens matching the given user ID.
  4. The notification payload is sent to APNS, containing most of the same information provided in the original webhook event, such as information about a Git commit that caused a deployment.
  5. APNS delivers the notification to the user’s device as a background push notification.

Note that the notification is delivered in the background. This allows the user’s preferences to take precedence in how notifications are finally surfaced to them. In Zeitgeist, users enable notifications on a per-project and per-event basis, so that they’re not swamped by a torrent of notifications about every deployment in their account.

Notification settings for a project named daneden.me. “Production only”, “Error building”, and “Deployed” are toggled on.
Notifications are enabled on a per-project and per-event basis. In this project, notifications will be received for production builds that fail and succeed. Since all events are sent to the device as silent notifications, the app feels responsive with real-time updates.


The background push notifications also play a part in one of Zeitgeist’s most important features: real-time updates.

When I set out to build Zeitgeist, I wanted it to feel performant and responsive on every page. This meant two features had to be included in the networking layer:

  1. Real-time updates
  2. Stale-while-revalidate” (SWR)-style caching

For real-time updates, background push notifications offer a clear signal to the app about when views should be updated. Whenever there’s a new build status, or a change to a project, the app should request the latest information from Vercel’s servers.

Changes on Vercel—such as new builds, completed deployments, or project changes—are instantly reflected in the Zeitgeist app thanks to its background notifications.

To make this kind of event propogation simple, I created a SwiftUI View modifier that makes a view respond to these events, and runs some asynchronous code (typically fetching new data). I also wanted this asynchronous code to run on other events, such as:

  1. When the view first appears
  2. When the user manually refreshes the page
  3. When the app receives a webhook from Vercel
  4. When the user’s session changes
  5. When the app reenters the foreground from the background

Here’s a simplified snippet of the dataTask View modifier:

import SwiftUI

struct DataTaskModifier: ViewModifier {
   @EnvironmentObject var session: VercelSession
   @Environment(\.scenePhase) var scenePhase
   let action: () async -> Void

   func body(content: Content) -> some View {
      // Run the async action when the view appears...
      .task { await action() }
      // ...when the user refreshes
      .refreshable { await action() }
      // ...when background notifications are received
      .onReceive(NotificationCenter.default.publisher(for: .ZPSNotification)) { _ in
         Task { await action() }
      // ...when the user session changes
      .onReceive(session.objectWillChange) { _ in
         if session.isAuthenticated {
            Task { await action() }
      // ...when the app reenters the foreground
      .onChange(of: scenePhase) { currentScenePhase in
         if case .active = currentScenePhase {
            Task { await action() }

extension View {
   // Usage:
   // View.dataTask { await someAsyncFunction() }
   func dataTask(perform action: @escaping () async -> Void) -> some View {
      modifier(DataTaskModifier(action: action))

To manage SWR-style caching, Swift’s Foundation framework provides URLCache, which is used to offer immediate responses when available while up-to-date data is requested. Here’s a simplified snippet of how this works for fetching deployments:

var request = VercelAPI.request(
  for: .deployments(),
  with: session.account.id,
  queryItems: queryItems

try session.signRequest(&request)

// Immediately show the cached data
if let cachedResponse = URLCache.shared.cachedResponse(for: request),
  let decodedFromCache = try? JSONDecoder().decode(VercelDeployment.APIResponse.self, from: cachedResponse.data) {
  deployments = decodedFromCache.deployments

// Asynchronously fetch the most recent data
let (data, _) = try await URLSession.shared.data(for: request)
let deploymentsResponse = try JSONDecoder().decode(VercelDeployment.APIResponse.self, from: data)
deployments = deploymentsResponse.deployments

Power and Simplicity

Vercel’s platform is improving all the time, with recent additions including storage and Edge configuration. Zeitgeist will never have full feature parity with Vercel’s own platform, but one of the design principles of the app is to make it as poweful as possible while keeping its interface simple.

It’s similar to the difference between a full-fledged toolkit with power tools and a travel-size screwdriver set. There are times you’d prefer a full toolkit, but it’s amazing what you can do with a few streamlined tools.

Zeitgeist supports environment variable editing, redeploying with or without build caches, deleting deployments, promoting to production, following build logs, with a feature list that continues to grow.

Environment variables are worth looking at in detail. There’s a lot of information associated with environment variables: what targets are they available in? Are they encrypted, plain text, or secret? Zeitgeist protects environment variables with on-device biometrics or passcode, and decrypts encrypted values on-demand to protect sensitive information. The app also lets you create, update, and delete environment variables. Combined with the ability to rebuild deployments, this makes it trivial to update an environment variable and rebuild a deployment to apply those changes, all on your phone.

Like most of my side projects, Zeitgeist was built to serve a personal need; I’m thrilled to see it providing value to so many other developers, too. It’s also one of the most architecturally complex and utilitarian projects in my portfolio, and a fun challenge to maintain the balance between simplicity and power.


17 ratings

Must have!
Even if you hooked up your vercel deploy with Slack notification or other webhooks, it won’t beat the convenience of having this app as widget; telling you if your code is building, deployed, or failed. Thanks for building this!
Truly useful
I used to have a webhook synced with Slack to view statuses of every deployments I make on Vercel but now I don’t have to, it’s right on top of my screen. I use it everyday and I highly recommend it.
Great lil app
I work on a lot of side projects, and this gives me the ability to see their current status directly on my Home Screen. So convenient - thanks Dan! - @mknepprath
On my iPhone 12 Pro Max, I cannot select some of the buttons. I looks like there are some compatibility problems with edge of screens. Edit: I complained about UI problems on my iPhone 12 Pro Max… the issues have now been fixed in the latest release! Updating my stars & review here.
Nothing special, no notifications
Need to buy a subscription within the app for notifications… The rest of the app is nothing special!
Great app
It does what it should. Perfect.
Was hoping to have notifications built in but it’s a bit steep to ask for a subscription on top of an already paid app. Works for viewing deployments but would be nice to list individual projects in a grid view as well. Well designed and speedy to navigate :) Would also be cool to have a default selected account and also a widget to view recently failed deployments etc.
Can’t sign in
Prompts for a sign in through Vercel but immediately crashes with an edge function error. Review update: dev was quick to respond and fixed the issue! Thanks!
Nice to have but limited
Doesn’t show you deployments that did not get executed due to failures in CD on GitHub. I get that that’s not the purpose of the app, but it would be nice to see them.
Useful app
Thanks for programming this, it really helps me keep track on all my deployments.
I recommend you to add an option to make deploys in the app (have more control, not only deploys) also please fix the notifications they aren’t working
Great UX!
Overall much nicer than checking email notifications from Vercel
It works quick and good and also has some good widgets
Absolutely worth the money
Great tool for checking status of deployments.
Can’t see any projects
Main app doesn’t work
Widget works fine but no projects display on the app screen. Friend has the same behavior on his end
Looks great but status isn’t updating correctly
When I click on a deployment, the “status” isn’t updating correctly For example if I click on a deployment that I can see on the website in front of me is “Building” - The Zeitgeist app correctly shows the deployment under the recent deployments section as building but when I click on it, I can see the UI initially shows “building” in purple but then immediately changes to a greyed out “Ready” which is incorrect. Once that gets fixed I’m happy to increase my rating but otherwise I’m not entirely trusting the data in this app to be accurate just yet. Would also be incredibly useful if the UI showed the branch that is being deployed, not just the commit name.

You can buy Zeitgeist on the App Store.