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

81 lines
4.6 KiB
Markdown

# 0001: Signage owns timing; a provider owns content
- Status: Accepted
- Date: 2026-08-27
- Related: Ekklesia [ADR 0006](https://git.zayin.net/Zayin/Ekklesia)
(versioned capabilities), [ADR 0041](https://git.zayin.net/Zayin/Ekklesia)
(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.