<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2" -->

---
title: Live Activities with Expo, End to End: Client and...
description: A detailed walkthrough builds an iOS Live Activity end to end using Expo, covering both the client widget and the backend push infrastructure needed to keep it...
canonical: https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Live Activities with Expo, End to End: Client and Backend — Expo blog | daily.dev
og:description: A detailed walkthrough builds an iOS Live Activity end to end using Expo, covering both the client widget and the backend push infrastructure needed to keep it...
og:url: https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2
og:image: https://api.daily.dev/og/posts/JWfJDu3w2.png
og:image:alt: Live Activities with Expo, End to End: Client and Backend — Expo blog
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Live Activities with Expo, End to End: Client and Backend — Expo blog

**[Expo Blog](https://daily.dev/sources/expo-blog)** · 12 min read · 0 upvotes · 0 comments

## Summary

A detailed walkthrough builds an iOS Live Activity end to end using Expo, covering both the client widget and the backend push infrastructure needed to keep it live. The widget is written as a React component with @expo/ui/swift-ui and compiled to native SwiftUI via expo-widgets, with a fixed content-state schema driving Lock Screen and Dynamic Island rendering across stages (received, processing, completed, failed). On the client, push-to-start and per-activity push token registration are handled through two listener paths, including a tricky case where ActivityKit starts an activity remotely while the app is backgrounded but not killed. The backend stores push tokens in one table, maps domain events into the content-state schema through a single mapper function, and sends APNs requests for push-to-start and push-update events. Several APNs gotchas are called out: content-state must be wrapped as {name, props} with props JSON-stringified, start events require attributes-type, attributes, and alert fields (unlike update events), and push tokens are tied to the APNs environment the build was signed with, not Debug vs Release.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://expo.dev/blog/live-activities-with-expo-end-to-end-client-and-backend>

## Questions this post answers

### What is the exact JSON shape APNs expects for a Live Activity content-state push payload?

The content-state field must be a wrapper object shaped as { name: string, props: string }, not your raw schema fields placed directly under content-state. The name field is the literal widget name passed to createLiveActivity, and props is your entire content-state object serialized with JSON.stringify into a single string. Sending schema fields directly still returns a 200 from APNs but the activity silently never updates because nothing decodes the payload.

_Developers debugging silent Live Activity updates can track APNs implementation gotchas like this on daily.dev._

### What extra fields does an ActivityKit push-to-start APNs payload need compared to a regular update payload?

A start event needs attributes-type set to the literal string LiveActivityAttributes, an attributes object containing the activity's correlating id, and an alert object, even though Apple's documentation describes alert as generically optional. That optionality only applies to update events; omitting alert on a start event causes APNs to accept the push with a 200 while silently never starting the activity on-device.

_Teams wiring up push-to-start flows can compare APNs payload requirements like these on daily.dev._

### Why would APNs return 400 BadDeviceToken for every push to a Live Activity even though the token looks valid?

Push tokens are tied to whichever APNs environment (sandbox or production) the build was signed with, not to a Debug versus Release build configuration. If a build forces one environment across all configurations, which is common for Live Activities since sandbox push-to-start support is inconsistent, sending to the wrong APNs host returns a 400 BadDeviceToken that looks identical to a genuinely invalid token.

_Developers chasing phantom BadDeviceToken errors can cross-check APNs environment pitfalls like this on daily.dev._

## Similar posts on daily.dev

- [The React Native Live Activities Handbook: How to Build iOS Live Activities and Android 16 Live Updates](https://daily.dev/posts/the-react-native-live-activities-handbook-how-to-build-ios-live-activities-and-android-16-live-upda-zpnvakxef) · freeCodeCamp · 1 upvotes · 0 comments
- [Let’s talk Live activities\!](https://daily.dev/posts/let-s-talk-live-activities--vsyx4n03l) · Medium · 0 upvotes · 0 comments
- [Building a native-first social platform with Expo — Expo blog](https://daily.dev/posts/building-a-native-first-social-platform-with-expo-expo-blog-mrpvaxkir) · Expo Blog · 1 upvotes · 0 comments
- [Making React Native actually native](https://daily.dev/posts/making-react-native-actually-native-bdan1xdzt) · Bytes by ui.dev · 2 upvotes · 0 comments
- [Live Activities in React Native, Expo Widgets, and Why Brownies Are Best Shared With Friends](https://daily.dev/posts/live-activities-in-react-native-expo-widgets-and-why-brownies-are-best-shared-with-friends-mepdreukd) · ITNEXT · 0 upvotes · 1 comments

---

Tags: [#ios](https://daily.dev/tags/ios), [#swiftui](https://daily.dev/tags/swiftui)

[View this post on daily.dev](https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Live Activities with Expo, End to End: Client and Backend — Expo blog","url":"https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2"},"datePublished":"2026-10-01T14:05:08.402Z","dateModified":"2026-10-01T14:05:34.210Z","description":"A detailed walkthrough builds an iOS Live Activity end to end using Expo, covering both the client widget and the backend push infrastructure needed to keep it...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/83698705e629aaf6eb5ac64c9961cb1d?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/83698705e629aaf6eb5ac64c9961cb1d?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Expo Blog","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Expo Blog","logo":"https://media.daily.dev/image/upload/s--E7LK6L3y--/f_auto,q_auto/v1774179051/logos/expo-blog?_a=BAMAMiWQ0","url":"https://daily.dev/sources/expo-blog"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"ios,swiftui","timeRequired":"PT12M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Expo Blog","item":"https://daily.dev/sources/expo-blog"},{"@type":"ListItem","position":3,"name":"Live Activities with Expo, End to End: Client and Backend — Expo blog"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/live-activities-with-expo-end-to-end-client-and-backend-expo-blog-jwfjdu3w2#faq","mainEntity":[{"@type":"Question","name":"What is the exact JSON shape APNs expects for a Live Activity content-state push payload?","acceptedAnswer":{"@type":"Answer","text":"The content-state field must be a wrapper object shaped as { name: string, props: string }, not your raw schema fields placed directly under content-state. The name field is the literal widget name passed to createLiveActivity, and props is your entire content-state object serialized with JSON.stringify into a single string. Sending schema fields directly still returns a 200 from APNs but the activity silently never updates because nothing decodes the payload. Developers debugging silent Live Activity updates can track APNs implementation gotchas like this on daily.dev."}},{"@type":"Question","name":"What extra fields does an ActivityKit push-to-start APNs payload need compared to a regular update payload?","acceptedAnswer":{"@type":"Answer","text":"A start event needs attributes-type set to the literal string LiveActivityAttributes, an attributes object containing the activity's correlating id, and an alert object, even though Apple's documentation describes alert as generically optional. That optionality only applies to update events; omitting alert on a start event causes APNs to accept the push with a 200 while silently never starting the activity on-device. Teams wiring up push-to-start flows can compare APNs payload requirements like these on daily.dev."}},{"@type":"Question","name":"Why would APNs return 400 BadDeviceToken for every push to a Live Activity even though the token looks valid?","acceptedAnswer":{"@type":"Answer","text":"Push tokens are tied to whichever APNs environment (sandbox or production) the build was signed with, not to a Debug versus Release build configuration. If a build forces one environment across all configurations, which is common for Live Activities since sandbox push-to-start support is inconsistent, sending to the wrong APNs host returns a 400 BadDeviceToken that looks identical to a genuinely invalid token. Developers chasing phantom BadDeviceToken errors can cross-check APNs environment pitfalls like this on daily.dev."}}]}
```

