Skip to content

Linked Servers

Brandon Johnson edited this page Sep 7, 2026 · 2 revisions

Linked Servers

Two households, two Obelos, one of them lending the other a few libraries.

You and someone you know each run an Obelo. One of you wants to let the other browse and play a couple of libraries — without handing out a login, without your watch state becoming theirs, and without anyone learning a second address.

Their libraries appear in your app as libraries. Nothing about how you watch changes.

How it works, in three sentences

Your server holds the credential, mirrors the other server's catalog, and carries the bytes. Your phones and televisions only ever talk to your own server — they never learn the other household exists as an address. Playback is negotiated and transcoded on the sharer's machine, under the sharer's rules, and relayed through yours in exactly one hop.

There is no directory, no account service, and no third party in the middle. Linking is a deliberately manual act between two people who already know each other, and it stays that way.

Two sides, and they are not symmetric

Does what
The sharer Creates a user for the other machine, grants it libraries, sets its ceilings, and sends one string. Pays the transcode CPU.
The receiver Pastes the string. From then on their server holds the credential, mirrors the catalog and carries the bytes. Pays the bandwidth.

Sharing does not travel. A library you received from a friend can never be shared onward to a third household — it is refused at the grant, at the export, and in the user dialog, which does not list it. The person whose disk those files are on decided who sees them.

Want it both ways? Do this page twice, once in each direction.

What it does not change

On the sharing side, nothing about your own household. Your users, your watch state, your libraries and your transcode budget are exactly as they were, and the new user can only ever read and play what you grant it.

On the receiving side, the scanner, the enrichment pass and every editor skip a linked library entirely. Nothing about your own libraries moves.

Neither server learns anything about the other's people.


0. Check both builds have it

On each server:

curl -s http://192.168.1.50:8080/api/v1/server | grep -o '"serverLinking":[a-z]*\|"linkedLibraries":[a-z]*'

Both flags, on both machines. If either is missing, that server predates linking and needs an upgrade — which is also what it will be told at paste time, naming which side is behind.


The sharing side

1. Create a user for their server

Settings → Users → Add user. Set the role to Linked server, and give it a name you will recognise in your own session list — "Kate's Obelo", not "guest".

There is no password field, and that is not an omission. This role has no password at all: it cannot be given one, cannot log in with one, and cannot ever be turned into a person's account. Its only credential is the token the invite leaves behind.

It is created with no libraries granted. That is deliberate — "all libraries by default" is a friendly default inside a household and a disclosure of your whole collection to a machine in someone else's.

2. Grant the libraries you mean

Settings → Users → (the new row) → Edit → Libraries. Tick exactly the libraries you are sharing. It is the same replace-set the rest of your household uses, and you can change it whenever you like: a library you revoke disappears from their server at the next sync, and one you add appears the same way.

Libraries you received from someone else are not listed here, and the dialog says why. See sharing does not travel, above.

3. Set the ceilings

Two independent caps, in the same dialog.

Rating ceiling — the same one you would use for a child's account. Titles above it are hidden from that server entirely.

Playback ceiling — this is the one that matters here. Three fields:

Field What it does
Max resolution 720p, 1080p or 2160p; blank is uncapped. A 4K-only file under a 1080p cap transcodes down on your machine, under your own governance. It is not hidden — a quality cap changes how something plays, never whether it exists.
Max bitrate Entered in Mbps. Blank or zero is uncapped.
Max streams How many of their household may be watching at once.

Set max streams before you set anything else. Everything transcoding costs is yours to pay, and this is your one lever over load from a household you cannot see into. It exists precisely so you do not need a second one: you will never see who over there is watching, only that your server is serving N streams to "Kate's Obelo".

The ceiling applies to any non-admin role, so it is also the answer to "the iPad may have 720p".

4. Decide how their server will reach yours

This is the whole of the difficulty. One machine — theirs — has to be able to open a connection to yours. Their phones and televisions do not; they only ever talk to their own server, which is what makes this possible at all.

Three ways, and you may put more than one in the invite. They are tried in the order you type them, and the one that answers is remembered.

(a) A public HTTPS address, with a port forward

If you already reach your own server from outside the house, you already have this — use the same origin. If you do not, set it up first: HTTPS.

The origin goes in the invite as https://media.example.org — scheme and host, no path, no trailing slash.

(b) A reverse proxy

If nginx, Caddy or a container ingress already terminates TLS in front of Obelo, its public origin is the address. Nothing special is needed, and Obelo cannot tell this apart from (a). Make sure OBELO_TRUSTED_PROXIES names the proxy — see Reverse Proxy — so rate limits stay per-client.

(c) A Tailscale machine-share — no port forward on either side

This is what most households should use if both operators are willing to make a Tailscale account. You share your Obelo machine into their tailnet from the Tailscale console; their server then dials it directly over WireGuard through its own tailnet node. Nothing is exposed to the internet, neither of you opens a router page, and there is no bandwidth cap.

Both servers need the tailnet feature connected first — follow Remote Access with Tailscale on each, at least as far as its step 3. Then, in the Tailscale admin console on your account:

  1. Machines → your Obelo server → ⋯ → Share…
  2. Enter the other operator's Tailscale account email and send the invite link.
  3. They accept it in their own console. Your machine now appears in their machine list as a shared node and resolves on their tailnet as obelo.<your-tailnet>.ts.net — it keeps your tailnet in its name, which is exactly how their server knows to dial it through the tailnet rather than the open internet.
  4. Machines → your Obelo server → ⋯ → Disable key expiry. Do not skip this.
  5. Copy that name. The origin you type in step 5 is https://obelo.<your-tailnet>.ts.net — or http://… if you have not enabled tailnet HTTPS, and Obelo's own Remote access panel shows you which is true.

A shared machine is quarantined by Tailscale: they can reach it, and it cannot initiate anything back at their network. There is no API for machine sharing — it is a console action, which is why this is a step here and not a button in Obelo.

Node key expiry is the one scheduled failure on this path. Tailscale expires a machine's membership after 180 days by default. When yours lapses, your server silently stops being reachable over the tailnet — and on their side it shows up only as unreachable, with no hint that a date arrived on a machine in your house. Nothing local breaks and nothing logs an error over there. Step 4 removes it permanently.

5. Generate the invite and send it

Settings → Users → (the linked-server row) → Edit → Link.

Type the addresses from step 4, one per row, in the order you want them tried — "Add another address" for a second. If your tailnet node is connected, the MagicDNS row is pre-filled; the free-text row is for the public origin. Obelo cannot know its own public address and never guesses one, which is why you are typing this.

Press Generate invite. You get one string:

obelo-link:eyJ2IjoxLCJpZCI6IjkwZjBhYTYyLTQ3NjktNDc4OC1iNjNhLWQ5ZTNjYjRhZDUxYyIsIm5hbWUiOi…

and a QR code of the same string. Send either one — iMessage, Signal, email, a photograph of the screen. Three things to know:

  • It is single-use and lasts 24 hours. Whoever holds it can link once, within a day, and it is dead the moment it is spent. That is why it is safe in a chat in a way a password never is.
  • Generating another one replaces it. Press the button twice and the first string stops working — so do not mint a fresh one while your friend is halfway through pasting the old one.
  • It is not a link to click. There is no page to open. The action it triggers belongs on their server, which the string cannot name; it gets pasted into Obelo, or scanned by the Obelo app.

Nothing has been shared yet. The grants and the ceilings above are what will apply the moment they redeem it.


The receiving side

6. Paste the string

Settings → Linked servers → Link a server. Paste the whole obelo-link: string and submit. In the iPhone or iPad app, Settings → Link a server opens the camera and scans the QR instead — you must be signed in as an admin of your own server, and the phone stores nothing from the code. It is a courier.

Within a few seconds the panel names the sharing server and lists the libraries that arrived. The whole thing happens in that one request: your server reads the addresses, tries them in order, checks that the other machine speaks linking and speaks the same version of it, spends the code for a credential, creates a library per granted library, and pulls the whole catalog once.

If it refuses, the sentence names the next move:

What it says What to do
That is not a valid invite You pasted something else, or the string got mangled in transit. Ask for it again.
This invite has expired More than 24 hours old. Ask for a fresh one.
The other server would not accept this invite The code was already spent, or replaced by a newer one. Ask for a fresh one.
Their server needs an upgrade / yours does The two speak different link protocol versions. The sentence says which side.
Could not reach that server at any of these addresses Nothing answered. The addresses are listed; go back to step 4 with them.

7. Grant the new libraries to your household

A linked library is a library. Settings → Users → (a person) → Edit → Libraries now lists it alongside your own, and it obeys that person's rating ceiling exactly as your own libraries do. This is what lets you say "the kids get his cartoons library and not the other one", with a dialog you already know.

The two households' ceilings stack, and the stricter side wins — each server enforces its own and neither knows about the other's.

8. What a linked library is like to live with

From there the mirrored titles are ordinary titles. They appear in Home rows, in search, in Collections and Playlists, and they carry your household's watch state. Nothing about what you watch crosses back.

What is different:

  • A link icon and the sharing server's name on the library and on its titles, so it is never a mystery whose shelf you are looking at.
  • Read-only. No scan, no enrichment, no editing, no artwork upload — the other server is the identity authority for its own files. Renaming is the single exception: what your household calls the shelf is your household's business.
  • Nothing is copied at sync except the catalog. Posters, backgrounds, logos and artist images are fetched from the sharer on demand and cached locally.
  • Playback is theirs, then yours. They negotiate the tier and do any transcoding, under their own governance and the ceiling from step 3; your server rewrites the URLs onto itself and streams the bytes through untouched. Your server pays bandwidth and never CPU — a relayed stream holds no slot against your own OBELO_MAX_CONCURRENT_TRANSCODES.
  • When their server is unreachable, it stays put — badged unavailable and greyed, not vanished. Their titles stay in Continue Watching, and a friend's reboot does not cost you a resume position.

Managing a link

Each row on Settings → Linked servers carries a state, the address currently in use with the alternatives beside it, and when it last synced.

State Means What you do
connected The last call succeeded. Nothing.
unreachable Their machine is not answering — off, rebooting, or its address moved. Nothing is deleted. Your server keeps retrying on a backoff. Wait, or press Sync now. If it persists, ask them: on the tailnet path, a lapsed node key looks exactly like this.
revoked They deleted the linked-server user, or its device. Your credential is dead and no amount of retrying will fix it, so nothing retries. The libraries stay. Ask for a fresh invite and press Re-key.

Three actions per row:

Sync now — one immediate sweep: reconcile which libraries you are granted, then pull each of them forward. It is synchronous and reports what happened, because you pressed it after plugging something back in. It is also the only thing that dials a revoked link — the automatic loop never does, but a human asking is not a retry.

Re-key — paste a fresh invite from the same server into the same box. The link, its libraries, their local ids and your household's watch state are all kept; only the credential and the addresses are replaced. An invite from a different server is refused rather than silently repointed. This is the fix for revoked, and also for a friend whose address changed.

Unlink — the only destructive action, and the only thing that ever deletes what came over a link. It removes the link, every library it provided, every mirrored title behind them, and your household's watch state for all of it. It also tells the other server, so your entry disappears from their users page instead of lingering as a ghost — but it succeeds either way, because a friend's server being switched off must not be able to keep you linked to them. The confirmation names the libraries; read it.

Staying fresh

After the first pull, the mirror refreshes two ways: immediately, when the sharing server says one of your libraries changed, and on a timer as the fallback.

OBELO_LINK_SYNC_INTERVAL=1h     # the default
OBELO_LINK_SYNC_INTERVAL=0      # background refresh off entirely

0 turns off the timer, the retry backoff and the change subscription — one knob for one question, "does this server keep its friends' libraries fresh on its own". Sync now, and the pull that follows a link or a re-key, still work.

Every link starts unreachable at boot until something succeeds, because that is a statement about this boot and not about the schedule.


Two things not to do

Do not try to re-share a library you received. It is refused at three surfaces and this is not a limitation to work around. The person whose disk those files are on decided who sees them, and they would be the one paying the transcode for a household they have never heard of.

Do not put a friend's address in OBELO_TRUSTED_PROXIES. It is not a proxy. See Reverse Proxy for what that setting actually does and why widening it is expensive.


Troubleshooting

Everything worked at paste time, but no libraries arrived. They granted the linked-server user nothing. Step 2, on their side.

"Stream limit reached" on a title from their library. Their server refused, not yours: the linked-server user is at its max streams cap. Someone else in your household is already watching something of theirs. The refusal passes through verbatim, counts and all.

Something of theirs plays at 1080p when the file is 4K. Working as intended — that is their playback ceiling, applied on their machine, transcoding down under their governance. Your server never re-encodes anything of theirs; it only carries the bytes.

They renamed a film and it did not change here. It will at the next sync. If you are impatient, Sync now. If it still does not, the link is not connected — read the state.

A film plays but stutters, and the LAN is fine. On the tailnet path, the two machines may have failed to connect directly and fallen back to a DERP relay, which is throttled and shared. Tailscale's console shows the connection type per machine; nothing in Obelo can see or report it.

It worked for months and then went unreachable. If you linked over a Tailscale machine-share, this is almost certainly the sharer's node key expiring after 180 days. It cannot be seen from your side — ask them to check their Tailscale console and disable key expiry on the shared machine.

A title of theirs disappeared. The sharer hid or removed it. It is hidden here, never dropped, so if it comes back so does your watch state. The same is true of a whole library they stopped granting: re-granting it later brings the same shelf back, with the same resume positions.

Obelo

Home

Getting started

Reference

Going further

Plugins

Sharing with another household

Reaching it from away

When it breaks

Clone this wiki locally