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
72 changes: 48 additions & 24 deletions CHANGELOG.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,53 @@
"commitConvention": "conventional",
"maintainers": ["grokify", "johncwang@gmail.com"],
"releases": [
{
"version": "v0.5.0",
"date": "2026-10-26",
"highlights": [
{
"description": "`@plexusone/invites` 0.1.0: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people",
"commit": "bd2c35a"
}
],
"added": [
{
"description": "`createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`; requests carry the browser's cookies",
"commit": "597b1d1"
},
{
"description": "`InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke",
"commit": "bd2c35a"
},
{
"description": "`InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones",
"commit": "39ff502"
}
],
"tests": [
{
"description": "Cover the invite API client and both pages",
"commit": "36d10c1"
},
{
"description": "Cover a new link surviving a failed reload, the copy note following the code, removed waitlist addresses leaving the selection, and requests sending cookies",
"commit": "6ceeb55"
}
],
"documentation": [
{
"description": "`@plexusone/invites` package page",
"commit": "68d625f"
},
{
"description": "Doc comments on the package's components and helpers",
"commit": "1685338"
},
{
"description": "Adding invite pages guide, README section, and v0.5.0 release notes"
}
]
},
{
"version": "v0.4.0",
"date": "2026-10-20",
Expand Down Expand Up @@ -483,28 +530,5 @@
}
]
}
],
"unreleased": {
"highlights": [
{
"description": "`@plexusone/invites`: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people"
}
],
"added": [
{
"description": "`createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`"
},
{
"description": "`InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke"
},
{
"description": "`InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones"
}
],
"documentation": [
{
"description": "`@plexusone/invites` package page"
}
]
}
]
}
22 changes: 16 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,28 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro

## [Unreleased]

## [v0.5.0] - 2026-10-26

### Highlights

- `@plexusone/invites`: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people
- `@plexusone/invites` 0.1.0: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people

### Added

- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`
- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke
- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones
- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`; requests carry the browser's cookies ([`597b1d1`](https://github.com/plexusone/systemforge-web/commit/597b1d1))
- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke ([`bd2c35a`](https://github.com/plexusone/systemforge-web/commit/bd2c35a))
- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones ([`39ff502`](https://github.com/plexusone/systemforge-web/commit/39ff502))

### Documentation

- `@plexusone/invites` package page
- `@plexusone/invites` package page ([`68d625f`](https://github.com/plexusone/systemforge-web/commit/68d625f))
- Doc comments on the package's components and helpers ([`1685338`](https://github.com/plexusone/systemforge-web/commit/1685338))
- Adding invite pages guide, README section, and v0.5.0 release notes

### Tests

- Cover the invite API client and both pages ([`36d10c1`](https://github.com/plexusone/systemforge-web/commit/36d10c1))
- Cover a new link surviving a failed reload, the copy note following the code, removed waitlist addresses leaving the selection, and requests sending cookies ([`6ceeb55`](https://github.com/plexusone/systemforge-web/commit/6ceeb55))

## [v0.4.0] - 2026-10-20

Expand Down Expand Up @@ -246,7 +255,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro

- Shared pnpm workflows from grokify/.github ([`bb07c1d`](https://github.com/plexusone/systemforge-web/commit/bb07c1d))

[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...HEAD
[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.5.0...HEAD
[v0.5.0]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...v0.5.0
[v0.4.0]: https://github.com/plexusone/systemforge-web/compare/v0.3.1...v0.4.0
[v0.3.1]: https://github.com/plexusone/systemforge-web/compare/v0.3.0...v0.3.1
[v0.3.0]: https://github.com/plexusone/systemforge-web/compare/v0.2.3...v0.3.0
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ function App() {
}
```

## Invite-gated sign-up

`@plexusone/invites` adds the pages for launching invite-only with
SystemForge: operators issue invite codes and email invites and work through
the waitlist, and members invite people from their allowance.

```tsx
import { createInvitesApi, InviteAdminPage, InvitePeoplePage } from '@plexusone/invites';

const invites = createInvitesApi('/invites/api');

<Route path="/invite" element={<InvitePeoplePage api={invites} />} />
<Route path="/admin/invites" element={<InviteAdminPage api={invites} />} />
```

See [Adding invite pages](docs/guides/invite-gated-signup.md).

## Telemetry with ProductGraph

Track user behavior, journeys, and performance with ProductGraph integration:
Expand Down
22 changes: 16 additions & 6 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,28 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro

## [Unreleased]

## [v0.5.0] - 2026-10-26

### Highlights

- `@plexusone/invites`: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people
- `@plexusone/invites` 0.1.0: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people

### Added

- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`
- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke
- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones
- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`; requests carry the browser's cookies ([`597b1d1`](https://github.com/plexusone/systemforge-web/commit/597b1d1))
- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke ([`bd2c35a`](https://github.com/plexusone/systemforge-web/commit/bd2c35a))
- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones ([`39ff502`](https://github.com/plexusone/systemforge-web/commit/39ff502))

### Documentation

- `@plexusone/invites` package page
- `@plexusone/invites` package page ([`68d625f`](https://github.com/plexusone/systemforge-web/commit/68d625f))
- Doc comments on the package's components and helpers ([`1685338`](https://github.com/plexusone/systemforge-web/commit/1685338))
- Adding invite pages guide, README section, and v0.5.0 release notes

### Tests

- Cover the invite API client and both pages ([`36d10c1`](https://github.com/plexusone/systemforge-web/commit/36d10c1))
- Cover a new link surviving a failed reload, the copy note following the code, removed waitlist addresses leaving the selection, and requests sending cookies ([`6ceeb55`](https://github.com/plexusone/systemforge-web/commit/6ceeb55))

## [v0.4.0] - 2026-10-20

Expand Down Expand Up @@ -246,7 +255,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro

- Shared pnpm workflows from grokify/.github ([`bb07c1d`](https://github.com/plexusone/systemforge-web/commit/bb07c1d))

[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...HEAD
[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.5.0...HEAD
[v0.5.0]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...v0.5.0
[v0.4.0]: https://github.com/plexusone/systemforge-web/compare/v0.3.1...v0.4.0
[v0.3.1]: https://github.com/plexusone/systemforge-web/compare/v0.3.0...v0.3.1
[v0.3.0]: https://github.com/plexusone/systemforge-web/compare/v0.2.3...v0.3.0
Expand Down
115 changes: 115 additions & 0 deletions docs/guides/invite-gated-signup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Adding invite pages

An application that launches invite-only, with SystemForge's invite-gated
sign-up, needs two pages: one for operators to issue invites and work through
the waitlist, and one for members to invite people from their allowance.
`@plexusone/invites` provides both. This guide wires them into an
application; SystemForge's
[invite-gated sign-up guide](https://plexusone.github.io/systemforge/guides/invite-gated-signup/)
covers turning it on in SystemAuth and what an invited person sees.

## 1. Choose where the invite API runs

The pages call SystemForge's invite API with the browser's cookies, so the
API must accept the session of the person using the page. There are two
places to serve it:

| Where | Base URL | Use when |
| ---------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Behind the application's own session** | A path on the application, such as `/invites/api` | The application embeds SystemAuth and has its own sign-in. The pages stay same-origin, so cookies just work |
| **On the SystemAuth host** | `https://auth.example.com/signup-invites/api` | The person is signed in to SystemAuth itself (`Options.SignupAPI`) |

Serving the API behind the application's session is the usual choice. In
the host binary, `Service.SignupAPI` takes a function that names the person
signed in to the application:

```go
api, err := svc.SignupAPI(func(r *http.Request) (uuid.UUID, error) {
return mySession.PrincipalID(r)
})
mux.Handle("/invites/api/", http.StripPrefix("/invites/api", api))
```

On the SystemAuth host, the page and the API are on different origins: the
API must allow credentialed requests from the application's origin, and the
SystemAuth session cookie must reach it.

## 2. Install and add the styles

```bash
pnpm add @plexusone/invites @plexusone/ui
```

The pages use Tailwind classes with `@plexusone/ui`'s theme names. Set up
Tailwind as in [@plexusone/ui](../packages/ui.md), then add the package's
built files to Tailwind's sources so their classes are generated:

```css
@source "../node_modules/@plexusone/invites/dist";
```

## 3. Route the pages

Create one client and route both pages where they belong in the
application. They are plain components, not an `AppModule`, because invites
are platform-wide rather than organization-scoped.

```tsx
import { createInvitesApi, InviteAdminPage, InvitePeoplePage } from '@plexusone/invites';

const invites = createInvitesApi('/invites/api');

<Route path="/invite" element={<InvitePeoplePage api={invites} />} />
<Route path="/admin/invites" element={<InviteAdminPage api={invites} />} />
```

`InviteAdminPage` is for platform administrators. Anyone else who reaches it
sees the API's refusal, so the route needs no guard of its own, but the menu
should not offer it to them.

## 4. Show the menu links to the right people

`settings()` says whether the person is an administrator, and `myInvites()`
gives their allowance. Offer **Invites** to administrators, and **Invite
people** to members who have an allowance:

```tsx
const [links, setLinks] = useState<{ admin: boolean; invite: boolean }>();

useEffect(() => {
void Promise.all([invites.settings(), invites.myInvites()]).then(([s, mine]) =>
setLinks({ admin: s.is_admin, invite: mine.allowance > 0 })
);
}, []);
```

A member with no allowance can still open `/invite`; the page tells them they
have no invites to give yet.

## 5. Point the sign-in page at the waitlist

A refused sign-up is not told why. Link the application's sign-in page to
SystemAuth's waitlist, `https://auth.example.com/signup/waitlist`, so a
person without an invite has somewhere to go, and link it from any public
page that announces the launch.

## What the pages do

- **Invite people** shows how many people the member can still invite,
creates an invite link and shows it once with a copy button, and revokes a
link nobody has used, which gives the slot back.
- **Invites** has a section each for invite codes, email invites, the
waitlist, member allowances, and the accounts that joined. New codes and
links are shown once, because only a hash is stored; destructive actions
are confirmed inline.

See [@plexusone/invites](../packages/invites.md) for each page's sections
and props, and the API client's methods.

## Testing

The pages fetch with the global `fetch`, so a test can stub it and answer
each route with JSON, as the package's own tests do. Answer `GET /settings`
and `GET /me/invites` for the member page, and the `/admin/...` routes for
the operator page. Use `vi.stubGlobal('fetch', ...)` in Vitest, and restore
it after each test.
2 changes: 2 additions & 0 deletions docs/packages/invites.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Pages for SystemForge's invite-gated sign-up: an operator page to manage
invite codes, email invites, the waitlist and member allowances, and a member
page to invite people. Both call SystemForge's invite API (`signupapi`); see
SystemForge's [invite-gated sign-up guide](https://plexusone.github.io/systemforge/guides/invite-gated-signup/).
[Adding invite pages](../guides/invite-gated-signup.md) walks through wiring
them into an application.

```bash
pnpm add @plexusone/invites @plexusone/ui
Expand Down
51 changes: 51 additions & 0 deletions docs/releases/v0.5.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# v0.5.0 Release Notes

**Release Date:** 2026-10-26

Adds `@plexusone/invites` at 0.1.0. The other packages are unchanged.

## Highlights

- **Pages for launching invite-only.** `@plexusone/invites` gives an
application the two pages SystemForge's invite-gated sign-up needs.
`InviteAdminPage` lets platform administrators create invite codes with a
use limit and expiry, invite people by email address, invite people from
the waitlist, set how many people each member may invite, and see who
joined with which invite. `InvitePeoplePage` lets a member create invite
links from their allowance and revoke ones nobody has used. See
[@plexusone/invites](../packages/invites.md).
- **Shown once.** SystemForge stores only a hash of each code, so a new code
or link is shown once, with a copy button, and the page says so.
- **A typed client.** `createInvitesApi` covers every route of SystemForge's
invite API, with snake_case wire types as the API sends them and
`ApiError` carrying the HTTP status and the API's message.
- **A guide.** [Adding invite pages](../guides/invite-gated-signup.md) covers
where to serve the invite API, styles, routes, which menu links to show to
whom, and testing.

## Requirements

- `@plexusone/invites` needs `@plexusone/ui` and React 19.
- systemforge v0.22.0 or later serves the invite API (`signupapi`), behind
the application's own session with `Service.SignupAPI` or on the SystemAuth
host with `Options.SignupAPI`.

## Installation

```bash
pnpm add @plexusone/invites @plexusone/ui
```

Add the package's built files to Tailwind's sources:

```css
@source "../node_modules/@plexusone/invites/dist";
```

## Upgrading

Nothing to change: no existing package moved.

## Not in this release

- Nothing is emailed. Operators and members share invite links themselves.
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ nav:
- Quick Start: getting-started/quickstart.md
- Guides:
- Writing an Application: guides/writing-an-application.md
- Adding Invite Pages: guides/invite-gated-signup.md
- Releasing: guides/releasing.md
- Packages:
- Overview: packages/index.md
Expand Down Expand Up @@ -86,6 +87,7 @@ nav:
- Tasks: specs/initiatives/invention/productgraph/TASKS.md
- Releases:
- Changelog: changelog.md
- v0.5.0: releases/v0.5.0.md
- v0.4.0: releases/v0.4.0.md
- v0.3.1: releases/v0.3.1.md
- v0.3.0: releases/v0.3.0.md
Expand Down
Loading