Questify

2.22.0

Released

Features

  • With NEXT_PUBLIC_FEATURE_GUILD_INVITES on, a guild owner invites people by email address, including people with no Questify account — which was the gap: the invite box matched an existing account by exact email or display name, so somebody without one could only be typed onto the roster by name and handed a claim code by hand. The roster now lists the invitations still waiting to be accepted, with when each link expires and how many times it was sent, and withdraws them. Each accountless member gets an "Invite by email" control that offers their place to an address: accepting links that place — and everything assigned to it — to their new account. The link lands on a public /guild-invites/<token> page that says who invited them, to what, and to which address, and offers signup or sign-in for anyone who needs an account; joining is a button, never a page view, so a mail scanner joins nobody. When the email cannot be sent the invitation still exists and the owner is shown the link to pass on. With the flag off, nothing changes. (#661)

Technical changes· 4 entries for database, tooling, CI, tests and docs

Database

  • 20260926120000_guild_email_invitations.sql adds guild_invitations, a guild's pending invitations by email address, and guild_invitation_sends, the per-inviter ledger behind the rate limits. create_guild_invitation() is owner-only: it writes a pending row, or re-issues the existing one for that address, and returns a single-use token once — only its SHA-256 digest is stored, and it expires after 14 days. Issuing is limited to 50 tokens per inviter in 24 hours, one send per invitation per 10 minutes, and 100 live invitations per guild; an expired invitation stays pending so it can be sent again but does not use up the cap, and the ledger outlives the rows it counts so no deletion resets anyone's total. An invitation may name an accountless roster row, in which case accepting LINKS that row to the new account rather than creating a second membership, keeping its party seats, assignments and history; one pending invitation per roster place, and re-aiming a place at a corrected address retires the first link. accept_guild_invitation() requires the caller's confirmed account email to equal the invited address, so a forwarded link seats nobody, and it moves an existing pending membership to accepted rather than duplicating it. revoke_guild_invitation() withdraws a pending invitation as a status, not a delete. get_guild_invitation_preview() is callable signed out and reveals the guild name, inviter, invited address, claimed roster name and expiry only for a live token. Every write goes through a SECURITY DEFINER function; the table has a read policy and nothing else, and the ledger is invisible to clients. The account link itself moved into link_guild_member_account(), which redeem_guild_member_claim() was refactored onto, so the claim path and the invitation path cannot drift apart; it is executable by nobody. (#661)

CI/CD

  • CI no longer runs twice on every pull request. ci.yml fired on pushes to feat/**, feature/**, bugfix/** and fix/** as well as on pull requests, so once a branch had a pull request open each commit matched both triggers and started two full runs — every required check listed twice, and twice the runner minutes. The two never collapsed, because the concurrency group keys on github.ref, which is refs/heads/<branch> for the push and refs/pull/<n>/merge for the pull request. The push trigger is now develop and main only, matching codeql.yml and supabase.yml; pull requests keep their run, which is the one that builds the merge commit rather than the branch tip. A branch pushed before its pull request exists now gets no CI — open the pull request as a draft for checks while the work is in progress. (#665)

Documentation

  • docs/notifications.md gains a Guild invitations section covering the four database functions, the shared link_guild_member_account(), what the guild path does differently from the Party Quest path it mirrors, and why the two 50-a-day budgets are separate. /guild-invites/* joins /party-invites/* in robots.txt. (#661)

  • docs/cicd.md records why a topic branch has no push trigger, and its workflow table no longer describes lighthouse.yml as running on Vercel deployment_status — it has audited a build served from the runner since (#491) — and now lists lighthouse-production.yml. (#665)