Skip to content

feat(app): invite teammates with one link that fills in Join your team - #561

Merged
iamnbutler merged 7 commits into
team-onboardingfrom
invite-link
Oct 22, 2026
Merged

iamnbutler merged 7 commits into
team-onboardingfrom
invite-link

Conversation

@iamnbutler

@iamnbutler iamnbutler commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Teammates join with one invite link, not by copying a directory URL and a secret separately. Part of #538 (g2 in the onboarding review) and #550.

  • Invite: Settings → Team → Invite… (and the last step of Get your team started) shows one copyable link, https://ace.githubnext.com/join#directory=…&secret=…. Chat apps linkify it, unlike ace://. The page (githubnext/ace-site) offers the download, then hands off to ace://join?…. The invite stays in the fragment, so the server never sees it. A toggle shows the URL and secret separately. A new local-owner-only team-invite host op reads them from the directory config and Keychain.
  • Join:
    • Opening the link registers the scheme per build channel (ace, ace-canary, ace-dev) and opens Join your team with both fields filled in.
    • Pasting the link into the URL field of either join form fills both fields.
    • The person still has to choose Check and join.

Smoke-tested:

  • A source host on a separate port, joined to a local wrangler dev directory: the ace:join event prefilled the form, joining succeeded, paste-to-fill worked, and Invite… copied the link.
  • The Ace-dev desktop build: open "ace-dev://join?…" opened Join your team with the URL and secret filled in.

Replacing the shared secret with tailnet identity is proposed separately.

Built in Ace

No. Claude Code wrote this while the user was away, outside Ace, and drove the desktop build with Desktop Tools to check the deep link. Ace can't yet run an unattended multi-hour polish pass across several lanes. Tracked under #5.

The Invite screen and Settings → Team → Invite… copy an ace://join link carrying the directory
URL and team secret. Pasting it into either join form fills both fields, and the desktop app
registers its channel's scheme so opening the link brings up Join your team, filled in, for the
person to check and join.

Why: apps/desktop/electrobun.config.ts | each build channel registers its own scheme (ace, ace-canary, ace-dev) so a dev or Canary build never answers stable's invite links; apps/app/src/invite.ts reads all of them.
Why: apps/app/src/invite.ts#function parse | an invite link only fills the join form; joining must stay an explicit Check and join, since a link can name any directory.
Chat apps linkify https, not ace://. The link opens ace.githubnext.com/join, which offers the download and hands off to ace://join. Pasting either form still fills Join your team.

Why: apps/app/src/invite.ts#function link | the invite must stay in the URL fragment so the secret never reaches the site's server
)

* feat(app): ask a teammate's host to join the team over the tailnet

A host that isn't in a team finds peers whose hello names a team directory and offers
"<person> on <machine> is in a team. Ask to join". The peer's owner approves or denies in
their app; on approval the directory URL and secret travel over the tailnet and the asking
host joins as with directory-set. Asks end on deny, disconnect, a repeat ask, or after five
minutes.

Why: apps/host/src/protocol.ts#type Hello | hello answers any tailnet peer, so team carries only the directory's host, never the secret
Why: apps/host/src/gateway.ts#function asksChanged | only the local app can answer an ask (team-answer is a keychain op), so asks reach local sockets only

* lane: confirm notes for ask to join

* lane: sync ask-to-join memory
@iamnbutler
iamnbutler merged commit 3a09658 into team-onboarding Oct 22, 2026
1 check passed
@iamnbutler
iamnbutler deleted the invite-link branch October 22, 2026 00:06
iamnbutler added a commit that referenced this pull request Oct 22, 2026
… from the app (#553)

* feat(app): offer Get your team started or Join your team on first run

* chore: confirm the app selection note

* fix(app): turn off autocorrect in the team setup fields

* feat(app): deploy the team's Cloudflare services from Get your team started

Ace Helper ships prebuilt directory and channel bundles and deploys them with the
owner's wrangler login, naming them after the tailnet, then joins the directory.
The Set up step shows progress instead of a command to copy. Fly.io still runs
the checkout script.

Why: apps/host/src/team.ts | Ace Helper runs wrangler as plain Bun via BUN_BE_BUN, and deploys the bundles apps/desktop/scripts/helper.ts writes to Resources/services; both layouts must stay in step.

* chore: confirm gateway and helper notes after adding team-deploy

* lane: sync team-onboarding memory

* feat(app): link Deploy to what it creates in the team's Cloudflare account

* feat(app): invite teammates with one link that fills in Join your team (#561)

* feat(app): invite teammates with one link that fills in Join your team

The Invite screen and Settings → Team → Invite… copy an ace://join link carrying the directory
URL and team secret. Pasting it into either join form fills both fields, and the desktop app
registers its channel's scheme so opening the link brings up Join your team, filled in, for the
person to check and join.

Why: apps/desktop/electrobun.config.ts | each build channel registers its own scheme (ace, ace-canary, ace-dev) so a dev or Canary build never answers stable's invite links; apps/app/src/invite.ts reads all of them.
Why: apps/app/src/invite.ts#function parse | an invite link only fills the join form; joining must stay an explicit Check and join, since a link can name any directory.

* lane: confirm notes for invite link

* lane: sync invite-link memory

* feat(app): send invites as an https link to the join page

Chat apps linkify https, not ace://. The link opens ace.githubnext.com/join, which offers the download and hands off to ace://join. Pasting either form still fills Join your team.

Why: apps/app/src/invite.ts#function link | the invite must stay in the URL fragment so the secret never reaches the site's server

* lane: confirm invite parse note

* lane: sync invite-link memory

* feat(app): ask a teammate's host to join the team over the tailnet (#563)

* feat(app): ask a teammate's host to join the team over the tailnet

A host that isn't in a team finds peers whose hello names a team directory and offers
"<person> on <machine> is in a team. Ask to join". The peer's owner approves or denies in
their app; on approval the directory URL and secret travel over the tailnet and the asking
host joins as with directory-set. Asks end on deny, disconnect, a repeat ask, or after five
minutes.

Why: apps/host/src/protocol.ts#type Hello | hello answers any tailnet peer, so team carries only the directory's host, never the secret
Why: apps/host/src/gateway.ts#function asksChanged | only the local app can answer an ask (team-answer is a keychain op), so asks reach local sockets only

* lane: confirm notes for ask to join

* lane: sync ask-to-join memory

* lane: confirm gateway note after merging main
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