Files
sofa/docs/content/docs/mobile-app.mdx
T
jakeandClaude Opus 4.6 dce333e128 refactor: organize API around operation domains (#24)
* 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>
2026-03-26 15:44:05 -04:00

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

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:

  1. Enter the URL of your self-hosted instance (e.g. https://sofa.example.com)
  2. The app validates the connection by checking the /api/health endpoint
  3. 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.