{"$schema":"https://ejfox.com/schema/page-twin@1.json","kind":"project","url":"https://ejfox.com/projects/vulpino","json_url":"https://ejfox.com/projects/vulpino.json","generator":"ejfox.com/json-twin@1","data":{"cacheVersion":"2026-05-13-gear-cards","html":"<p class=\"\">Any JSON endpoint into a home-screen iOS widget, no code, in about a minute</p>\n<p class=\"\">I have data scattered across a dozen endpoints — analytics, servers, side projects, my own life. Vulpino is the fox that eats all of it and puts one clean number on my home screen. You paste a URL, it fetches the JSON and shows you the whole tree, you tap the values you care about, you pick a template, and you're done. Five taps: URL, data, template, customize, deploy. The whole thing is native SwiftUI and it works at all three widget sizes.</p>\n<p class=\"\">The part I care most about is what it <em class=\"\">doesn't</em> let you do. There's no font picker, no color wheel, no padding slider. You get seven typographic templates — Mono Stat for a single hero number, Dual Stat to compare two, Stat Stack for a little dashboard, plus Headline, List, Grid, and Timestamp — and every one of them is already designed. That's the whole idea behind the vulpes family: the constraint is the product. Vulpino says no to customization theater, and the widgets look good because I made the decisions instead of pushing them onto you at 11pm when you just want to see your visitor count.</p>\n<p class=\"\">It's real infrastructure too. API keys and auth headers live in the Keychain, not in some plist. Data is cached offline so the widget still shows the last good value with a stale indicator when the network's down, and tapping a widget deep-links straight into whatever URL you set or back into the app to edit it. Built with a lot of AI pairing — I was the one saying no to features, the model helped me build the ones that survived.</p>\n<figure role=\"figure\" aria-label=\"The widget editor — a SwiftUI step flow from URL to data to template to customize\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/code.png\" alt=\"The widget editor — a SwiftUI step flow from URL to data to template to customize\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/projects/vulpino/code.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/projects/vulpino/code.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/code.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/projects/vulpino/code.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"The widget editor — a SwiftUI step flow from URL to data to template to customize\" width=\"1400\" height=\"900\" style=\"--splay-rot:0.6deg;\" data-dimensions=\"1400×900\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">The widget editor — a SwiftUI step flow from URL to data to template to customize</figcaption>\n</figure>\n<figure role=\"figure\" aria-label=\"The typographic design system behind the widgets — opinionated, minimal, restrained\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/code2.png\" alt=\"The typographic design system behind the widgets — opinionated, minimal, restrained\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/projects/vulpino/code2.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/projects/vulpino/code2.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/code2.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/projects/vulpino/code2.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"The typographic design system behind the widgets — opinionated, minimal, restrained\" width=\"1400\" height=\"900\" style=\"--splay-rot:-1.36deg;\" data-dimensions=\"1400×900\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">The typographic design system behind the widgets — opinionated, minimal, restrained</figcaption>\n</figure>\n<p class=\"\">Running on device: the widget list, pasting a live JSON endpoint, and the tree Vulpino builds from the response so you can tap the values you want.</p>\n<figure role=\"figure\" aria-label=\"The app&#x27;s home — your widgets live here; a new one is sixty seconds away\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/app-home.png\" alt=\"The app&#x27;s home — your widgets live here; a new one is sixty seconds away\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/projects/vulpino/app-home.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/projects/vulpino/app-home.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/app-home.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/projects/vulpino/app-home.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"The app&#x27;s home — your widgets live here; a new one is sixty seconds away\" width=\"1206\" height=\"2622\" style=\"--splay-rot:1.74deg;\" data-dimensions=\"1206×2622\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">The app's home — your widgets live here; a new one is sixty seconds away</figcaption>\n</figure>\n<figure role=\"figure\" aria-label=\"Paste any endpoint and Vulpino validates it live before it fetches\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/app-fetch.png\" alt=\"Paste any endpoint and Vulpino validates it live before it fetches\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/projects/vulpino/app-fetch.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/projects/vulpino/app-fetch.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/app-fetch.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/projects/vulpino/app-fetch.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"Paste any endpoint and Vulpino validates it live before it fetches\" width=\"1206\" height=\"2622\" style=\"--splay-rot:0.79deg;\" data-dimensions=\"1206×2622\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">Paste any endpoint and Vulpino validates it live before it fetches</figcaption>\n</figure>\n<figure role=\"figure\" aria-label=\"Vulpino fetches the JSON and lays the whole response out as a tree — tap the values you care about\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/app-tree.png\" alt=\"Vulpino fetches the JSON and lays the whole response out as a tree — tap the values you care about\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/projects/vulpino/app-tree.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/projects/vulpino/app-tree.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/projects/vulpino/app-tree.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/projects/vulpino/app-tree.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"Vulpino fetches the JSON and lays the whole response out as a tree — tap the values you care about\" width=\"1206\" height=\"2622\" style=\"--splay-rot:0.6deg;\" data-dimensions=\"1206×2622\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">Vulpino fetches the JSON and lays the whole response out as a tree — tap the values you care about</figcaption>\n</figure>","title":"Vulpino","metadata":{"title":"Vulpino","date":"2026-01-10T05:00:00.000Z","category":"Tools","featured":false,"url":"https://github.com/ejfox/vulpino","tech":["Swift"],"state":"deployed","ai-involvement":"ai-assisted","tags":["tools"],"words":387,"images":5,"imageDetails":{"total":5,"cloudinary":5,"withDimensions":0},"links":0,"codeBlocks":0,"headers":{},"toc":[],"type":"post"}},"_links":{"self":"https://ejfox.com/projects/vulpino.json","html":"https://ejfox.com/projects/vulpino","index":"/projects.json"}}