Building a Resilient SPFL Widget Stack
The Wee Table started as a personal itch before it became an engineering exercise.
I built it for myself first as a Scottish football fan who wanted a better way to follow the SPFL. Outside Scotland, these leagues often get very little product attention, so the goal shifted quickly: build something reliable that would also help other Scottish football fans.
Sports data feeds are messy, inconsistent, and occasionally unavailable. If the product depends on one source behaving perfectly, the user experience eventually breaks.
So I treated data resilience as a product feature, not a backend detail.
The architecture goal
Keep the widget fast and useful even when upstream providers fail.
To do that, I split concerns clearly:
- Cloudflare Worker for source aggregation and fallback logic
- Shared
SPFLWidgetKitpackage for domain models, API access, caching rules, and formatting - SwiftUI app + WidgetKit extension consuming the same shared service layer
This kept app and widget behaviour consistent while avoiding duplicated parsing and caching logic.
Source-priority ingestion
The Worker uses a strict provider-priority chain across multiple independent sources: a primary feed, a secondary fallback feed, and a final public backup feed.
When source A fails or changes shape, source B is attempted before the API returns an error path.
That simple policy reduced full-failure scenarios and made incident behaviour predictable.
Layered cache behaviour
Caching exists at three levels:
- Worker KV for standings and fixtures responses
- App Group
UserDefaultscache shared between app and widget - File-based badge image cache for widget-safe rendering
This gave me a clean degradation path:
- Fresh data if available
- Stale-but-usable data if upstreams are down
- Explicit offline/empty states if nothing is available
In practice, that made the app feel stable under real-world conditions, not just ideal network paths.
Widget constraints were real constraints
WidgetKit has refresh and rendering limits that shape engineering choices.
I learned quickly that rendering badge images naively in widgets is fragile. Pre-downloading and caching badge files produced much more reliable output than depending on runtime image fetch behaviour inside widget views.
Timeline refresh rules also forced careful decisions about what to refresh, how often, and from which cache layer.
Data normalisation work
A less obvious issue was team-name mismatch across providers.
Fixtures and standings can differ on naming conventions, abbreviations, and aliases. Normalising names and supporting alternate-name mapping was necessary to keep predictor calculations and table joins accurate.
Without this, the product looked broken even when source data technically existed.
Product-level outcome
The final system balanced:
- Fast glanceability for widget-first usage
- Reliability under upstream volatility
- Maintainability through a shared Swift package
It reinforced a pattern I keep reusing: reliability decisions are UX decisions.