21
Features shipped
0
Spaces hosted
0
With recording
0
Webhook events
What you get
Listen surface
Public /listen page. Live now, scheduled next, recording shelf. No auth required to listen - SIWF only for participation.
Host + admin tools
/live/create UI plus admin API for programmatic space creation, end-space, recording handling, agent join.
Webhook receiver
HMAC-verified inbound webhooks (room.started, finished, participant.*, recording.ready). Idempotent. Auto-cast on recap.
Public status dashboard
/juke-status mirrors what you shipped + recent webhook deliveries + open asks. JSON + markdown + HTML, all in sync.
Why Zuke, not raw Juke
- Your domain. Listeners land at
audio.yourbrand.com, notjuke.audio/space/xyz. Cast unfurls show your card. - Your database. All space metadata, participant counts, recordings land in your Supabase - queryable from your existing community tooling.
- Your integrations. Recap casts from your own community account, custom CTAs on the live page, agent participants tied to your accounts.
- No infra. Juke runs the LiveKit cluster, the iOS app, the iframe. You ship a Vercel project + a Supabase + 6 env vars.
The build - every feature, newest first
Sourced from jukeIntegrationManifest. Every PR linked where one exists. Auto-updates as we ship.
Consumer code for Juke developer reads + rate-limit observability
2026-05-25Wraps Juke's PR #175 ship (2026-05-25): GET /v1/developer/spaces/{id} returns RoomDetailResponse (status + participants + recording in one call), GET /v1/developer/webhooks/{id} returns delivery health, DELETE /v1/developer/webhooks/{id} cleans up orphans (already existed). New helper at src/lib/spaces/juke-api-reads.ts surfaces all three behind one client + extracts X-Juke-Rate-Limit-Limit / Remaining / Reset from every response, logging a warn when remaining drops below 20% of the limit. Stale-room cron at /api/cron/juke-stale-rooms now uses GET /spaces/{id} as the authoritative source - only flips a row to ended when Juke confirms ended (or 404s), trusting Juke over our webhook timeline. Fallback to the older heuristic when JUKE_API_KEY is absent (local/preview). Admin route /api/juke/admin/delete-webhook wraps DELETE with an introspection-before-delete audit log.
Recap cast on room.finished (ended_via host/api only)
2026-05-25When a Juke space ends with ended_via in {host, api}, the webhook handler auto-casts a 'Just wrapped: {title}' message to /zao from @thezao via autoCastToZao. Embeds the /live/{id} URL so Farcaster unfurls the OG card. Skips silent idle-timeouts (ended_via=null) since there's nobody to recap to. The recording.ready handler still fires its own 'Recording up' follow-up cast independently when a recording is on - two-cast pattern is intentional so listeners get a re-engagement ping when the file lands.
Host "End space" button on /live/{id} + admin end-space route
2026-05-24Iframe Leave is a pure LiveKit room.disconnect() with anon: participant identity - no API call, so rooms we create via developer API stay alive until LiveKit's 300s empty-room timeout. EndJukeSpaceButton on /live/{id} (gated to host or admin via SSR session) calls POST /api/juke/admin/end-space which proxies to Juke's POST /v1/developer/spaces/{id}/end (Nicky's PR #174). On a 404 from Juke (endpoint not shipped yet, or cross-app room), the route falls back to flipping our local juke_spaces row to ended so /spaces stops showing dead rooms as Live. The webhook handler remains the source of truth for the canonical room.finished event - we do not pre-flip our DB on the success path. Two-step confirm pattern on the button prevents fat-finger ends.
parseWebhookEvent now reads Juke 2026-05-23 shape (event_type + event_id at top level, data.room_id for the space id) instead of the legacy event / type / data.id fields. Defensive aliases keep the older shape working. readParticipant accepts fid / participant_fid / user_fid / host_fid + display_name / displayName / username for human-or-agent identification. Result: webhooks no longer log "no space_id" and lifecycle updates apply.
Initial admin route POSTed { url, events, secret } and Juke returned 422 extra_forbidden on the secret field - Juke generates the secret server-side and returns it in the response. Route now POSTs { url, events } only, captures juke.secret from the response, returns it with an action_required instructing the admin to copy it into Vercel's JUKE_WEBHOOK_SECRET env. Server logs the registration with the secret redacted.
Three new sections on the public dashboard. (1) Recent webhooks - last 15 events with type / space_id / age / processed-vs-failed pill. (2) Recent spaces - last 10 juke_spaces rows with status pill + time marker + participant count + recording link. (3) Code examples - 4 reference snippets matching production (create-space, embed, webhook verify, subscribe). Plus OG + Twitter card meta on the page itself, and recent_spaces + recent_events arrays added to /api/juke/status and /juke-integration.md.
POST /api/juke/admin/register-webhook calls Juke /v1/developer/webhooks from a Vercel context that already has JUKE_API_KEY loaded. Juke generates the HMAC secret server-side and returns it in the response; the admin caller copies it into the JUKE_WEBHOOK_SECRET env var (Production + Preview + Development) and redeploys. Admin-only.
Recurring weekly Juke schedule script
2026-05-23scripts/schedule-zao-recurring.ts pre-creates Juke spaces for ZAO's weekly events (fractal call, ZAOstock standups). Idempotent (dedupes against juke_spaces.scheduled_at +/- 30min). Safe to wire into a weekly cron.
Unified /spaces Live tab - Juke spaces alongside Stream/100ms
2026-05-23Browser-side juke_spaces query in parallel with the rooms query, realtime subscription on both tables, JukeLiveSection rendered above ZAO stages when active rows exist. Cards route to /live/{id} with a purple Juke accent.
Juke as 3rd audio provider in the Go-Live modal
2026-05-23HostRoomModal on /spaces now exposes Juke alongside Stream.io + 100ms. When picked, the modal collapses (mode/theme/gate/multistream hidden), POSTs /api/juke/space, redirects to /live/{spaceId}.
Schedule-a-space UI on /live/create
2026-05-23Operator form to pre-create Juke spaces with a real scheduled_at - threads through to Juke. Optional announceCast toggle. Pre-fills "1h from now, rounded up to the next half hour".
Public /live index of ZAO Juke spaces
2026-05-23Anyone can browse Live / Scheduled / Recent ZAO Juke spaces without auth. Each card routes to /live/{id} (keyless iframe). Includes a paste-link form for non-ZAO spaces.
Public build-status surfaces for the Juke team
2026-05-23Three mirrors of this manifest: /juke-status (HTML dashboard with live stats + architecture diagram), /api/juke/status (JSON, CORS open, X-ZAO-Juke-Status: v1 header), /juke-integration.md (llms.txt-style markdown). Single source of truth in jukeIntegrationManifest.ts.
Auto-cast on recording.ready
2026-05-23After persisting recording_url, the webhook handler posts a recap cast to /zao via the @thezao official account, embedding the Juke /live/{id} URL so the Juke OG image renders in the cast preview. Silently no-ops when the @thezao signer env is missing.
Public /live/recordings shelf
2026-05-23Lists ended Juke spaces with recording_url, most-recent first. Server-fetched from juke_spaces. Each card shows the Juke OG image + a "Listen to recording" CTA. Populated by the recording.ready webhook.
"Open in Juke app" CTA
2026-05-23jukeAppDeeplinkUrl(spaceId) returns juke.audio/space/{id}?open=app. Button on /live/{id} routes desktop visitors into the iOS app via universal link.
generateMetadata pulls juke.audio/space/{id}/opengraph-image for Open Graph + Twitter card meta tags. Cast/X shares of /live/{id} render the Juke-branded card without ZAO having to render its own.
jukeEmbedUrl(spaceId, { audioOff: true }) returns the embed with audio disabled. UI offers a "Mute (second screen)" toggle on /live/{id}. Solves the laptop-alongside-iOS-app double-broadcast case.
HMAC-SHA256 verifier for X-Juke-Signature: t={ts},v1={hex} over `{ts}.{body}`. 5-minute replay window. Idempotent via signature_hash unique constraint. Handlers cover room.started, room.finished, participant.joined, participant.left, recording.ready.
Key-only auth (X-Juke-Api-Key), room owner derived from app.owner_fid. Admin-or-password gated route at /api/juke/space; /live/create web form. Persists juke_spaces row on success.
Public route that embeds juke.audio/embed/{id} with ZAO chrome. No API keys, anonymous listen by default, SIWF inside the iframe for participation.
Deploy your own
Fork the repo
github.com/ZAODEVZ/Zuke. MIT-style license. Next.js 16 + Supabase + Juke developer API.
git clone https://github.com/ZAODEVZ/Zuke.git
Provision Supabase + apply migrations
Create a Supabase project. Apply the two migration files in scripts/:
scripts/juke-spaces-migration.sql scripts/juke-spaces-migration-2.sql
Set 6 env vars + register the webhook
Apply for a Juke developer key at juke.audio/developers. Then in Vercel:
NEXT_PUBLIC_SUPABASE_URL SUPABASE_SERVICE_ROLE_KEY JUKE_API_KEY JUKE_WEBHOOK_SECRET # filled by step-6 webhook register ZUKE_ADMIN_PASSWORD CRON_SECRET
Deploy. Hit POST /api/juke/admin/register-webhook with the admin cookie. Copy the returned whsec_ into JUKE_WEBHOOK_SECRET, redeploy.