Skip to content

Crews: start one, invite people, compete as a group - #114

Merged
iamnotstatic merged 2 commits into
mainfrom
iamnotstatic/crews
Oct 15, 2026
Merged

iamnotstatic merged 2 commits into
mainfrom
iamnotstatic/crews

Conversation

@iamnotstatic

Copy link
Copy Markdown
Owner

Second of two PRs for crews, following #113 (web sign-in). It is all server and web, with no CLI change and no npm release, so it works for every user on every version.

What it adds

Page What it does
/crews Crews ranked by ships this week, month and all time, with a start a crew button. Your crews are highlighted; open crews get an open tag.
/crews/new One field, the name. Signed-out visitors go through sign-in first.
/crew/<slug> Crew stats (ships, rank and the gap to the crew above, members shipping), the members' board for the window, and the invite link with a copy button for members. The owner also gets: open/close, rename, hand over, reset the link, remove a member, delete.
/join/<code> The invite. Signed out: join with GitHub plus the three install lines. Signed in: one-click join. It says so instead when you're at 3 crews or the crew is full.
/@handle Profiles now list the crews someone is in.

The top bar gains crews. On screens 420px wide or narrower the bar shows only ◆, so it fits with the account controls.

Rules

  • Score. A crew's number is its members' verified ships, read from the same ship_events rows as the leaderboard, so a crew can never show a ship the main board doesn't.
  • Which weeks count. A ship counts in any week its author was a member, including the week they joined and the week they left. Memberships are never deleted (leaving sets left_at), so a week that has been shown never changes. The check uses EXISTS, so rejoining in the same week cannot count a ship twice.
  • Limits. 3 crews per person and 50 members per crew, both checked inside the INSERT ... SELECT itself, so parallel requests can't get past them. A crew needs 2 members to be ranked.
  • Joining. Invite link only, or one click on an open crew. New crews start invite only. Visitors to an invite-only crew see "ask @owner for an invite".
  • Owners. An owner can't leave without handing over first. If they're the only member, leaving deletes the crew. Deleting frees every member's slot and makes the name usable again.
  • Safety. Every POST is checked for the same origin, and every name is escaped. The copy button is a script served from the site itself, so the CSP stays default-src 'self'.

Tests

server/e2e/crews.mjs runs against a local worker and a stand-in GitHub; the file header has the command line. 48 of 48 pass, and the run was repeated 6 times clean. Against main it fails from the first check.

ok   signed out: /crews renders, links crews in the bar, and "start a crew" goes through sign-in
ok   signed out: /crews/new sends you to sign in first
ok   a one-letter name is refused on the form, with no crew made
ok   starting a crew lands on its page with a welcome
ok   the owner sees the invite link and owner tools, and the crew starts invite only
ok   a crew of one is not on the board yet
ok   the same name again gets its own address
ok   a reserved word cannot take over a route
ok   a crew name is escaped everywhere it shows
ok   signed out, the invite shows the crew, a join with GitHub button and the install lines
ok   signed in, the invite offers one-click join
ok   joining by invite lands on the crew page
ok   opening the invite again as a member just opens the crew
ok   joining twice does not add a second membership
ok   with two members the crew is on the board
ok   an invite-only crew refuses a join from its page
ok   a cross-site join is refused
ok   a member who is not the owner cannot change settings
ok   a visitor to an invite-only crew is told who to ask
ok   an open crew shows a join button to visitors
ok   anyone can join an open crew from its page
ok   this week the crew number is the members' ships added up
ok   ships from weeks before someone joined do not count, even all time
ok   the board shows the same number as the crew page
ok   leaving takes you off the member count
ok   a member who left this week keeps their ships here, marked left
ok   rejoining in the same week does not count anything twice
ok   the owner is asked to hand over before leaving
ok   the owner cannot leave by posting directly
ok   handing over makes the other member the owner
ok   the old owner loses the owner tools
ok   a crew cannot be handed to someone outside it
ok   the owner can remove a member
ok   after a reset the old invite link stops working
ok   renaming keeps the address
ok   a profile lists the crews someone is in
ok   a fourth crew is refused
ok   at three crews the invite says so instead of offering a join
ok   and posting the join anyway changes nothing
ok   five joins at once still stop at three crews
ok   a crew of 50 takes nobody else
ok   deleting asks first
ok   a deleted crew leaves the board and its page is a 404
ok   deleting frees every member's slot
ok   the name can be used again once deleted
ok   the leaderboard bar links to crews
ok   the copy script is served as JavaScript from the site itself
ok   an unknown crew is a 404 page, not an error

all passed

Other checks:

  • e2e/web-signin.mjs still passes 26 of 26.
  • npm test passes 149/149, and the server typecheck is clean.
  • I rendered and looked at every page at desktop and phone widths, signed in and out: the crews board, an owned crew, an open crew as a visitor, an invite-only crew as a visitor, the invite page, the create form and a profile with crews. That's how the two phone fixes in this PR were found.

Limits of the testing:

  • The parallel-join test fires 5 joins at once from one process. The guarantee comes from the single INSERT ... SELECT (D1 runs statements one at a time), not from the test's timing.
  • Board speed is fine at today's size: about 3.9k ship_events rows in production. The board uses correlated subqueries, so revisit it if the crew count grows into the hundreds.

Deploy

  1. Apply migration 0011 remotely and confirm the ✅.
  2. Then deploy.

If the code goes out before the migration, profiles and the leaderboard still render, but /crews errors until the migration runs.

A crew is a name, an invite link and the people who chose to join. Each
crew page has its own weekly board of members, and crews are ranked
against each other on /crews by their members' verified ships, the same
ship_events rows the leaderboard reads, so a crew can never show a ship
the main board does not.

A ship counts for a crew in any week its author was a member, joining
and leaving weeks included. Memberships are never deleted, so a week
that has been shown keeps the members who earned it, and rejoining in
the same week cannot count a ship twice.

Joining needs the invite link, or one click on an open crew. Up to 3
crews per person and 50 per crew, both enforced inside the insert so
parallel joins cannot pass them. A crew needs 2 members to be ranked.
Owners can open or close the crew, rename it, reset the link, remove a
member, hand it over, or delete it; an owner cannot leave without
handing over first.

The top bar gains crews, and profiles list the crews someone is in.
No CLI change.
The board walked every ship for every crew and checked membership row by
row: 1.6s for all time at five times today's data. It now starts from
the memberships and reads each person's ships through the (user, day)
index, 80ms on the same data and identical totals for all 150 crews in
every window.

Someone outside a crew was offered a leave page; a crew link typed in
capitals was a JSON 404; and the remove confirmation said a removed
member needs a new invite, when the current link still lets them back.
@iamnotstatic

Copy link
Copy Markdown
Owner Author

Pre-prod review, fixed in a38c42e:

Speed. Measured at about 5x production size (20k ship_events, 150 crews, 2.5k memberships). The board walked every ship for every crew. It now starts from memberships and reads ships through the (user, day) index.

page before after
/crews this week 180ms 35ms
/crews?window=all 1.6s 80ms
crew page, all time 1.55s 90ms

For comparison, the leaderboard takes 25ms on the same data. The old and new queries were compared on that dataset and give identical totals for all 150 crews in the all-time, this-week and this-month windows (0 mismatches).

Behaviour fixes:

  • Someone outside a crew was offered a leave page. It now redirects to the crew.
  • /crew/Lagos-Crew returned a JSON 404. It now 301s to the lowercase address.
  • The remove confirmation said a removed member needs a new invite, which is wrong because the current link still lets them back. It now tells the owner to reset the link, and for an open crew to make it invite only first.

Checks: each new scenario failed before its fix. e2e passes 51/51 and web-signin 26/26; npm test passes 149/149; the server typecheck is clean.

Remote D1 has only 0011_crews pending.

Known deviation from the prototype: signing in from "join with GitHub" brings you back to the crew or invite page with the join button, so joining takes one more click. Joining on the way back would mean a GET that changes data, which is the thing the same-origin checks exist to prevent.

@iamnotstatic
iamnotstatic merged commit edc499b into main Oct 15, 2026
3 checks passed
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