Files
Signage/docs/architecture/decisions/0001-signage-owns-timing-providers-own-content.md
T
ekklesiaandClaude Opus 5.5 f8b131f4ed
CI / typecheck (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 24s
CI / build (pull_request) Successful in 36s
CI / test (pull_request) Successful in 38s
CI / package (pull_request) Successful in 29s
CI / integration (pull_request) Failing after 1m6s
docs(adr): point platform cross-references at the repository's current location
Link targets only: the Ekklesia repository moved from
git.riverofglorymn.org/River_of_Glory_Church to git.zayin.net/Zayin, and the
old host no longer answers. No decision text changes, which is the
cross-reference correction the ADR rules allow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 22:23:05 -05:00

4.6 KiB

0001: Signage owns timing; a provider owns content

  • Status: Accepted
  • Date: 2026-08-27
  • Related: Ekklesia ADR 0006 (versioned capabilities), ADR 0041 (browser contributions)

Context

Church screens show announcements, scripture, trivia, service times, giving prompts, and photographs. The obvious design — one module that knows about all of them — is the design the legacy Announcements container has, and it is why adding trivia to that product means changing that product.

Signage exists to make the opposite true: adding trivia to the screens means installing a Trivia module, and Signage never changes.

That only works if the line between the two is drawn somewhere principled. Drawn in the wrong place, Signage ends up with an announcementId column, or a provider ends up owning a timer and two displays drift apart.

Decision

Signage owns everything about when. A provider owns everything about what.

Signage decides A provider decides
Which slot is on screen, starting exactly when, until exactly when Which of its items is next in a given selection scope
That a slot's turn has come and a selection must be made Whether that item may repeat, and when its pool is exhausted
That every channel in a group advances together What the item looks like, in its own React renderer
How long a slot lasts when nothing overrides it Its own default duration, and its own availability window
That a resolved selection is persisted, revised, and replayed identically What is inside the payload it hands back

Concretely:

  • Signage calls a provider; a provider never calls Signage's playback. A provider can notify Signage that something changed (invalidation) but cannot start, stop, skip, extend, or reorder anything.
  • Signage supplies a stable opaque selection-scope id and asks for the next item. The provider advances its own cursor within that scope, in its own tables. Signage stores no cursor of the provider's, and the provider stores no timing of Signage's.
  • Signage persists the result — item identity, revision, payload per audience, duration, availability — so that the same slot renders identically for a viewer that joined ten seconds later, survives a restart, and can be replayed by previous without asking the provider again.
  • Signage has no column, field, route, or type naming an announcement, a passage, a question, a campus, or a service. A payload is opaque JSON on the way through and opaque JSON at rest.

The one exception, and why it is not one

Native images are Signage's own content: it stores them, processes them, and renders them. That looks like an exception to the rule until you see how it is built — as Signage's own built-in content provider, signage:native-images, implementing the same registration shape an external provider implements, resolving through the same code path, and persisting into the same resolved-selection table.

There is therefore exactly one selection pipeline, not two. A native image slot and a Trivia slot differ in which provider answers, and in nothing else. Had native images been special-cased, every later feature — history, offline continuation, audience variants, previous — would have had to be written twice and would have drifted.

Consequences

  • A provider module can be written, installed, versioned, and removed without Signage knowing it exists. Signage never enumerates provider modules and declares no dependency on one.
  • A church that never installs Trivia has a Signage that is not missing anything; a timeline simply has no Trivia slot.
  • Provider failure cannot corrupt playback, because playback state was never the provider's to hold. What a failure costs is at most the freshness of one slot's content.
  • Signage cannot offer a feature that requires understanding content — "show announcements sorted by campus" is a Trivia-shaped request that belongs to whichever module owns campuses.

Alternatives

  • One module that knows every content type. Rejected: it is the legacy design, and every new kind of content is a change to the module every screen depends on.
  • Providers push content into Signage's tables. Rejected: two writers to one timeline, no way to attribute a bad row, and a provider outage that leaves stale rows nobody owns.
  • Providers own their own timing and Signage only composes screens. Rejected outright: two independent clocks cannot hold two televisions on the same slide, which is the single thing a synchronized channel group exists to guarantee.