Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 5 additions & 128 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,6 @@
# AGENTS.md
Read `README.md` for the three sites (moq.dev, moq.pub, moq.watch), setup, and the broadcast URL scheme.

## Project Overview

moq.dev is a web blog and demo for Media over QUIC (MoQ) protocol. It's built with Astro, Solid.js, and uses WebTransport to connect to MoQ relay servers for live streaming.

The repo holds three sites, each its own Cloudflare Worker but built and deployed together:

| Site | Source | What it is |
|-------------|-------------|-------------------------------------------------------|
| `moq.dev` | `src/` | The Astro blog and demos |
| `moq.pub` | `sites/pub` | A bare `<moq-publish-ui>` publisher, no framework |
| `moq.watch` | `sites/watch` | A bare `<moq-watch-ui>` player, no framework |

## Essential Commands

```bash
# Development
just dev # Start the moq.dev dev server with auto-open
just dev-pub # moq.pub on :5174
just dev-watch # moq.watch on :5173

# Build & Deploy
just build # Production build of all three sites
just deploy # Deploy all three to Cloudflare (staging by default)
just deploy live # Deploy to production
just prod # Build and preview production locally

# Code Quality
just check # Run Biome linting and TypeScript checking
just fix # Auto-fix code formatting/lint issues
```

## Required
# Required

- Dig into the root cause and fix it at the source. Never work around a fixable bug with a retry, sleep, or timeout.
- Fail loud and early. Error on unsupported or malformed input rather than warn and continue.
Expand All @@ -49,7 +18,7 @@ just fix # Auto-fix code formatting/lint issues
- Add the AI marker `(Written by <model>)` to any posts on GitHub, excluding commit messages that contain `Co-Authored-By:` trailers.
- Any AI comments may be challenged, and not confused with human maintainers.

## Guidelines
# Guidelines

- Comments should explain the non-obvious why, and never the history.
- Inline simple helpers.
Expand All @@ -63,102 +32,10 @@ just fix # Auto-fix code formatting/lint issues
- Try to do stuff asynchronously. ex. ask about follow-ups while checks run.
- If the core problem is addressed, ship it instead of spinning your wheels on meaningless revisions.

## Development
# Development

PRs target `main`. Before starting, `git fetch origin` and set the upstream to the base branch.
`git stash` is one stack shared by every worktree; set work aside with a commit instead.
Use the Nix dev shell (direnv, or `nix develop --command just ...`) so tooling matches CI.
Run `just fix` before committing. `just check` alone is not verification: look at the change in the dev server.
Run `just fix` before committing. `just check` alone is not verification: look at the change in the dev server (`just dev`, `just dev-pub`, or `just dev-watch`).
Quests: before starting work, run `quest guide` and follow it. The `quest` binary comes from the kixelated/quest flake input and serves the quest skills; change them upstream and bump the input.

## Architecture Overview

### Technology Stack
- **Framework**: Astro (static output)
- **UI Components**: Solid.js for interactive elements
- **Styling**: Tailwind CSS
- **Build**: Vite
- **Code Quality**: Biome for linting/formatting
- **Package Manager**: bun
- **Task Runner**: just
- **Toolchain**: pinned in `flake.nix`; `nix develop` (or direnv, via `.envrc`) provides bun, node, and just

The Bun version in nixpkgs must match `packageManager` in `package.json`.
CI reads its version from `package.json` and evaluates the flake assertion to
catch drift. When a weekly Dependabot Nix update changes Bun, update
`package.json` in the same PR.

### Key Components

**MoQ Client Implementation** (`@moq/publish` + `@moq/watch` packages):
- Custom web components: `<moq-publish>`, `<moq-watch>`
- UI wrapper components: `<moq-publish-ui>`, `<moq-watch-ui>`

### Important Patterns

- **No REST APIs**: Uses WebTransport directly for streaming
- **Stateless**: No database or user management
- **Error Handling**: Component-level with `src/components/fail.tsx`
- **Authentication**: Basic JWT support via query parameters for demo broadcasts
- **Content Management**: MDX files in `src/pages/blog/` for documentation

### The moq.pub / moq.watch URL scheme

A broadcast is identified by its path: `/<project>/<name>`, e.g.
`moq.pub/try/h2z15dgmdh.hang`. The project is the relay tenant and the
name is everything after it, so names may contain slashes. The same path on
either site refers to the same broadcast: publish at `moq.pub/try/x.hang` and
watch it back at `moq.watch/try/x.hang`.

Anything that *isn't* part of the broadcast's identity stays in the query
string: `?relay=<url>`, `?cloudflare=<subdomain>`, `?jwt=<token>`, and
(moq.pub only) `?source=camera`. `?cloudflare=draft-16` is shorthand for
`https://draft-16.cloudflare.mediaoverquic.com`; the project path segment still
carries Cloudflare's relay token. `cloudflare` and `relay` are mutually
exclusive.

A bare `moq.pub/` mints a private broadcast in the `try` project from
`POST ${PUBLIC_API_URL}/try/token` and keeps the publish token in its own URL.
**A watch link never carries `?jwt=`**: moq tokens are prefix-scoped with
separate publish (`put`) and subscribe (`get`) grants, so a shared publish token
lets anyone publish. moq.pub links to moq.watch only for `try` on the default
relay, and moq.watch mints its own subscribe token from `/try/watch` for a
tokenless `try` path. Other projects get no link; swap the hostname.

`sites/lib` holds the scheme itself, shared by both sites and by both the Worker
and the Vite dev server so they can't drift:

- `broadcast.ts`: parse and build `/<project>/<name>`, and the relay URL
- `route.ts`: old `?project=&name=` links redirect into the path
- `try.ts`: mint `try` credentials from the API
- `worker.ts`: the Worker both sites export
- `dev.ts`: a Vite plugin giving `just dev-pub` / `just dev-watch` the same
routing (`just dev` is the Astro site and doesn't use it)

Two things are easy to break here:

- `assets.run_worker_first: ["/"]` in each `wrangler.jsonc` is load-bearing. `/`
matches `index.html`, so without it Cloudflare's asset server answers first and
the Worker never runs and old `?project=&name=` links don't redirect.
- The Worker can't decide "asset vs. page" by looking for a dot, because names
end in `.hang`. It asks the asset store and falls back to the page on a 404.

### Deployment

- Cloudflare Workers via Wrangler, one per site
- `just deploy` for staging, `just deploy live` for production
- Deploys are manual; nothing ships on merge to `main`

`just deploy <env>` builds all three sites in that mode and uploads each Worker.
The player sites read the repo-root `.env.<env>` files that the Astro site uses,
so `PUBLIC_RELAY_URL` is the single place the relay is configured. Staging is
`new.moq.dev`, `new.moq.pub`, and `new.moq.watch`.

The player sites deploy before moq.dev.

## Development Tips

- Broadcasts are ephemeral - no persistence layer
- The `@moq/publish` and `@moq/watch` packages handle all MoQ protocol implementation
- For new blog posts, add MDX files to `src/pages/blog/`
- Component changes in `src/components/` automatically reload with HMR
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
devShells.default =
assert pkgs.lib.assertMsg
((builtins.fromJSON (builtins.readFile ./package.json)).packageManager == "bun@${pkgs.bun.version}")
"Update package.json to match Nix Bun ${pkgs.bun.version}";
"Bun pins disagree: packageManager in package.json must be bun@${pkgs.bun.version}, the Bun in the pinned nixpkgs. Update them together.";
pkgs.mkShell (
{
packages = with pkgs; [
Expand Down
4 changes: 1 addition & 3 deletions quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
The sites host the `@moq` player worklets and workers themselves, so the
hosted path gets real traffic.

The agent context is lean and the checks cover what they claim, passing on a
developer's machine.
The checks cover what they claim, passing on a developer's machine.

## Required

- [Lean AGENTS.md](/quest/m0/lean-agents-md.md) - agent context keeps only rules and conventions, no repo description
- [Type-check .astro files](/quest/m0/astro-typecheck.md) - `just check` covers `.astro` via the TypeScript 7.1 content mapper
- [Dogfood hosted worklets](/quest/m0/dogfood-assets.md) - the sites serve the @moq worklets and workers themselves
19 changes: 0 additions & 19 deletions quest/m0/lean-agents-md.md

This file was deleted.

2 changes: 2 additions & 0 deletions sites/pub/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ function mount(broadcast: Broadcast.Broadcast, params: URLSearchParams) {
const ui = document.createElement("moq-publish-ui");
ui.appendChild(publish);

// Only here does moq.watch mint its own subscribe token. Share the bare path:
// this page's ?jwt= is a publish token, and would let anyone publish.
if (broadcast.project === "try" && !params.has("relay") && !params.has("cloudflare")) {
const share = new URL(import.meta.env.PUBLIC_WATCH_URL);
share.pathname = Broadcast.path(broadcast);
Expand Down
2 changes: 2 additions & 0 deletions src/lib/try.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export async function publish() {
import.meta.env.PUBLIC_RELAY_URL,
);
if (!url) throw new Error("Invalid relay configuration.");
// The bare path: the watch page mints its own subscribe token, and the
// publish token would let anyone publish.
const share = new URL(import.meta.env.PUBLIC_WATCH_URL);
share.pathname = Broadcast.path(broadcast);
return { broadcast, url, share: share.toString() };
Expand Down
Loading