Skip to main content
The video uses a specific AI coding tool to demonstrate the setup, but the configuration works 1:1 with Cursor, Claude Code, or any other tool. Despia is web framework and tooling agnostic, so the only things that matter are the SDK call, the public SVG URL, and the refresh interval.
widget:// registers a remote SVG endpoint as the source for an iOS home screen widget. The OS fetches the SVG at the interval you set, swaps the rendered widget contents, and shows the new state on the home screen even when your app is closed. Your backend personalizes the SVG per user by reading the request, looking up the data, and substituting tokens like {NAME} and {SCORE} before responding.
Home Widgets is currently iOS-only. The Android equivalent uses a different rendering pipeline (RemoteViews) that does not accept SVG content. Plan widget UX around iOS first, and gate the SDK call behind isDespiaIOS so Android users do not see a broken affordance.

Apple Developer and Despia setup

Home Widgets on iOS runs as a separate Widget Extension target with its own bundle identifier. The widget and your core app communicate through an App Group, which is what lets the widget reach into shared storage when it builds the SVG payload. You configure the bundle ID and App Group in Apple Developer, then enable the target in the Despia Editor and rebuild.
1

Sign in to Apple Developer

Go to developer.apple.com and sign in with the Apple Developer account that owns your app’s primary bundle ID. Navigate to Certificates, Identifiers & Profiles > Identifiers.
2

Create the App Group

Click the + button next to Identifiers, choose App Groups, and click Continue. Use a description like MyApp Widget Group and the identifier group.com.despia.myapp.widgetsharing. Click Continue, then Register. The widgetsharing suffix is the official Despia naming, do not invent your own.
3

Add the App Group to your core app bundle ID

Back in Identifiers, find your core app (e.g. com.despia.myapp), click into it, and check App Groups. Click Edit, select the group.com.despia.myapp.widgetsharing group you just created, then Continue and Save.
4

Create the Widget Extension bundle ID

Click + under Identifiers again, choose App IDs, then App. Use a description like MyApp Image Widget and the bundle ID com.despia.myapp.ImageWidget (your core bundle ID with .ImageWidget appended). This exact naming is what Despia provisions during the build, do not change it.
5

Add the App Group to the Widget Extension bundle ID

Scroll down to Capabilities, check App Groups, click Edit, and select the same group.com.despia.myapp.widgetsharing group. Click Continue and Save. Both bundles now share storage, which is how the widget reads user-specific data the core app wrote.
6

Enable Home Widgets in the Despia Editor

Open the Despia Editor and go to App > Targets > Home Widget. Toggle the integration on.
7

Rebuild your app

Trigger a fresh build from the Despia Editor. The widget target has to be compiled into your app binary and signed with the matching provisioning profile, so this cannot be applied over-the-air. After the build finishes, the widget becomes available for users to add to their home screen, and your widget:// SDK call sets the SVG source.
Bundle IDs and App Group identifiers in the Despia Editor must match exactly what you registered in Apple Developer. A typo in either place causes the widget extension to fail provisioning silently, and the widget either does not appear in the picker at all or shows a placeholder image instead of your SVG. Copy and paste, do not retype.

Installation

npm install despia-native
import despia from 'despia-native';

How it works

Pass a publicly accessible HTTPS URL to a server endpoint that returns SVG, plus a refresh interval in minutes. The OS picks up the URL on the next widget refresh cycle and treats it as the new source.
import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

const isDespiaIOS = isDespia && (
    navigator.userAgent.toLowerCase().includes('iphone') ||
    navigator.userAgent.toLowerCase().includes('ipad')
)

if (isDespiaIOS) {
    despia('widget://https://myapp.com/widgets/user/abc123?refresh=10')
}
The URL must be a publicly fetchable HTTPS endpoint that returns Content-Type: image/svg+xml. Data URLs (data:image/svg+xml;...), blob URLs, and file paths are rejected. The OS fetches the SVG from the network on each refresh, so the bytes have to live on a real server or CDN.

Scheme parameters

PositionValueNotes
URLA full HTTPS URL to your SVG endpointMust respond with Content-Type: image/svg+xml. Use a path that includes the user ID so each user gets their own widget
refreshMinutes between refreshesWhole minutes only. Common values: 1, 2, 3, 5, 10, 15, 30, 60. iOS may extend the interval under battery pressure, so treat your value as a minimum, not a guarantee
const userId  = currentUser.id
const minutes = 10

if (isDespiaIOS) {
    despia(`widget://https://myapp.com/widgets/user/${userId}?refresh=${minutes}`)
}

Register the widget after login

The natural place to register or update the widget is right after authentication, since that is when you have the user ID needed to personalize the URL. Re-call on every authenticated app load to handle users who switch accounts.
import { useEffect } from 'react'
import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

const isDespiaIOS = isDespia && (
    navigator.userAgent.toLowerCase().includes('iphone') ||
    navigator.userAgent.toLowerCase().includes('ipad')
)

function WidgetSync({ user }) {
    useEffect(() => {
        if (!user || !isDespiaIOS) return

        const url = `https://myapp.com/widgets/user/${user.id}`
        despia(`widget://${url}?refresh=10`)
    }, [user])

    return null
}
To change the refresh rate later (for example, slowing it down for inactive users), call again with a new refresh value. The OS replaces the previous registration with the new one.

Backend SVG endpoint

The endpoint receives the request, loads the user’s data, substitutes the tokens, and returns the SVG with the correct content type. Keep the SVG simple, the home screen renders it at a fixed size and ignores most CSS animations.
// /widgets/user/:userId
export default async function handler(req, res) {
    const { userId } = req.query

    const user = await db.users.findUnique({ where: { id: userId } })
    if (!user) return res.status(404).end()

    const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="169" viewBox="0 0 360 169">
  <defs>
    <linearGradient id="bg" x1="0%" y1="0%" x2="0%" y2="100%">
      <stop offset="0%"   stop-color="#2c2c2e"/>
      <stop offset="100%" stop-color="#1c1c1e"/>
    </linearGradient>
  </defs>

  <rect width="360" height="169" fill="url(#bg)"/>
  <text x="28" y="51"  font-size="24" font-weight="700" fill="#ffffff">Hey ${escapeXml(user.name)}</text>
  <text x="30" y="100" font-size="16" font-weight="600" fill="#ffffff">Current Score: ${user.score}</text>
</svg>`.trim()

    res.setHeader('Content-Type', 'image/svg+xml')
    res.setHeader('Cache-Control', 'no-store')
    res.send(svg)
}

function escapeXml(s) {
    return String(s)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&apos;')
}
Two practical notes:
  • Always escape user input. A user with a name like Bob & "Friends" will break the SVG without escaping, and a malicious user could inject markup that breaks rendering for everyone. The escapeXml helper above covers the five XML-significant characters.
  • Send Cache-Control: no-store so iOS does not cache an older version of the SVG between refreshes. The widget will only ever look as fresh as the most recently cached response.

Refresh interval guidance

iOS does not honor your refresh interval literally. The OS schedules widget updates in batches and may stretch your interval based on battery state, network quality, and how often the user is actually looking at the home screen. Use these intervals as rough targets:
IntervalPractical use
1 to 2 minutesLive scores, active timers, real-time tracking. iOS may still throttle to 5 minutes under battery pressure
5 to 15 minutesStocks during market hours, weather, recently active app data
30 to 60 minutesDaily summaries, dashboards, content that updates a few times a day
60 plusStatic or near-static content where freshness barely matters
Aim for the longest interval that feels acceptable. Shorter intervals do not always deliver, and they drain the user’s battery more visibly than longer ones. A widget that updates every 15 minutes consistently is a better experience than one that might update every minute but sometimes goes 30 minutes between refreshes.

Resources

NPM Package

despia-native