Skip to content

Add Bark transport for free-sideload notification delivery - #9

Merged
nickclyde merged 12 commits into
mainfrom
bark-transport
Jul 7, 2026
Merged

Add Bark transport for free-sideload notification delivery#9
nickclyde merged 12 commits into
mainfrom
bark-transport

Conversation

@nickclyde

Copy link
Copy Markdown
Collaborator

Summary

Adds a per-device Bark transport so notifications reach free-Apple-ID sideloads that can't register for APNs. Pairs with Apollo-Reborn/Apollo-Reborn#578, which registers devices with transport: bark + a Bark push URL.

Changes

  • Schema: migration 000014 adds devices.transport (apns/bark, default apns) + transport_endpoint; also wired into docs/schema.sql and the docker migrate.sh idempotent patch path. Repositories scan/write both columns everywhere devices are fetched, including the watcher-joined SELECTs.
  • New internal/push package: a Sender that routes per-device — APNs path is exact parity with the previous inline code (prod/sandbox, permanent-rejection unregistration); the Bark path POSTs a JSON push to the device's endpoint with title/body/badge/group and an apollo:// deep link opened on tap. Bark failures never unregister a device — a down relay or bad key must not destroy the watcher graph.
  • Workers + API test routes swap their inline APNs sends for the shared sender, so all six /v1/device/<token>/test/* routes exercise the full Bark path.
  • Device upsert accepts the transport via X-Apollo-Transport(-Endpoint) headers (which win over the body) and validates bark devices (endpoint required, http/https).
  • Icon: pushes with no post thumbnail get Apollo's icon instead of Bark's (BARK_DEFAULT_ICON env, sane default).
  • Sound: the payload's sound (traloop.wav) is forwarded extensionless so Bark plays the matching imported .caf; per-user picks ride as ?sound= on the stored push URL (query beats body on bark-server), needing no schema here.
  • docker-compose ships an optional self-hosted bark-server behind the bark profile (make docker-up-bark).
  • README + Getting Started document the setup, including that Bark keys are bearer capabilities and content transits the relay in plaintext (client-side encryption is a flagged follow-up).

Testing

  • go test ./... green; new unit tests cover payload→Bark translation for every notification category, deep-link escaping, default-icon fallback, and sound forwarding.
  • Verified end-to-end with curl against a local rig (postgres/redis + fake Bark server): registration, transport flips apns↔bark on re-upsert, 422 on bark-without-endpoint, test pushes with correct JSON, and failed Bark sends leaving the device row intact.
  • Device-confirmed in production self-hosting: real Bark deliveries with working deep links, default icon, and sound passthrough.

nickclyde added 8 commits July 2, 2026 13:25
Free-Apple-ID sideloads can't obtain the aps-environment entitlement, so
APNs registration always fails and notifications were paid-cert-only.
This adds a per-device delivery transport so those devices can receive
notifications via Bark (App Store app id1403753865) instead:

- devices gain transport ('apns'|'bark') + transport_endpoint columns
  (migration 000014, mirrored in docs/schema.sql and the docker migrate
  patch mounts).
- New internal/push.Sender routes each send per-device: APNs path is
  exact behavioral parity with the old inline apns2 calls; the Bark path
  POSTs the payload as Bark JSON (title/subtitle/body/badge/group/icon)
  with an apollo:// click URL (thread deep link, or apollo://reborn/inbox
  for private messages). Bark failures never set ShouldUnregister, so a
  down bark-server or bad key can't destroy the registration/watcher
  graph; only permanent APNs rejections still delete device rows.
- All four worker send sites plus both API test endpoints go through the
  sender, so /v1/device/{token}/test/* exercises the full Bark path.
- upsertDeviceHandler honors X-Apollo-Transport /
  X-Apollo-Transport-Endpoint headers over the body (Apollo posts
  /v1/device as an upload task, so the tweak can only reliably tag the
  request via headers) and now validates the device (422 on bad input).
- docker-compose gains an opt-in self-hosted bark-server service behind
  the "bark" profile (make docker-up-bark), with docs in README and
  GETTING_STARTED.
Bark pushes with no post thumbnail showed the Bark app icon. The sender
now falls back to Apollo's stock app icon, hosted in the Apollo-Reborn
repo (assets/bark-icons/default.png); BARK_DEFAULT_ICON overrides the
URL. Thumbnail-bearing pushes keep the post image, and devices whose
push URL pins ?icon= (the tweak does this when the user selected an
alternate app icon in Apollo) override both — bark-server gives query
parameters priority over the JSON body.
Bark pushes played the default iOS tone: the payload's sound
(traloop.wav) references a file in Apollo's bundle, and Apollo's
notification service extension — which swaps in the user's in-app sound
pick — never runs for deliveries that land in the Bark app.

The Bark translation now forwards the payload's sound name minus its
extension ("traloop"), matching the .caf conversions of Apollo's sounds
hosted in Apollo-Reborn's assets/bark-sounds (bark-server appends
".caf" to extensionless values). The user imports the .caf for their
sound into the Bark app once; sounds that aren't imported fall back to
the default alert tone, so nothing breaks without the import. The
tweak pins the in-app sound pick via ?sound= on the push URL, which
overrides this body-level value the same way ?icon= does.
golangci-lint (testpackage, copyloopvar, usetesting) has been failing the
Unit Tests workflow since bark_test.go landed: internal tests belong in a
*_internal_test.go file, the Go 1.22 loop-var copy is dead, and tests
should use t.Context().
A Bark-only self-host (docker compose --profile bark) had no way to start:
every worker and the api called LoadAPNS unconditionally and crash-looped
without a .p8, spamming rmq heartbeat errors from the teardown of the
already-opened queue client. Reported by Gliglue on the announcement thread.

LoadAPNS is now tri-modal on the four APPLE_* vars: all set works as
before, all empty disables APNs (nil token, logged as Bark-only mode), and
a partial set fails fast naming the missing vars. It also runs before any
queue client exists, so config failures no longer produce heartbeat noise.

In Bark-only mode the sender skips building apns2 clients and APNs sends
fail with a plain error — deliberately not ShouldUnregister, which would
have deleted every legacy APNs device row. APNs device registrations and
Live Activity registrations get a 422 explaining the mode, and the
live-activities worker drops (and deletes) any leftover rows instead of
pushing.
Plain 'docker compose up' reuses the previously built image, so after a
git pull people silently keep running old code — exactly how the first
Bark-only tester ended up debugging a fix they didn't have. Bake --build
into the docker-up/docker-up-bark targets and every documented compose
invocation; with layer caching a no-change rebuild costs seconds. The
prebuilt-registry-image flow (compose pull, no --build) stays documented
as the explicit alternative, with its caveats spelled out.
Tapping a native notification for a reply or mention lands on the
specific comment; Bark taps only opened the post. The payload already
carries comment_id, so emit Apollo's own share-link form —
/r/<sub>/comments/<post>/_/<comment>/?context=1 — which round-trips its
link parser (regex verified against the binary: the comment id is
captured as (\w+) after an optional (?:/\w+)? slug, so the placeholder
slug must be _, not - — a /-/ link silently opens the post unanchored).
context=1 shows the parent above the comment, matching the native tap.
@Write

Write commented Jul 5, 2026

Copy link
Copy Markdown

Testing this w/ Bark works great, one issue regarding thumbnail implementation, Reddit's API returns "thumbnail": "self" for text-only self-posts. The worker code at internal/worker/subreddits.go:475 checks post.Thumbnail != "" which is true for "self" , so it blindly passes it as the icon, which make the post with no thumbnail have the Bark Icon instead of the selected one.

Reddit fills the thumbnail field with "self", "default", "nsfw",
"spoiler", or "image" when a post has no real thumbnail. The Bark
translation copied any non-empty value into the icon field, which both
sent a bogus icon URL and suppressed the default-icon fallback, so
text posts showed Bark's icon instead of Apollo's. Only accept
http(s) URLs as icons.

Reported in PR #9 by @Write.
@nickclyde

Copy link
Copy Markdown
Collaborator Author

Good catch, thanks for testing! You're right — Reddit uses sentinel values (self, plus default/nsfw/spoiler/image) in the thumbnail field for posts without a real thumbnail, and the non-empty check let those through. Worse, since the Bark request's icon field ended up non-empty, the default Apollo-icon fallback in sendBark never kicked in.

Fixed in eb4c155: the Bark translation layer now only accepts http(s):// values as the icon, so sentinel thumbnails fall through to the default icon (or your ?icon= override if your push URL pins one). I fixed it there rather than in the workers so every producer is covered in one place.

nickclyde added 3 commits July 6, 2026 15:59
The push URL is registrant-supplied, so chasing a 3xx would let a
registration bounce the notification POST at an address of the
redirector's choosing — an SSRF amplifier on open-registration
deployments. bark-server never redirects, so surface the 3xx as a
failed send instead, and name the SSRF trade-off in the README next
to the bearer-capability and plaintext caveats.
When X-Apollo-Transport arrived without X-Apollo-Transport-Endpoint,
the empty header overwrote a transport_endpoint the body did manage to
carry, turning a registrable request into a 422. Headers still win
whenever they're present; the body value now survives only when the
endpoint header is absent.
- GETTING_STARTED.md is now APNs-only with a path chooser up top,
  dropping the interleaved "Bark-only mode? skip this" asides and the
  outdated "no way around the $99/year" note
- New GETTING_STARTED_BARK.md: self-contained free path from Docker
  install to a verified Bark notification; links to the main guide for
  the transport-agnostic exposure and long-term-ops sections
- New ORACLE_CLOUD.md (first draft): Always Free tier walkthrough
  covering A1 capacity/region gotchas, idle reclamation, and the
  VCN + host-iptables double firewall; all stack images verified arm64
- README points at both guides plus the Oracle option
@nickclyde
nickclyde merged commit 82ce277 into main Jul 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants