* feat: reorganize API around operation domains
Restructure the oRPC contract from 14 resource-oriented routers to 8
domain-oriented routers for a cleaner public API surface.
- Consolidate 7 watch procedures into unified tracking.watch/unwatch
with scope + ids input (movie, episode, season, series)
- Split dashboard across tracking (stats, history), library
(continueWatching, upcoming), and discover (recommendations)
- Merge explore + search + discover into single discover router
- Absorb integrations into account.integrations
- Merge system.authConfig into system.publicInfo
- Collapse 6 admin setting endpoints into admin.settings.get/update
- Deduplicate platforms.list + explore.watchProviders into
discover.platforms
- Add symmetric unwatchMovie/unwatchSeries core functions
- Rename titles.detail→get, titles.recommendations→similar,
people.detail→get
BREAKING CHANGE: All client API paths have changed. REST paths now
mirror router structure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback on API reorganization
- Fix updateRating invalidation in native app — was only invalidating
title queries, now calls invalidateTitleQueries() to also refresh
tracking.userInfo (drives the rating UI)
- Make unwatchMovie status revert consistent with unwatchSeries — revert
any non-watchlist status, not just "completed"
- Add sync invariant comment on handleWatch/handleUnwatch loops
- Remove unused queryClient import in native use-title-actions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: updateStatus NOT_FOUND error + rate() invalidation in native
- updateStatus now throws NOT_FOUND when quickAddTitle returns null
(title doesn't exist), instead of silently succeeding
- Add NOT_FOUND error to updateStatus contract definition
- Fix rate() in native title-actions.ts to call invalidateTitleQueries()
instead of only invalidating orpc.titles.key() — mirrors the fix
already applied to the hook-based path in d2ddc0f
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: expand mobile app docs and add Play Store badge
- Add the Google Play badge to the README alongside the App Store badge
- Expand the mobile app docs with the Android/Play Store release details
- Refresh docs site dependencies and related package versions for the updated docs stack
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
title, description, icon
| title | description | icon |
|---|---|---|
| Mobile App | Set up the iOS and Android app. | Smartphone |
export const AppStoreBadge = (props) => ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119.664 40" {...props}
<path
d="M110.135 0H9.535Q8.986 0 8.44.002q-.457.004-.919.013A13 13 0 0 0 5.517.19a6.7 6.7 0 0 0-1.9.627 6.4 6.4 0 0 0-1.62 1.18A6.3 6.3 0 0 0 .82 3.617 6.6 6.6 0 0 0 .195 5.52a13 13 0 0 0-.179 2.002c-.01.307-.01.615-.015.921V31.56c.005.31.006.61.015.921a13 13 0 0 0 .18 2.002 6.6 6.6 0 0 0 .624 1.905A6.2 6.2 0 0 0 1.998 38a6.3 6.3 0 0 0 1.618 1.179 6.7 6.7 0 0 0 1.901.63 13.5 13.5 0 0 0 2.004.177c.31.007.613.011.919.011.366.002.728.002 1.095.002h100.6c.36 0 .724 0 1.084-.002.304 0 .617-.004.922-.01a13 13 0 0 0 2-.178 6.8 6.8 0 0 0 1.908-.63A6.3 6.3 0 0 0 117.666 38a6.4 6.4 0 0 0 1.182-1.614 6.6 6.6 0 0 0 .619-1.905 13.5 13.5 0 0 0 .185-2.002c.004-.31.004-.61.004-.921.008-.364.008-.725.008-1.094V9.536q.002-.549-.008-1.092.001-.46-.004-.92a13.5 13.5 0 0 0-.185-2.003 6.6 6.6 0 0 0-.62-1.903 6.47 6.47 0 0 0-2.798-2.8 6.8 6.8 0 0 0-1.908-.627 13 13 0 0 0-2-.176c-.305-.005-.618-.011-.922-.013-.36-.002-.725-.002-1.084-.002"
style={{
fill: "#a6a6a6",
}}
/>
<path d="M8.445 39.125c-.305 0-.602-.004-.904-.01a13 13 0 0 1-1.87-.164 5.9 5.9 0 0 1-1.656-.548 5.4 5.4 0 0 1-1.397-1.016 5.3 5.3 0 0 1-1.02-1.397 5.7 5.7 0 0 1-.544-1.657 12.4 12.4 0 0 1-.166-1.875c-.007-.21-.015-.913-.015-.913v-23.1s.009-.692.015-.895a12.4 12.4 0 0 1 .165-1.872 5.8 5.8 0 0 1 .544-1.662 5.4 5.4 0 0 1 1.015-1.398 5.6 5.6 0 0 1 1.402-1.023 5.8 5.8 0 0 1 1.653-.544A12.6 12.6 0 0 1 7.543.887l.902-.012h102.769l.913.013a12.4 12.4 0 0 1 1.858.162 6 6 0 0 1 1.671.548 5.6 5.6 0 0 1 2.415 2.42 5.8 5.8 0 0 1 .535 1.649 13 13 0 0 1 .174 1.887c.003.283.003.588.003.89.008.375.008.732.008 1.092v20.929c0 .363 0 .718-.008 1.075 0 .325 0 .623-.004.93a13 13 0 0 1-.17 1.853 5.7 5.7 0 0 1-.54 1.67 5.5 5.5 0 0 1-1.016 1.386 5.4 5.4 0 0 1-1.4 1.022 5.9 5.9 0 0 1-1.668.55 12.5 12.5 0 0 1-1.869.163c-.293.007-.6.011-.897.011l-1.084.002Z" />
<g>
<g>
<path
d="M24.769 20.3a4.95 4.95 0 0 1 2.356-4.151 5.07 5.07 0 0 0-3.99-2.158c-1.68-.176-3.308 1.005-4.164 1.005-.872 0-2.19-.988-3.608-.958a5.32 5.32 0 0 0-4.473 2.728c-1.934 3.348-.491 8.269 1.361 10.976.927 1.325 2.01 2.805 3.428 2.753 1.387-.058 1.905-.885 3.58-.885 1.658 0 2.144.885 3.59.852 1.489-.025 2.426-1.332 3.32-2.67a11 11 0 0 0 1.52-3.092 4.78 4.78 0 0 1-2.92-4.4m-2.732-8.09a4.87 4.87 0 0 0 1.115-3.49 4.96 4.96 0 0 0-3.208 1.66 4.64 4.64 0 0 0-1.144 3.36 4.1 4.1 0 0 0 3.237-1.53"
style={{
fill: "#fff",
}}
/>
</g>
<path
d="M42.302 27.14H37.57l-1.137 3.356h-2.005l4.484-12.418h2.083l4.483 12.418h-2.039Zm-4.243-1.55h3.752l-1.85-5.446h-.051Zm17.101.38c0 2.813-1.506 4.62-3.779 4.62a3.07 3.07 0 0 1-2.848-1.583h-.043v4.484h-1.86V21.442h1.8v1.506h.033a3.21 3.21 0 0 1 2.883-1.6c2.298 0 3.813 1.816 3.813 4.622m-1.91 0c0-1.833-.948-3.038-2.393-3.038-1.42 0-2.375 1.23-2.375 3.038 0 1.824.955 3.046 2.375 3.046 1.445 0 2.393-1.197 2.393-3.046m11.876 0c0 2.813-1.506 4.62-3.779 4.62a3.07 3.07 0 0 1-2.848-1.583h-.043v4.484h-1.859V21.442h1.799v1.506h.034a3.21 3.21 0 0 1 2.883-1.6c2.298 0 3.813 1.816 3.813 4.622m-1.91 0c0-1.833-.948-3.038-2.393-3.038-1.42 0-2.375 1.23-2.375 3.038 0 1.824.955 3.046 2.375 3.046 1.445 0 2.392-1.197 2.392-3.046m8.496 1.066c.138 1.232 1.334 2.04 2.97 2.04 1.566 0 2.693-.808 2.693-1.919 0-.964-.68-1.54-2.29-1.936l-1.609-.388c-2.28-.55-3.339-1.617-3.339-3.348 0-2.142 1.867-3.614 4.519-3.614 2.624 0 4.423 1.472 4.483 3.614h-1.876c-.112-1.239-1.136-1.987-2.634-1.987s-2.521.757-2.521 1.858c0 .878.654 1.395 2.255 1.79l1.368.336c2.548.603 3.606 1.626 3.606 3.443 0 2.323-1.85 3.778-4.793 3.778-2.754 0-4.614-1.42-4.734-3.667ZM83.346 19.3v2.142h1.722v1.472h-1.722v4.991c0 .776.345 1.137 1.102 1.137a6 6 0 0 0 .611-.043v1.463a5 5 0 0 1-1.032.086c-1.833 0-2.548-.689-2.548-2.445v-5.189h-1.316v-1.472h1.316V19.3Zm2.719 6.67c0-2.849 1.678-4.639 4.294-4.639 2.625 0 4.295 1.79 4.295 4.639 0 2.856-1.661 4.638-4.295 4.638s-4.294-1.782-4.294-4.638m6.695 0c0-1.954-.895-3.108-2.401-3.108s-2.4 1.162-2.4 3.108c0 1.962.894 3.106 2.4 3.106s2.401-1.144 2.401-3.106m3.426-4.528h1.773v1.541h.043a2.16 2.16 0 0 1 2.177-1.635 3 3 0 0 1 .637.069v1.738a2.6 2.6 0 0 0-.835-.112 1.873 1.873 0 0 0-1.937 2.083v5.37h-1.858Zm13.198 6.395c-.25 1.643-1.85 2.771-3.898 2.771-2.634 0-4.269-1.764-4.269-4.595 0-2.84 1.644-4.682 4.19-4.682 2.506 0 4.08 1.72 4.08 4.466v.637h-6.394v.112a2.358 2.358 0 0 0 2.436 2.564 2.05 2.05 0 0 0 2.09-1.273Zm-6.282-2.702h4.526a2.177 2.177 0 0 0-2.22-2.298 2.29 2.29 0 0 0-2.306 2.298"
style={{
fill: "#fff",
}}
/>
</g>
<path
d="M37.826 8.731a2.64 2.64 0 0 1 2.808 2.965c0 1.906-1.03 3.002-2.808 3.002h-2.155V8.73Zm-1.228 5.123h1.125a1.876 1.876 0 0 0 1.967-2.146 1.88 1.88 0 0 0-1.967-2.134h-1.125Zm5.082-1.41a2.133 2.133 0 1 1 4.248 0 2.134 2.134 0 1 1-4.247 0m3.334 0c0-.976-.439-1.547-1.208-1.547-.773 0-1.207.571-1.207 1.547 0 .984.434 1.55 1.207 1.55.77 0 1.208-.57 1.208-1.55m6.558 2.254h-.922l-.93-3.317h-.07l-.927 3.317h-.913l-1.242-4.503h.902l.806 3.436h.067l.926-3.436h.852l.926 3.436h.07l.803-3.436h.889Zm2.281-4.503h.855v.715h.066a1.35 1.35 0 0 1 1.344-.802 1.465 1.465 0 0 1 1.559 1.675v2.915h-.889v-2.692c0-.724-.314-1.084-.972-1.084a1.033 1.033 0 0 0-1.075 1.141v2.635h-.888Zm5.24-1.758h.888v6.26h-.888Zm2.124 4.007a2.133 2.133 0 1 1 4.247 0 2.134 2.134 0 1 1-4.247 0m3.333 0c0-.976-.439-1.547-1.208-1.547-.773 0-1.207.571-1.207 1.547 0 .984.434 1.55 1.207 1.55.77 0 1.208-.57 1.208-1.55m1.849.98c0-.81.604-1.278 1.676-1.344l1.22-.07v-.389c0-.475-.315-.744-.922-.744-.497 0-.84.182-.939.5h-.86c.09-.773.818-1.27 1.84-1.27 1.128 0 1.765.563 1.765 1.514v3.077h-.855v-.633h-.07a1.52 1.52 0 0 1-1.353.707 1.36 1.36 0 0 1-1.501-1.348m2.895-.384v-.377l-1.1.07c-.62.042-.9.253-.9.65 0 .405.351.64.834.64a1.06 1.06 0 0 0 1.166-.983m2.052-.596c0-1.423.732-2.324 1.87-2.324a1.48 1.48 0 0 1 1.38.79h.067V8.437h.888v6.26h-.851v-.71h-.07a1.56 1.56 0 0 1-1.415.785c-1.145 0-1.869-.901-1.869-2.328m.918 0c0 .955.45 1.53 1.203 1.53.75 0 1.212-.583 1.212-1.526 0-.938-.468-1.53-1.212-1.53-.748 0-1.203.58-1.203 1.526m6.964 0a2.133 2.133 0 1 1 4.247 0 2.134 2.134 0 1 1-4.247 0m3.333 0c0-.976-.438-1.547-1.208-1.547-.772 0-1.207.571-1.207 1.547 0 .984.435 1.55 1.207 1.55.77 0 1.208-.57 1.208-1.55m2.107-2.249h.855v.715h.066a1.35 1.35 0 0 1 1.344-.802 1.465 1.465 0 0 1 1.559 1.675v2.915h-.889v-2.692c0-.724-.314-1.084-.972-1.084a1.033 1.033 0 0 0-1.075 1.141v2.635h-.889Zm8.845-1.121v1.141h.976v.749h-.976v2.315c0 .472.194.679.637.679a3 3 0 0 0 .339-.021v.74a3 3 0 0 1-.484.046c-.988 0-1.381-.348-1.381-1.216v-2.543h-.715v-.749h.715V9.074Zm2.19-.637h.88v2.481h.07a1.39 1.39 0 0 1 1.374-.806 1.483 1.483 0 0 1 1.55 1.679v2.907h-.889V12.01c0-.72-.335-1.084-.963-1.084a1.052 1.052 0 0 0-1.134 1.142v2.63h-.888Zm9.056 5.045a1.83 1.83 0 0 1-1.95 1.303 2.045 2.045 0 0 1-2.081-2.325 2.077 2.077 0 0 1 2.076-2.352c1.253 0 2.009.856 2.009 2.27v.31h-3.18v.05a1.19 1.19 0 0 0 1.2 1.29 1.08 1.08 0 0 0 1.07-.546Zm-3.126-1.451h2.275a1.086 1.086 0 0 0-1.109-1.167 1.15 1.15 0 0 0-1.166 1.167"
style={{
fill: "#fff",
}}
/>
export const GooglePlayBadge = (props) => ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 238.96 70.87" {...props}> <path d="M230.21 1.42c4.1 0 7.44 3.34 7.44 7.44v53.15c0 4.1-3.34 7.44-7.44 7.44H8.75c-4.1 0-7.44-3.34-7.44-7.44V8.86c0-4.1 3.34-7.44 7.44-7.44zm0-1.42H8.75C3.88 0-.11 3.99-.11 8.86v53.15c0 4.87 3.99 8.86 8.86 8.86h221.46c4.87 0 8.86-3.99 8.86-8.86V8.86c0-4.87-3.99-8.86-8.86-8.86" style={{ fill: "#a6a6a6", }} /> <path d="M83.9 18.15c0 1.48-.44 2.67-1.32 3.55-1 1.05-2.3 1.57-3.9 1.57s-2.84-.53-3.91-1.59-1.61-2.38-1.61-3.96.54-2.89 1.61-3.96c1.07-1.06 2.38-1.6 3.91-1.6.76 0 1.49.15 2.18.44.69.3 1.25.69 1.66 1.19l-.93.94q-1.05-1.26-2.91-1.26c-1.12 0-2.09.39-2.9 1.18s-1.22 1.81-1.22 3.06.41 2.28 1.22 3.06c.82.79 1.78 1.18 2.9 1.18 1.19 0 2.18-.4 2.97-1.19.51-.52.81-1.23.89-2.15h-3.86v-1.28h5.15c.05.28.07.54.07.8Zm8.17-4.44h-4.84v3.37h4.37v1.28h-4.37v3.37h4.84v1.31h-6.21V12.41h6.21v1.31Zm5.76 9.32h-1.37v-9.32h-2.97V12.4h7.3v1.31h-2.97v9.32Zm8.25 0V12.4h1.36v10.63zm7.43 0h-1.37v-9.32h-2.97V12.4h7.3v1.31h-2.97v9.32Zm16.79-1.37c-1.05 1.08-2.34 1.61-3.9 1.61s-2.85-.54-3.9-1.61-1.57-2.39-1.57-3.94.52-2.87 1.57-3.94c1.04-1.08 2.34-1.61 3.9-1.61s2.84.54 3.89 1.62 1.57 2.39 1.57 3.93-.52 2.87-1.57 3.94Zm-6.78-.89c.79.8 1.75 1.19 2.89 1.19s2.1-.4 2.89-1.19c.79-.8 1.18-1.81 1.18-3.05s-.4-2.26-1.18-3.05c-.78-.8-1.75-1.19-2.89-1.19s-2.1.4-2.89 1.19c-.78.8-1.18 1.81-1.18 3.05s.4 2.26 1.18 3.05Zm10.27 2.26V12.4h1.66l5.17 8.27h.06l-.06-2.05V12.4h1.37v10.63h-1.43l-5.41-8.67h-.06l.06 2.05v6.62h-1.37Z" style={{ stroke: "#fff", strokeMiterlimit: 10, strokeWidth: ".2px", fill: "#fff", }} /> <path d="M120.61 38.54c-4.17 0-7.56 3.17-7.56 7.53s3.4 7.53 7.56 7.53 7.56-3.2 7.56-7.53-3.4-7.53-7.56-7.53m0 12.1c-2.28 0-4.25-1.88-4.25-4.57s1.97-4.57 4.25-4.57 4.25 1.85 4.25 4.57-1.97 4.57-4.25 4.57m-16.5-12.1c-4.17 0-7.56 3.17-7.56 7.53s3.4 7.53 7.56 7.53 7.56-3.2 7.56-7.53-3.4-7.53-7.56-7.53m0 12.1c-2.28 0-4.25-1.88-4.25-4.57s1.97-4.57 4.25-4.57 4.25 1.85 4.25 4.57-1.97 4.57-4.25 4.57m-19.63-9.79v3.2h7.65c-.23 1.8-.83 3.11-1.74 4.03-1.11 1.11-2.85 2.34-5.91 2.34-4.71 0-8.39-3.8-8.39-8.51s3.68-8.51 8.39-8.51c2.54 0 4.4 1 5.76 2.28l2.26-2.26c-1.91-1.83-4.45-3.22-8.02-3.22-6.45 0-11.87 5.25-11.87 11.7s5.42 11.7 11.87 11.7c3.48 0 6.11-1.14 8.16-3.28 2.11-2.11 2.77-5.08 2.77-7.48 0-.74-.06-1.43-.17-2H84.48Zm80.27 2.48c-.63-1.68-2.54-4.8-6.45-4.8s-7.11 3.05-7.11 7.53c0 4.22 3.2 7.53 7.48 7.53 3.45 0 5.45-2.11 6.28-3.34l-2.57-1.71c-.86 1.26-2.03 2.08-3.71 2.08s-2.88-.77-3.65-2.28l10.08-4.17-.34-.86Zm-10.28 2.51c-.09-2.91 2.26-4.4 3.94-4.4 1.31 0 2.43.66 2.8 1.6zm-8.19 7.31h3.31V31h-3.31zm-5.42-12.93h-.11c-.74-.88-2.17-1.68-3.97-1.68-3.77 0-7.22 3.31-7.22 7.56s3.45 7.51 7.22 7.51c1.8 0 3.22-.8 3.97-1.71h.11v1.08c0 2.88-1.54 4.42-4.03 4.42-2.03 0-3.28-1.46-3.8-2.68l-2.88 1.2c.83 2 3.03 4.45 6.68 4.45 3.88 0 7.16-2.28 7.16-7.85V38.99h-3.14v1.23Zm-3.8 10.42c-2.28 0-4.2-1.91-4.2-4.54s1.91-4.6 4.2-4.6 4.03 1.94 4.03 4.6-1.77 4.54-4.03 4.54M180.26 31h-7.92v22.15h3.31v-8.39h4.62c3.66 0 7.27-2.65 7.27-6.88s-3.6-6.88-7.27-6.88Zm.08 10.68h-4.7v-7.59h4.7c2.47 0 3.87 2.05 3.87 3.8s-1.4 3.8-3.87 3.8Zm20.43-3.18c-2.39 0-4.87 1.05-5.9 3.39l2.94 1.23c.63-1.23 1.79-1.62 3.02-1.62 1.71 0 3.45 1.03 3.48 2.85v.23c-.6-.34-1.88-.86-3.45-.86-3.16 0-6.38 1.74-6.38 4.99 0 2.96 2.59 4.87 5.5 4.87 2.22 0 3.45-1 4.22-2.16h.11v1.71h3.19v-8.49c0-3.93-2.94-6.13-6.73-6.13Zm-.39 12.13c-1.08 0-2.59-.54-2.59-1.88 0-1.71 1.88-2.37 3.5-2.37 1.45 0 2.14.31 3.02.74-.26 2.05-2.02 3.5-3.93 3.5Zm18.74-11.65-3.79 9.6h-.11l-3.93-9.6h-3.56l5.9 13.42-3.36 7.47h3.45l9.09-20.89h-3.68Zm-29.77 14.17h3.31V31h-3.31z" style={{ fill: "#fff", }} /> <path d="M36.6 34.41 17.74 54.43v.01a5.09 5.09 0 0 0 4.92 3.77c.94 0 1.83-.26 2.58-.7l.06-.04 21.23-12.25-9.94-10.82Z" style={{ fill: "#ea4335", }} /> <path d="M55.68 31h-.02l-9.17-5.33-10.33 9.19 10.36 10.36 9.12-5.26c1.6-.86 2.68-2.55 2.68-4.49s-1.07-3.61-2.65-4.47Z" style={{ fill: "#fbbc04", }} /> <path d="M17.73 16.44c-.11.42-.17.86-.17 1.31v35.38c0 .45.06.89.17 1.31l19.51-19.51z" style={{ fill: "#4285f4", }} /> <path d="m36.74 35.43 9.76-9.76-21.21-12.3c-.77-.46-1.67-.73-2.63-.73-2.36 0-4.34 1.6-4.92 3.78s19 19 19 19Z" style={{ fill: "#34a853", }} /> );
Sofa has a native mobile app for iOS and Android built with Expo and React Native. It connects to your self-hosted Sofa server and provides the same core features as the web app.
The mobile app is not currently available on Google Play. You'll need to build it from source.Features
The mobile app includes:
- Home — dashboard with stats, continue watching, and recommendations
- Explore — trending and popular movies and TV shows, genre browsing
- Search — full-text search for movies, TV shows, and people
- Settings — account management, integrations, and admin controls
- Episode-level tracking, ratings, and watchlist management
- Media server integration management (Plex, Jellyfin, Emby, Sonarr, Radarr)
Building from Source
Prerequisites
- Node.js 22+
- Bun runtime
- Xcode (for iOS) or Android Studio (for Android)
- An Apple Developer account (for running on a physical iOS device)
Steps
Clone the repository and install dependencies:
git clone https://github.com/jakejarvis/sofa.git
cd sofa
bun install
Generate native projects:
cd apps/native
bun prebuild
Run on your target platform:
# iOS Simulator
bun ios
# Android Emulator
bun android
For physical devices, follow the Expo development build guide.
Connecting to Your Server
On first launch, the app will ask for your Sofa server URL:
- Enter the URL of your self-hosted instance (e.g.
https://sofa.example.com) - The app validates the connection by checking the
/api/healthendpoint - Once connected, log in with the same credentials you use on the web
The server URL is stored locally on the device. You can change it at any time from the login screen by tapping Change server.
Connection Requirements
- Your server must be reachable from the device's network
- HTTPS is recommended, especially when connecting over the internet
- If using a self-signed certificate, you may need to trust it on the device
Authentication
The mobile app uses the same authentication system as the web app:
- Email & password — standard login
- OIDC / SSO — if configured on your server, the SSO button appears on the login screen
Auth tokens are stored in the platform's secure storage (iOS Keychain / Android Keystore) and scoped per server instance, so you can connect to multiple Sofa servers without conflicts.