19
Features shipped
2
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 + a handful of 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; wiring shipped, posting not yet live)
2026-05-25When a Juke space ends with ended_via in {host, api}, the webhook handler calls autoCastToZao with a 'Just wrapped: {title}' message for /zao, embedding the /live/{id} URL so Farcaster would unfurl the OG card. Skips silent idle-timeouts (ended_via=null) since there's nobody to recap to. Same caveat as recap-cast: autoCastToZao is a stub in the Zuke repo pending a @thezao signer credential, so no cast is actually posted yet. The recording.ready handler still fires its own 'Recording up' follow-up call independently when a recording is on - two-cast pattern is intentional so listeners get a re-engagement ping when the file lands, once casting is live.
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 (a cross-app / iOS-native room we don't own - the end-space endpoint itself has shipped since PR #174), the route falls back to flipping our local juke_spaces row to ended so /live 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 - up to 8 events shown (of the 15 fetched; the fuller 15-row list is what /api/juke/status and /juke-integration.md return) with type / space_id / age / processed-vs-failed pill. (2) Recent spaces - up to 6 rows shown (of the 10 fetched, same JSON/markdown-vs-page split) 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.
Consumer code for the agent-join endpoint (admin-triggered; auto-join off by default)
2026-05-23Juke's free, key-only POST /v1/developer/rooms/{id}/agent-join (shipped 2026-05-23; data-publish only in v1, audio-publish is v1.x roadmap) has two independent consumers in this repo: POST /api/juke/admin/agent-join (an admin-only route with its own inline fetch to the endpoint, mints a short-lived session_token for the caller to store) and the shared helper joinAgentInJukeRoom (src/lib/spaces/jukeAgentJoin.ts), called from an auto-join hook on the room.started webhook, gated off by default behind ZAO_AUTO_AGENT_JOIN (isAutoAgentJoinEnabled()). The two do not share code - the admin route does not call joinAgentInJukeRoom. The mechanism works today for a human-triggered admin join; the auto-join hook exists but stays off because ZOE has no VPS-side consumer for the minted session_token yet - flipping the flag before that exists would join + immediately drop the token with no value. See the 'agents' OPEN_ASKS entry for what's still actually blocked.
scheduledAt support in the space-create API
2026-05-23POST /api/juke/space accepts an optional ISO-8601 scheduledAt and threads it through to Juke (createSpaceSchema in the route). The public /live/create page itself only exposes password + title fields, no scheduled_at input or time prefill - scheduling a space currently requires calling the API directly with a scheduledAt value.
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: v3 header), /juke-integration.md (llms.txt-style markdown, X-ZAO-Juke-Status: v2 header). Single source of truth in jukeIntegrationManifest.ts.
Auto-cast on recording.ready (wiring shipped, posting not yet live)
2026-05-23After persisting recording_url, the webhook handler calls autoCastToZao with a recap cast to /zao, embedding the Juke /live/{id} URL so the Juke OG image renders in the cast preview. In the Zuke repo autoCastToZao (src/lib/publish/auto-cast.ts) is currently a stub - no @thezao Farcaster signer credential is provisioned yet, so it logs and no-ops rather than actually posting. The call site is wired and ready; only the signer credential is missing.
Public /live/recordings shelf
2026-05-23Merges two sources, most-recently-ended first: legacy juke_spaces rows with recording_url set (recording.ready webhook), and any space with a row in juke_recordings (host uploads, imported X Spaces, multi-part) even if recording_url was never set on juke_spaces. Each card shows the Juke OG image (Juke-hosted only) or a generic icon, a source badge (Juke / X Space), and an inline audio player with an "Open in new tab" link when a recording URL exists.
"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. 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 four migration files in scripts/:
scripts/juke-spaces-migration.sql scripts/juke-spaces-migration-2.sql scripts/juke-spaces-migration-3.sql scripts/juke-spaces-migration-4.sql
Set 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 JUKE_CREATE_PASSWORD # shared team password, gates /live/create SESSION_SECRET # 32+ chars, required for SIWF admin sessions ZUKE_ADMIN_FIDS # comma-separated Farcaster FIDs allowed as admin CRON_SECRET # also set as a GitHub Actions repo secret
Deploy. Hit POST /api/juke/admin/register-webhook with the admin cookie. Copy the returned whsec_ into JUKE_WEBHOOK_SECRET, redeploy.