There’s a moment in every multi-platform tool where the abstraction clicks into place and you feel briefly like a proper software architect. keryx posts reels to four platforms, and the whole thing sits behind an interface you could read aloud in one breath:
// Publisher posts one reel to one platform.
type Publisher interface {
// Name is the platform key (e.g. "instagram").
Name() string
// Publish uploads video with post's copy and returns the live post URL/id.
Publish(ctx context.Context, video Video, post PostMeta) (PostResult, error)
}
(pinned at d643a7a.) Instagram, YouTube, TikTok, LinkedIn: each adapter self-registers under its name, takes the same platform-neutral PostMeta, maps it onto its own caption limits and upload dance, and hands back a URL. Adding a fifth platform is a directory and an init().
So that’s the tidy half of the story. This post is about the other half… because every abstraction leaks somewhere, and this one didn’t leak where I thought it would.
The part I thought would hurt
I’d braced myself for posting itself to be the mess. Captions have different length limits, uploads are chunked differently, one platform wants a cover frame… and none of it mattered, not architecturally. Those are exactly the differences an adapter is for: each backend soaks up its platform’s quirks behind Publish and the caller stays innocent. Even the properly annoying cases stay contained. TikTok won’t render a clickable link, so its adapter drops the URL from the caption rather than posting dead text; LinkedIn wants the link in a first comment. Per-platform behaviour, one interface, no drama.
No, the leak is in the bit that happens before any posting: staying logged in.
Then came the tokens
Token refresh looked like it ought to be one algorithm: token gets old, you exchange it, you store the new one. The spec that fell out of building it says otherwise, in as many words: refresh isn’t one algorithm, it’s a per-platform strategy behind a common interface. Behold the taxonomy:
- Instagram hands you a ~60-day token you refresh in place. Same credential, new lease. Tidy.
- YouTube gives you a durable refresh token that never rotates: refresh mints a fresh access token and there’s nothing new to store. Its refresher takes the write-back hook every other backend needs and ignores it, by name:
_ oauth.WriteBack. That underscore is the leak, wearing a suit. - TikTok rotates the refresh token on every use. The exchange hands you a new one, and the old one is now a used match: the adapter’s own comment says the caller must persist it immediately, because losing that write means the next refresh has nothing valid to present, and re-auth is the only way back.
- LinkedIn doesn’t refresh at all. Without a partnership programme I don’t qualify for, a 60-day token simply expires, full stop. The only honest thing a tool can do is what keryx does: watch the calendar and raise an alert saying re-auth required by this date. An abstraction over “refresh” whose fourth implementation is “there is no refresh, set an alarm” has told you something true about the world.
One interface, four contradictory theories about what a token even is. And it goes one layer deeper: the platforms also disagree about what your app is allowed to be, before any token exists (YouTube wants a verification-and-audit gauntlet for public posting; TikTok caps you at posting to yourself until you’ve passed review). The interface can’t hide that; it can only be upfront about it in each backend’s setup docs.
Why I built all four first
Here’s the decision that saved this design, and I made it on purpose: I didn’t write the refresh abstraction until all four publishers existed. Get the posting working first, then go looking for the abstraction, because until all four are up you’re guessing at what they’ve got in common.
It’s the old rule of three with the stakes cranked up. If I’d drawn the refresh interface after Instagram (refresh-in-place, so pleasant, so misleading), I’d have baked in “exchange old for new, store the result”… which is wrong for YouTube (nothing to store), a disaster for TikTok (storing it isn’t optional, and it’s a different token), and plain gibberish for LinkedIn (nothing to exchange). Instagram’s shape would’ve gone through life passing itself off as the general case, and every backend after it would be bending itself around a first guess it never got a vote on.
Instead the interface got distilled out of four worked examples, and what it unifies is only what’s genuinely universal: a backend can be asked to make its credential current, and to say when a human needs to re-auth. That’s the lot. That one sentence survives contact with all four platforms. Everything more specific stays down in the adapters, where the weirdness gets paid for locally.
Could I have guessed all this after building Instagram? Probably. I’d also have guessed it wrong, and spent the next three backends unpicking the guess… which is the pricey sort of quick. Four publishers up front, then the interface. Slower, and I’d do it again tomorrow.





