Docs / temple-live-darshan.md · mirrored from the repo
Temple Live Darshan 🛕📺
The devotional live-video darshan plane: a back-office–curated catalog of Indian temple live streams that consumers browse, save, and get “aarti is live” alerts for. This doc is the single anchoring reference — read it before touching any temple code. It captures the data model, the legal stance, the freshness mechanism, the full lifecycle/scenario matrix, the BO surface, and the consumer contract, so any session (human or agent) can carry the work on.
Status: BO plane in build. Contract + schema + seed + catalog helpers landed (PR1). BO API + console page next (PR2). Consumer “Live Darshan” surface is designed here, built later — the consumer app UX is being built in a separate track and integrates against §8’s contract.
1. What this is (and is NOT)
- A catalog of pointers to official live darshan streams + rich metadata + verification state.
- Rendered on the consumer through the source’s own embedded player (YouTube IFrame Player API).
- NOT a video host. We never download, re-encode, proxy, or re-broadcast temple video. That is the whole legal foundation (§7). “The feed” is a list of stream references, never media bytes.
2. Why a separate plane (not the content feed)
The generated-content plane (content_assets, the ranked image feed) is built around image
embeddings, an offline recommender, and a batch generation pipeline. Live streams have none of that:
they are a small, curated, browsable set with a runtime liveness signal. Folding them into the vector
feed would couple two very different lifecycles and defeat instant legal takedown. So Live Darshan is
its own catalog domain in the same catalog D1, with its own browsable serving (not ranked).
Decision confirmed with the founder (2026-07-19).
3. Data model — two entities, split on purpose
Migration: [packages/catalog/migrations/0006_temples.sql]. Contract:
[packages/shared/src/temple.ts] (TEMPLE_SCHEMA_VERSION). Typed access:
[packages/catalog/src/temples.ts]. Same evolvability principle as the rest of the catalog: each row
is a JSON payload + a few promoted, indexed columns — shape changes need no migration.
| Entity | Table | What it is | Lifetime |
|---|---|---|---|
| Temple | temples | The durable thing a user saves / watchlists / subscribes to. Stable temple_id (temple_<slug>). | Permanent |
| Stream | temple_streams | An ephemeral live-video pointer (channel + resolved video id). A temple has 0..N, ordered by priority. | Rotates daily, dies often |
| Event | temple_events | Append-only domain audit (created/verified/published/…): the BO Activity tab + takedown paper trail. | Permanent |
Principle P1 — decouple identity from source. A user references the temple, never a stream. This single decision is what makes every hard scenario in §5 degrade gracefully: streams rotate and die underneath a temple that stays saved, named, and notifiable.
3a. Two orthogonal state axes
Do not conflate these — a page or query that treats them as one will mislead operators and users.
status— the editorial lifecycle a BO operator drives:draft → in_review → published → unpublished → retired.health— the runtime liveness the resolver derives:live | offline | degraded | dead | unknown.
A published temple is normally offline at 3pm (darshan hours ended). That is not an error;
it is the resting state. The consumer shows “offline — next aarti 6:00pm”, not “unavailable”.
4. Editorial lifecycle (status)
draft ──▶ in_review ──▶ published ──▶ unpublished ──▶ published (reversible)
│
└────────▶ retired (permanent; tombstoned for savers)
- draft — being added; visible only in the BO.
- in_review — metadata + ≥1 stream present; awaiting verify + publish.
- published — live to consumers (per-env status gate, §8). Requires a human verify first.
- unpublished — pulled from consumers, reversible. A soft state, never a delete.
- retired — permanently withdrawn (rights revoked, temple gone). Tombstoned for anyone who saved it; eligible for hard-prune only after a grace period with zero user references.
Principle P2 — soft states, never hard delete. There is intentionally no “delete temple” button that destroys a row users may reference. Unpublish/retire + tombstone (§5) is the path.
5. The lifecycle scenario matrix (the core design question)
The founder’s question: “a temple is on a user’s watch list and the BO admin drops it — what happens
to the user’s experience?” Here is the full matrix. The consumer contract that makes these graceful
is resolveSavedTemples() in [packages/catalog/src/temples.ts] and the SavedTemple shape in the
contract (§8).
| # | Event | Catalog effect | What the saver/consumer sees |
|---|---|---|---|
| 1 | Stream rotates (new live video id each session) | Resolver updates current_video_id | Nothing. They watch the temple; the player resolves the current id at play time. |
| 2 | Stream goes offline (darshan hours end) | health = offline (normal) | “Offline — next aarti 6:00 AM” from the schedule. Save intact. Optional notify-when-live. |
| 3 | Stream dies (channel deleted / embedding disabled) | consecutive_failures ≥ 5 → health = dead; a backup stream auto-promotes | If a backup exists, seamless. If not, temple health rolls up to degraded/dead → unavailable card (“temporarily unavailable”), still saved. |
| 4 | Admin unpublishes a temple | status = unpublished | Removed from discovery/feed. A saver sees a tombstone (“no longer available”) — never a silent vanish. Notifications suppressed. Save row retained; user may remove it. |
| 5 | Admin retires a temple (permanent) | status = retired | Same tombstone, marked permanent. Pruned only after grace + zero refs. |
| 6 | Legal takedown / DMCA (§7) | Instant unpublished + short serving cache TTL | Disappears from consumers within the cache window. Provenance + takedown contact on record. |
| 7 | Metadata edit (rename, new image, fixed deity) | Same temple_id | Saved entry updates in place. Seamless. |
| 8 | Duplicate merged | merged_into = <canonical> | Saver is redirected to the canonical temple (redirect_to), not dropped. |
| 9 | New temple added | draft → verify → publish | Appears in discovery once published. |
| 10 | Notify-when-live | Resolver flips offline → live → emit event | Push “Somnath is live now — evening aarti is on” (§9). On unpublish/retire, subscription cancelled with a courtesy notice. |
| 11 | Stream embedding disabled (is_embeddable = false) | health = degraded; fall back to next stream or “Watch on YouTube” link | Never a broken player. |
Principle P4 — the client always renders something. Every saved
temple_idresolves toavailable | unavailable | tombstoned. The consumer never shows a blank card or crashes on a dropped temple. This closes the pre-existing gap:interactions(likes/saves) live in the consumer D1 with no FK / no cascade / no cleanup to the catalog, so without this resolver a dropped item would silently disappear.resolveSavedTemples()is the reconciliation the platform lacked.
6. Freshness — “keep the list fresh”
Two loops, different cadences:
A. Per-stream resolve/health (frequent, automated). A stream’s live video id rotates and its liveness changes hourly. The resolver, for each published temple’s streams:
- Resolves the current live video for the channel — YouTube Data API
search?channelId=…&eventType=live(authoritative), or the public/livecanonical redirect as a keyless first cut. - Checks
videos?part=statusforembeddable. - Writes
current_video_id,is_embeddable,health,last_checked_at; incrementsconsecutive_failureson failure; marksdeadafter 5.rollupTempleHealth()recomputes the temple’s health + primary stream.
Implemented as streamsDueForCheck() + applyResolveResult() (data side landed). The runner
(a Cron Trigger; ~10–15 min during darshan windows, backing off overnight) is PR3. Needs a
YouTube Data API key in the Secrets Store (Google project manaste-ji, enable YouTube Data API v3).
Quota note: search = 100 units/call; budget by resolving on a schedule + caching, not per view.
Until PR3, the BO’s manual “Resolve now” / “Test” actions cover verification.
B. Catalog discovery (periodic, Claude). “Keep it up to date using Claude from time to time” —
a scheduled pass that discovers new temples and re-checks existing channel URLs, proposing
draft rows into the BO for human review (never auto-publishing). MVP = this seed + a manual refresh;
a temple-scout agent is the natural home later (fits the agent-operated-product vision). The seed
file [packages/catalog/seed/temples.seed.json] is the human/Claude-editable source of truth for
bootstrap + discovery diffs.
7. Legal compliance (read before publishing anything)
The stance, and why each piece exists:
- Embed, never host. We render the source’s own player (YouTube IFrame Player API). YouTube’s ToS grants a sublicense to embed publicly-available, embeddable videos; SDNY has upheld this. We do not download, cache, proxy, or re-broadcast the video stream.
- Prefer authoritative sources. Every stream carries
source_authority:official_temple_trust > government > verified_broadcaster > unverified. Embedding an infringing re-broadcast still exposes us, so unverified sources are never published as-is; a human tiers each stream during review. Great authoritative seeds: the Ministry of Culture portal [utsav.gov.in/public/livedarshan], official trust channels (SVBC/TTD, Somnath, Siddhivinayak, Shirdi, SGPC), and statutory shrine boards (SMVDSB, BKTC, TDB). - Respect
embeddable. If a channel disables embedding, we do not force it — we fall back or link out (“Watch on YouTube”). Enforced viais_embeddable. - Instant takedown. Unpublish is one action and propagates within the serving cache TTL. Each
stream records a
takedown_contact/rights_holderand every action is logged intemple_events. - Sensitive cases flagged in the seed. Jagannath Puri (sanctum telecast contested), SGPC/Golden Temple (Kirtan telecast rights litigated) are seeded with notes and low confidence — legal review before publishing those.
- Image licensing. Still images are freely-licensed (Wikimedia Commons/CC). We store
license+attributionand must render the credit. Seeded licenses are marked pending verification; an operator confirms the license before publish. Optionally cache bytes to R2 (image.r2_key). - DPDP alignment. No PII in stream/telemetry events; watchlist + notify are user-consented; the under-18 personalization ban is absolute (see [events-analytics-dpdp-plan]). Live darshan itself is non-personalized public content, which keeps this plane low-risk.
8. Consumer contract (built later; frozen here)
The consumer “Live Darshan” surface reads the same catalog D1 over its existing read-only
CATALOG_DB binding (exactly like content_assets), gated by status:
- Env gate. Serve
status = 'published'only in production (mirror theFEED_STATUSESdiscipline:publishedin prod;published,in_reviewallowed in staging for QA).CONSUMER_VISIBLE_STATUSESin the contract is the guard. - Browse.
listPublishedTemples()— filter by deity/state/significance/health, “live now” first. - Watch. Resolve the primary stream’s
current_video_id→youtubeEmbedUrl(); if none playable, show the schedule + a “notify me” affordance. - Saved/watchlist.
resolveSavedTemples(userSavedIds)returns, per id, one ofavailable | unavailable | tombstoned(+redirect_tofor merges) so the client renders every entry gracefully (§5, P4). This is the endpoint the existing consumer save/notify UX binds to — the notification demo already anticipates it (saved-temple, “aarti goes live”).
Endpoints to add to consumer-api in the consumer track (not yet built):
GET /api/darshan/temples, GET /api/darshan/temples/:id, GET /api/darshan/saved.
9. Schedule & notifications
schedule.aarti_times (per-temple, Asia/Kolkata) powers “next aarti” hints and the notify-when-live
signal. The push service is a stub today ([packages/notification]); this plane defines the event
it will emit: on a resolver offline → live flip for a temple a user subscribed to, enqueue a
saved-temple/aarti-live notification. On unpublish/retire, cancel the subscription with a courtesy
notice. Wiring is deferred to the notification track; the trigger lives in the resolver (PR3).
10. Back-office surface (PR2)
Follows the console conventions ([docs/console/DESIGN-PATTERNS.md]): server-authoritative collection,
URL-persisted filters, DrawerStack detail, RBAC-gated actions, --color-* tokens only.
/temples— collection. Header counts (published · live now · needs-attention · draft). URL-state filters: deity, state, significance, source-authority, status, health,q, sort. Grid/table rows: image, name, deity + authority + status + health chips, last verified. Bulk actions (RBAC): Publish · Unpublish · Verify · Resolve-now · Retire. “Add temple”./temples/:id(+ drawer, one shared body) — tabs:- Overview — metadata editor (name/deity/tradition/location/languages/schedule/description/image
- license + attribution).
- Streams — per-stream health, priority,
source_authority,embeddable. Test renders the YouTube embed inline so the operator watches it and confirms it’s the right temple + it plays → Verify stampsverified_at/by. Add/remove/reorder streams, Resolve now. - Legal / Provenance — authority, rights holder, license, takedown contact, notes.
- Activity —
temple_eventslog. - Publish gate — Test → Publish/Unpublish/Retire with a dry-run confirm (mirrors Heimdall’s run-scoped publish gate). Only verified temples are publishable.
- Overview — metadata editor (name/deity/tradition/location/languages/schedule/description/image
RBAC (PR2)
New permissions in [packages/rbac] + a console-api seed migration (mirrors 0013/0014):
temple:read (viewer+), temple:edit (editor+), temple:publish (admin). Routes live in a
console-api sub-router services/console-api/src/temples/routes.ts, mounted at /api/temples,
each withIdentity() + requirePermission(...), writing to CATALOG_DB.
Deploy (PR2)
Catalog migration 0006 is applied by deploy-content-creator.yml (landed). Because console-api is
the temple writer, also apply 0006 in deploy-console.yml (idempotent) so the table leads the
routes, and add the temple:* RBAC seed migration to the console DB migration list.
11. Build plan (stacked PRs)
- PR1 — data foundation (this PR). Contract (
temple.ts), migration0006, typed helpers (temples.ts), seed data (38 temples / 41 streams from an authoritative crawl), idempotent seeder (scripts/seed-temples.mjs), migration wired into content-creator deploy, this doc. - PR2 — BO plane. RBAC perms + seed migration, console-api
/api/templesroutes, the/templesconsole page (collection + detail + test-embed + publish gate), deploy wiring. - PR3 — freshness runner. Cron resolver (YouTube Data API key in Secrets Store), health rollups, notify-when-live event emission.
- Consumer track (separate). The Live Darshan surface + saved/tombstone rendering against §8.
12. Seed provenance snapshot (2026-07-19)
38 temples (14 Shiva/Jyotirlinga, ~12 Vishnu/Krishna/Sai, ~12 Devi/Ganesha/Sikh/other). Streams by
authority: 21 official-trust, 18 government, 1 verified-broadcaster, 1 unverified. Confidence: 10
high / 18 medium / 10 low. 5 temples intentionally seeded with no stream (identity solid, no
credible official source found — e.g. Bhimashankar, Grishneshwar, Kamakhya). Every temple is draft;
image licenses are pending until an operator verifies them. The crawl deliberately stored channel
URLs/handles + an authority tier rather than fabricating live video ids (which rotate and which the
operator confirms in the BO anyway).