Open source on GitHub

Live audio for Farcaster communities.White-label. Open. Yours.

Zuke is the white-label live audio surface for Farcaster communities. Powered by Juke. Graduated from the ZAO OS lab. Fork, drop in a handful of env vars, deploy.

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, not juke.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.

  1. Consumer code for Juke developer reads + rate-limit observability

    2026-05-25

    Wraps 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.

  2. Recap cast on room.finished (ended_via host/api only; wiring shipped, posting not yet live)

    2026-05-25

    When 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.

  3. Host "End space" button on /live/{id} + admin end-space route

    2026-05-24

    Iframe 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.

  4. Webhook payload parser: event_type / data.room_id / event_id

    2026-05-24View PR

    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.

  5. Register-webhook fix: Juke generates the HMAC secret, not us

    2026-05-24View PR

    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.

  6. Richer /juke-status: recent webhooks + recent spaces + code examples

    2026-05-24View PR

    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.

  7. Admin route to register the Juke webhook server-side

    2026-05-24View PR

    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.

  8. Consumer code for the agent-join endpoint (admin-triggered; auto-join off by default)

    2026-05-23

    Juke'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.

  9. scheduledAt support in the space-create API

    2026-05-23

    POST /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.

  10. Public /live index of ZAO Juke spaces

    2026-05-23

    Anyone 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.

  11. Public build-status surfaces for the Juke team

    2026-05-23

    Three 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.

  12. Auto-cast on recording.ready (wiring shipped, posting not yet live)

    2026-05-23

    After 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.

  13. Public /live/recordings shelf

    2026-05-23

    Merges 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.

  14. "Open in Juke app" CTA

    2026-05-23

    jukeAppDeeplinkUrl(spaceId) returns juke.audio/space/{id}?open=app. Button on /live/{id} routes desktop visitors into the iOS app via universal link.

  15. OG image per space

    2026-05-23View PR

    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.

  16. ?audio=off second-screen mode

    2026-05-23View PR

    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.

  17. Inbound webhook consumer at /api/juke/webhooks

    2026-05-23View PR

    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.

  18. Path B — server-side space creation via POST /v1/developer/spaces

    2026-05-22View PR

    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.

  19. Path A — keyless iframe at /live/{spaceId}

    2026-05-20View PR

    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

1

Fork the repo

github.com/ZAODEVZ/Zuke. Next.js 16 + Supabase + Juke developer API.

git clone https://github.com/ZAODEVZ/Zuke.git
2

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
3

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.

Resources