Skip to content

Live shows: a concert kind, artists, doors and encore, and tickets - #125

Merged
ralyodio merged 1 commit into
mainfrom
feat/live-concert-events
Sep 12, 2026
Merged

Live shows: a concert kind, artists, doors and encore, and tickets#125
ralyodio merged 1 commit into
mainfrom
feat/live-concert-events

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

NixAmp core learns what a live show is, so a concert site can be as thin a client as BackToSchool.help is. Nothing here is named after a site.

A kind on every event (talk, class, concert). It decides the layout, and /api/v1/events?kind=concert is the directory a venue reads. Concert presets: concert-viewer (stage + till), concert-ticketholder (stage, setlist, tip jar, merch, chat), concert-artist (adds soundcheck and the controls). New panels: stage-video, setlist, tip-jar, merch, tickets, lineup, soundcheck.

An artist role beside moderator. Artists perform without presiding: doors, start, encore, end, publish to the room; not the guest list, not moderation. Cancelling and archiving stay with the host.

Doors and an encore. POST /api/v1/events/:id/{doors,start,encore,end,cancel,archive}. encore is its own state because a listener arriving during one is arriving at something.

Tickets: a paid pass to one room. The same x402 machinery as the crawler paywall, scoped to an event and paid to the event's own payTo rather than to the platform.

  • GET /api/v1/events/:id/tickets — price, and whether you hold one
  • POST /api/v1/events/:id/tickets — X-PAYMENT proof in, ticket out
  • POST /api/v1/events/:id/tickets/comp — the guest list, hosts only
  • The room answers 402 with the price rather than 404; the ticket rides in x-nixamp-ticket or ?ticket= (an <audio src> cannot set a header)
  • Each event's passes are signed with a secret derived from NIXAMP_TICKET_SECRET (or the CoinPay key) and the event id, so a ticket to Friday is not a ticket to Saturday
  • No COINPAY_X402_KEY means every event is simply free

Also: the /live/:slug page's brand is read from the shell it is injected into instead of being hard-coded to BackToSchool.help, and a concert's JSON-LD is a MusicEvent with an Offer.

Schema changes are additive and idempotent (ADD COLUMN IF NOT EXISTS, CHECK constraints replaced by name), matching the repo's on-demand convention.

Tests

bun test test web/test: 538 pass, 1 skip, 0 fail. bun run typecheck: clean. New test/tickets.test.ts (10) plus concert cases in test/layouts.test.ts, test/live-events.test.ts and test/live-api.test.ts.

The client is c0ncerts.com, in a PR of its own.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f

A live event now carries a kind (talk, class, concert), so one NixAmp serves
a school and a venue without either knowing about the other. Concerts bring
their own layout presets: a stage, a setlist, a tip jar, a merch shelf and a
till. The directory can be asked for one kind.

An artist is an invitation role beside moderator. They go on stage and drive
the show -- doors, start, encore, end -- without being handed the guest list;
cancelling and archiving stay with the host. The encore is its own state,
because a listener arriving during one is arriving at something and "ended"
would turn them away.

A ticket is a paid pass to one room, over x402 and settled by CoinPay: the
same machinery as the crawler paywall, scoped to a single event and paid to
the event's own address rather than to the platform. The room answers 402
with the price instead of pretending not to be there, the ticket rides in
x-nixamp-ticket or in ?ticket= for an element that cannot set a header, and
each event's passes are signed with a secret derived from the event id, so a
ticket to Friday is not a ticket to Saturday. Hosts can comp one, because a
venue with no guest list can only answer a problem with "pay again".

Nothing here is named after a site. The event page's brand is read from the
shell it is injected into rather than hard-coded, and a concert's JSON-LD is
a MusicEvent with an Offer on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f
@ralyodio
ralyodio marked this pull request as ready for review September 12, 2026 11:23
@ralyodio
ralyodio merged commit 480e842 into main Sep 12, 2026
2 checks passed
@ralyodio
ralyodio deleted the feat/live-concert-events branch September 12, 2026 11:23
ralyodio added a commit that referenced this pull request Sep 12, 2026
After rebasing on #125 the INSERT carries kind, doors and the ticket
fields, so the positional fake read status out of the wrong slot and
every bridged party came back 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f
ralyodio added a commit that referenced this pull request Sep 12, 2026
… a nixamp room (#127)

* Sign in to bittorrented.com with nixamp, and make its watch party a nixamp room

nixamp.com becomes an OAuth 2.1 authorization server, and a watch party on a
site that connects through it becomes an ordinary nixamp live event with a
room -- so the same party is joinable from the web app, the terminal, the
desktop app, the television and an agent over MCP, without any of them
learning anything about torrents.

Which side is the authorization server was the first question. nixamp already
keeps the accounts, the revocable tokens and the provider identities, and
bittorrented.com keeps none of those, so nixamp is the AS and bittorrented is
the client. src/oauth.ts stays what it was -- nixamp signing in WITH GitHub --
and src/oauth-server.ts is the other direction.

It is 2.1 rather than 2.0, which is to say the things 2.0 left optional are
not optional here: PKCE with S256 on every request from every client, public
or confidential; redirect URIs matched exactly, with only a loopback port
allowed to vary because a CLI cannot know its port before it listens; a code
spent once, where a second use withdraws everything the first produced;
refresh tokens that rotate, where a retired one presented again withdraws the
whole family. There is no implicit grant and no password grant, and both are
refused by name rather than shrugged at. RFC 8414 metadata says so at
/.well-known/oauth-authorization-server.

Access tokens are nixamp's own `nxa_` tokens with a client's name on them,
which is what makes them revocable and what makes every existing /api/v1 route
understand one with no change. An error that cannot be redirected is a page:
bouncing a refusal to an unregistered redirect URI would make this an open
redirector.

The bridge itself (src/watch-party.ts) is deliberately thin. A bridged party
is a live_events row like any other, plus one row saying which external party
it is, where to watch it, and the second the host says everybody is at. That
is what makes discovery, invitations, chat, layouts and /live/:slug work with
no special case. Bridging is idempotent, because the client calls it every
time somebody opens the party page. A watch link must be on the client's own
site, read off the redirect URIs it registered rather than configured twice.

Surfaces: web gets a Watch parties panel and a Connected apps list in the
Account panel; `nixamp party` gets list/join/host/sync/end; `nixamp mcp` is a
new stdio MCP server -- JSON-RPC on a pipe, no dependency -- offering those
same five as tools to an agent.

545 tests pass, typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f

* Teach the OAuth test's fake live_events row the concert columns

After rebasing on #125 the INSERT carries kind, doors and the ticket
fields, so the positional fake read status out of the wrong slot and
every bridged party came back 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio added a commit that referenced this pull request Sep 12, 2026
Two fragmented-MP4 packagings met: the relay's, with a Packaging type and a
per-run token on the init segment, and the transport-stream work's, which
asks for fMP4 for an H.265 channel. The relay's shape stays and the H.265
rule folds into it: playlist(id, fmp4) picks fmp4 when asked, else the
compression policy's packaging. One segmentType, the finer one. The
transport tests learn the tokened init name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEguYxw44VkpjFrxxGrNGx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant