Skip to content

fix(frontend): make /invite reach discord and stop asking for admin - #1889

Merged
LucasSantana-Dev merged 3 commits into
mainfrom
fix/invite-funnel
Jul 26, 2026
Merged

LucasSantana-Dev merged 3 commits into
mainfrom
fix/invite-funnel

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Jul 26, 2026 •

Copy link
Copy Markdown
Owner

Closes #1888. Found while investigating the Top.gg rejection (#1885); neither of these caused it, but both are live and both are risks for the resubmission.

1. /invite never reached Discord

https://lucky.lucassantana.tech/invite is the canonical invite URL, used by the Top.gg listing body, the README CTA and every tracked campaign URL in .agents/tracking-urls.md.

There was no /invite route. App.tsx defines /, /login and a catch-all <Navigate to='/' replace />, and vercel.json rewrites /:path* to index.html. So the link served the SPA and bounced to the landing page.

Verified before the fix:

$ curl -sI https://lucky.lucassantana.tech/invite
HTTP/2 200
content-type: text/html; charset=utf-8      # landing page, no Location header

Every invite CTA in the funnel was a no-op, and the utm_source attribution the tracking doc is built on never recorded an install. Fixed with a redirects entry in vercel.json (Vercel evaluates redirects before rewrites, so the SPA catch-all is unaffected).

2. The docs invite requested Administrator

Docs.tsx hardcoded permissions=8. That is Administrator.

Landing.tsx deliberately used 3165184 (View Channels, Send Messages, Embed Links, Connect, Speak) with a comment explaining that Administrator is not wanted.

The Top.gg listing states:

Lucky only requests the permissions it actually needs:

  • No admin permission

So we shipped a link contradicting our own public copy, and a reviewer following the docs invite would have hit an Administrator consent screen. Independently, it is a poor default for a bot that sells itself on minimal permissions.

Change

One helper in lib/discord.ts, used by both pages. The drift was only possible because each page rolled its own URL.

Tests

lib/discord.test.ts asserts the URL never requests Administrator, keeps the applications.commands scope (its absence is the shape of #1885), and targets the OAuth endpoint. Verified the guard fails when the permission set is put back to 8:

× never requests Administrator
× requests the minimal permission set
Tests  2 failed | 1017 passed

Frontend suite green: 86 files, 1019 tests. Typecheck clean.


Summary by cubic

Fixes the invite funnel so /invite reaches Discord OAuth and all invite links request minimal permissions (no Administrator). Consolidates the invite URL in one helper and adds tests to prevent regressions.

  • Bug Fixes

    • Added a vercel.json redirect so /invite goes to Discord OAuth (bypasses SPA rewrite and keeps UTM).
    • All invite links now use permissions=3165184 with applications.commands; removed permissions=8 from docs.
  • Refactors

    • Introduced getBotInviteUrl in packages/frontend/src/lib/discord.ts, used by Landing and Docs; always returns a URL. Added packages/frontend/src/lib/discord.test.ts to assert endpoint, scope, and non-admin permissions.

Written for commit 6d8b3ff. Summary will update on new commits.

Review in cubic

Two defects in the invite funnel, both found while investigating the
Top.gg rejection (#1885).

/invite had no route. App.tsx defines `/`, `/login` and a catch-all that
navigates to `/`, and vercel.json rewrites everything else to index.html,
so https://lucky.lucassantana.tech/invite served the SPA and bounced to
the landing page. That URL is what the Top.gg listing body, the README
CTA and every tracked campaign URL point at, so no invite link in the
funnel actually reached Discord and no utm_source ever recorded an
install. Adds a vercel redirect.

Docs.tsx hardcoded `permissions=8`, which is Administrator, while
Landing.tsx deliberately used the minimal 3165184 set. The public
listings state "No admin permission", so we were shipping a link that
contradicted our own copy, and a reviewer following the docs invite would
have seen an Administrator consent screen.

Both pages now build the URL from one helper in lib/discord.ts. The drift
was only possible because each page rolled its own.

Test asserts the URL never requests Administrator, keeps the
applications.commands scope (its absence is the shape of #1885), and
targets the OAuth endpoint. Verified it fails when the permission set is
put back to 8. Frontend suite green: 86 files, 1019 tests.

Closes #1888.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@LucasSantana-Dev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cf23bdb9-f49d-4c32-bf90-82dde3c53240

📥 Commits

Reviewing files that changed from the base of the PR and between f7d38d5 and 6d8b3ff.

📒 Files selected for processing (5)
  • packages/frontend/src/lib/discord.test.ts
  • packages/frontend/src/lib/discord.ts
  • packages/frontend/src/pages/Docs.tsx
  • packages/frontend/src/pages/Landing.tsx
  • vercel.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/invite-funnel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread packages/frontend/src/lib/discord.ts
@github-actions

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@github-actions

github-actions Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Warnings
⚠️

User-facing change without a CHANGELOG.md update. Add a line under ## [Unreleased] if this should appear in release notes. (Or apply the skip-changelog label if this PR does not affect end users.)

Generated by 🚫 dangerJS against 6d8b3ff

@github-actions

Copy link
Copy Markdown

Size Change: -40 B (-0.01%)

Total Size: 497 kB

📦 View Changed
Filename Size Change
packages/frontend/dist/assets/AddStyledRoleForm-C34o7zjw.js 0 B -3.11 kB (removed) 🏆
packages/frontend/dist/assets/AddStyledRoleForm-DiV1wQNM.js 3.11 kB +3.11 kB (new file) 🆕
packages/frontend/dist/assets/Admin-CRBYcvDX.js 0 B -2.3 kB (removed) 🏆
packages/frontend/dist/assets/Admin-DTHGY0C8.js 2.3 kB +2.3 kB (new file) 🆕
packages/frontend/dist/assets/AdminSupport-C6ackCMw.js 0 B -1.59 kB (removed) 🏆
packages/frontend/dist/assets/AdminSupport-DwjSU6l3.js 1.6 kB +1.6 kB (new file) 🆕
packages/frontend/dist/assets/AutoMessages-CGOwJYzH.js 2.65 kB +2.65 kB (new file) 🆕
packages/frontend/dist/assets/AutoMessages-p9Sg0ree.js 0 B -2.65 kB (removed) 🏆
packages/frontend/dist/assets/AutoMod-_gAyZaMc.js 4.19 kB +4.19 kB (new file) 🆕
packages/frontend/dist/assets/AutoMod-D7QIC-IV.js 0 B -4.19 kB (removed) 🏆
packages/frontend/dist/assets/badge-BhnWhbGk.js 503 B +503 B (new file) 🆕
packages/frontend/dist/assets/badge-DvThFFX7.js 0 B -502 B (removed) 🏆
packages/frontend/dist/assets/BatchJobs-B0bUM02D.js 0 B -3.72 kB (removed) 🏆
packages/frontend/dist/assets/BatchJobs-HZyy4094.js 3.72 kB +3.72 kB (new file) 🆕
packages/frontend/dist/assets/Card-BYrSPonZ.js 0 B -507 B (removed) 🏆
packages/frontend/dist/assets/Card-LIAHwOQg.js 509 B +509 B (new file) 🆕
packages/frontend/dist/assets/Changelog-87OnmZD8.js 0 B -59.1 kB (removed) 🏆
packages/frontend/dist/assets/Changelog-Bt5VrV74.js 59.1 kB +59.1 kB (new file) 🆕
packages/frontend/dist/assets/CommandsConfig-9ZFKwNAp.js 0 B -1.5 kB (removed) 🏆
packages/frontend/dist/assets/CommandsConfig-BsBqkite.js 1.5 kB +1.5 kB (new file) 🆕
packages/frontend/dist/assets/Config-BFZhOsTK.js 1.91 kB +1.91 kB (new file) 🆕
packages/frontend/dist/assets/Config-c43AJQwx.js 0 B -1.91 kB (removed) 🏆
packages/frontend/dist/assets/CustomCommands-D6w2ykL8.js 2.12 kB +2.12 kB (new file) 🆕
packages/frontend/dist/assets/CustomCommands-sAdTeAL5.js 0 B -2.12 kB (removed) 🏆
packages/frontend/dist/assets/DashboardOverview-BgMVFUVT.js 0 B -3.95 kB (removed) 🏆
packages/frontend/dist/assets/DashboardOverview-CwENGyq3.js 3.95 kB +3.95 kB (new file) 🆕
packages/frontend/dist/assets/dialog-BGmWo0-0.js 958 B +958 B (new file) 🆕
packages/frontend/dist/assets/dialog-DQuLMaVv.js 0 B -959 B (removed) 🏆
packages/frontend/dist/assets/Docs-BEaelIuy.js 0 B -17.6 kB (removed) 🏆
packages/frontend/dist/assets/Docs-JKWGxou2.js 17.5 kB +17.5 kB (new file) 🆕
packages/frontend/dist/assets/DocsShell-BcX6BnPf.js 0 B -1.42 kB (removed) 🏆
packages/frontend/dist/assets/DocsShell-CKDch1gw.js 1.42 kB +1.42 kB (new file) 🆕
packages/frontend/dist/assets/EmbedBuilder-BbSkuS3O.js 0 B -3.28 kB (removed) 🏆
packages/frontend/dist/assets/EmbedBuilder-BDcg1b2J.js 3.28 kB +3.28 kB (new file) 🆕
packages/frontend/dist/assets/Features-CcTKBI8w.js 758 B +758 B (new file) 🆕
packages/frontend/dist/assets/Features-FLOrw0Qd.js 0 B -756 B (removed) 🏆
packages/frontend/dist/assets/GuildAutomation-3_rjfwxC.js 2.88 kB +2.88 kB (new file) 🆕
packages/frontend/dist/assets/GuildAutomation-Cf19LwO4.js 0 B -2.89 kB (removed) 🏆
packages/frontend/dist/assets/index-BDLYM_Uy.js 71.2 kB +71.2 kB (new file) 🆕
packages/frontend/dist/assets/index-BXyoaeJX.js 0 B -71.1 kB (removed) 🏆
packages/frontend/dist/assets/input-fcr1E_rI.js 465 B +465 B (new file) 🆕
packages/frontend/dist/assets/input-J0bJ_99V.js 0 B -464 B (removed) 🏆
packages/frontend/dist/assets/label-C-9VJOYq.js 479 B +479 B (new file) 🆕
packages/frontend/dist/assets/label-jhYzUnNq.js 0 B -477 B (removed) 🏆
packages/frontend/dist/assets/Landing-BUEv8u08.js 0 B -5.2 kB (removed) 🏆
packages/frontend/dist/assets/Landing-DW9R7_nS.js 5.11 kB +5.11 kB (new file) 🆕
packages/frontend/dist/assets/LastFm-CEHQ8eAO.js 0 B -1.74 kB (removed) 🏆
packages/frontend/dist/assets/LastFm-X6OV8Nxy.js 1.74 kB +1.74 kB (new file) 🆕
packages/frontend/dist/assets/Levels-BylqG2Op.js 2.27 kB +2.27 kB (new file) 🆕
packages/frontend/dist/assets/Levels-CMt9bekz.js 0 B -2.27 kB (removed) 🏆
packages/frontend/dist/assets/Login-DBV2WhnQ.js 0 B -2.5 kB (removed) 🏆
packages/frontend/dist/assets/Login-DI1dgDtm.js 2.49 kB +2.49 kB (new file) 🆕
packages/frontend/dist/assets/Lyrics-DgNELcTv.js 1.34 kB +1.34 kB (new file) 🆕
packages/frontend/dist/assets/Lyrics-DoxxeA2n.js 0 B -1.34 kB (removed) 🏆
packages/frontend/dist/assets/Moderation-4ZxeXAQc.js 0 B -3.78 kB (removed) 🏆
packages/frontend/dist/assets/Moderation-tqzqJrfG.js 3.77 kB +3.77 kB (new file) 🆕
packages/frontend/dist/assets/Music-DLoA5Ixw.js 0 B -5.97 kB (removed) 🏆
packages/frontend/dist/assets/Music-DYF2BdDL.js 5.97 kB +5.97 kB (new file) 🆕
packages/frontend/dist/assets/MusicConfig-Bwvh8gzd.js 0 B -1.68 kB (removed) 🏆
packages/frontend/dist/assets/MusicConfig-fmvB_wIK.js 1.68 kB +1.68 kB (new file) 🆕
packages/frontend/dist/assets/PreferredArtists-7zaEj3AU.js 0 B -3.72 kB (removed) 🏆
packages/frontend/dist/assets/PreferredArtists-zXKMfGkK.js 3.72 kB +3.72 kB (new file) 🆕
packages/frontend/dist/assets/PrivacyPolicy-VNvNdVa_.js 0 B -1.77 kB (removed) 🏆
packages/frontend/dist/assets/PrivacyPolicy-YpWq8EHG.js 1.77 kB +1.77 kB (new file) 🆕
packages/frontend/dist/assets/ReactionRoles-qYSeeZ0J.js 7.04 kB +7.04 kB (new file) 🆕
packages/frontend/dist/assets/ReactionRoles-SnfNEjKE.js 0 B -7.04 kB (removed) 🏆
packages/frontend/dist/assets/RoleGroups-i-BhEgKp.js 0 B -2.24 kB (removed) 🏆
packages/frontend/dist/assets/RoleGroups-ICgei5gP.js 2.24 kB +2.24 kB (new file) 🆕
packages/frontend/dist/assets/Roles-BF8pcxZQ.js 0 B -3.34 kB (removed) 🏆
packages/frontend/dist/assets/Roles-w_244sIi.js 3.34 kB +3.34 kB (new file) 🆕
packages/frontend/dist/assets/SectionHeader-Coa0hOTZ.js 895 B +895 B (new file) 🆕
packages/frontend/dist/assets/SectionHeader-nNKuFOu6.js 0 B -895 B (removed) 🏆
packages/frontend/dist/assets/select-C_isWSLl.js 1.23 kB +1.23 kB (new file) 🆕
packages/frontend/dist/assets/select-p9GlXJht.js 0 B -1.23 kB (removed) 🏆
packages/frontend/dist/assets/ServerLogs-BE9nsiC7.js 0 B -3.04 kB (removed) 🏆
packages/frontend/dist/assets/ServerLogs-BYdf7IbC.js 3.04 kB +3.04 kB (new file) 🆕
packages/frontend/dist/assets/ServerSettings-BTilw0CS.js 3.98 kB +3.98 kB (new file) 🆕
packages/frontend/dist/assets/ServerSettings-DV3Z89iu.js 0 B -3.99 kB (removed) 🏆
packages/frontend/dist/assets/ServersPage-67osdYHV.js 0 B -3.04 kB (removed) 🏆
packages/frontend/dist/assets/ServersPage-C6-h_e5P.js 3.03 kB +3.03 kB (new file) 🆕
packages/frontend/dist/assets/Skeleton-DeQ9K8XT.js 0 B -234 B (removed) 🏆
packages/frontend/dist/assets/Skeleton-RD9L55Dy.js 235 B +235 B (new file) 🆕
packages/frontend/dist/assets/Spotify-CXq0K6dg.js 0 B -1.75 kB (removed) 🏆
packages/frontend/dist/assets/Spotify-DmmwX1HW.js 1.75 kB +1.75 kB (new file) 🆕
packages/frontend/dist/assets/Starboard-2bFf9Lte.js 1.82 kB +1.82 kB (new file) 🆕
packages/frontend/dist/assets/Starboard-BuOLyEXz.js 0 B -1.82 kB (removed) 🏆
packages/frontend/dist/assets/StatTile-ClESCJKl.js 0 B -638 B (removed) 🏆
packages/frontend/dist/assets/StatTile-jdsJFTrV.js 640 B +640 B (new file) 🆕
packages/frontend/dist/assets/Support-BZchhdhp.js 1.56 kB +1.56 kB (new file) 🆕
packages/frontend/dist/assets/Support-CcZbtaUi.js 0 B -1.55 kB (removed) 🏆
packages/frontend/dist/assets/switch-C3nrkrU0.js 0 B -541 B (removed) 🏆
packages/frontend/dist/assets/switch-Db7o1pbf.js 543 B +543 B (new file) 🆕
packages/frontend/dist/assets/TermsOfService-BBcQ1nRx.js 0 B -1.59 kB (removed) 🏆
packages/frontend/dist/assets/TermsOfService-wIar5Rn0.js 1.59 kB +1.59 kB (new file) 🆕
packages/frontend/dist/assets/TrackHistory-BHwjZruh.js 2.31 kB +2.31 kB (new file) 🆕
packages/frontend/dist/assets/TrackHistory-og_WYLzE.js 0 B -2.31 kB (removed) 🏆
packages/frontend/dist/assets/TwitchNotifications-CnCb727i.js 2.43 kB +2.43 kB (new file) 🆕
packages/frontend/dist/assets/TwitchNotifications-nd8OwWc9.js 0 B -2.43 kB (removed) 🏆
packages/frontend/dist/assets/useActiveHeading-9X5pQW2X.js 1.36 kB +1.36 kB (new file) 🆕
packages/frontend/dist/assets/useActiveHeading-nlVHdaHx.js 0 B -1.35 kB (removed) 🏆
packages/frontend/dist/assets/useFeatures-Cx0fOde7.js 0 B -2.06 kB (removed) 🏆
packages/frontend/dist/assets/useFeatures-De9yWbwR.js 2.06 kB +2.06 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size
packages/frontend/dist/assets/api-CtLilru1.js 4 kB
packages/frontend/dist/assets/index-DY6JagVQ.css 17.6 kB
packages/frontend/dist/assets/legalNav-B6k3CWsW.js 274 B
packages/frontend/dist/assets/rolldown-runtime-Cyuzqnbw.js 471 B
packages/frontend/dist/assets/routeMeta-BZjtwMbs.js 595 B
packages/frontend/dist/assets/sentry-DhXOA89y.js 3.76 kB
packages/frontend/dist/assets/usePageMetadata-DTv-6eVb.js 327 B
packages/frontend/dist/assets/vendor-forms-C-bof8GF.js 25.9 kB
packages/frontend/dist/assets/vendor-radix-qkfmDH9H.js 39.9 kB
packages/frontend/dist/assets/vendor-react-B7C34xnu.js 55.7 kB
packages/frontend/dist/assets/vendor-state-Kvbn3gqw.js 25.2 kB
packages/frontend/dist/assets/vendor-ui-BBN61NBD.js 66.2 kB

compressed-size-action

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/frontend/src/lib/discord.ts Outdated
DEFAULT_DISCORD_CLIENT_ID is a non-empty literal, so the `||` fallback
always yields a client id and `if (!clientId) return ''` could never run.
Raised by cubic on #1889.

The helper's real contract is that it always returns a URL, so say that
rather than keeping a guard that reads like a configuration path but is
not one.

Frontend suite green: 86 files, 1019 tests.
@LucasSantana-Dev

Copy link
Copy Markdown
Owner Author

Both threads addressed.

cubic P3 — unreachable disabled-invite path. Correct, fixed in ceb45c08. DEFAULT_DISCORD_CLIENT_ID is a non-empty literal, so the || fallback always yields an id and if (!clientId) return '' could never run. It read like a configuration path without being one. The helper's real contract is that it always returns a URL, so it now says that. (Carried over from the original Landing.tsx implementation rather than introduced here, but no reason to keep it.)

Trivy — "Secret Discord client ID" (MEDIUM). False positive. A Discord client id is the application id, and it is public by design: it is embedded in every OAuth invite URL, it is visible in the Top.gg listing, and it is already published in Docs.tsx and the README invite links. It is not a credential and there is nothing to rotate.

The value that must never ship is CLIENT_SECRET, which lives in the backend env and is not in this diff. The constant is annotated in place:

// Public Discord Application ID (a.k.a. client_id). Safe to ship: it appears in
// every OAuth invite link and is not a secret. Used as the default so the CTA
// works out of the box; override via VITE_DISCORD_CLIENT_ID for a fork.

Worth noting the rule is finding this only because the constant was centralised by this PR. The same literal was already hardcoded in Docs.tsx and inlined in Landing.tsx before, so this is not new exposure, it is the same public value in one place instead of two.

Frontend suite green: 86 files, 1019 tests.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Auto-approved: Fixes two bugs: makes /invite redirect to Discord OAuth and removes Administrator permission from docs invite, consolidating URL generation. Changes are bounded, well-tested, and reduce risk.

Re-trigger cubic

@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit c322834 into main Jul 26, 2026
44 checks passed
@LucasSantana-Dev
LucasSantana-Dev deleted the fix/invite-funnel branch July 26, 2026 21:29
LucasSantana-Dev added a commit that referenced this pull request Jul 26, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>2.37.1</summary>

##
[2.37.1](v2.37.0...v2.37.1)
(2026-07-26)


### Bug Fixes

* **bot:** register slash commands globally, not per cached guild
([#1887](#1887))
([f7d38d5](f7d38d5))
* **ci:** make the security gate passable and clear production
advisories
([#1876](#1876))
([ca410d2](ca410d2))
* **frontend:** make /invite reach discord and stop asking for admin
([#1889](#1889))
([c322834](c322834))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved bot slash command registration.
  * Updated the CI and security gate configuration.
* Improved the frontend invite flow so it reaches Discord without
requesting administrator permissions.

* **Chores**
  * Released version 2.37.1.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
LucasSantana-Dev added a commit that referenced this pull request Jul 27, 2026
cubic was right on #1893: redirecting at the edge fixes the click but
drops the attribution. backend/src/routes/invite.ts already existed and
logs the utm_* parameters before redirecting, but nginx only proxies
/api, so /invite never reached it and fell through to the SPA catch-all.

nginx now proxies the exact path to the backend instead of returning a
redirect itself, so tracked links keep emitting `[invite] click`.

Chasing that turned up a third permission value. The codebase shipped:

  backend invite.ts       36970496  Manage Messages, Connect, Speak
  frontend discord.ts      3165184  View Channels, Send Messages,
                                    Embed Links, Connect, Speak
  docs page (fixed #1889)         8  Administrator

The backend one is the worst of the three: it asks for Manage Messages,
which is message deletion, while omitting View Channels and Send
Messages, so it is over-scoped and non-functional at the same time. It is
also the value that every tracked link resolved to.

Single source of truth is now BOT_INVITE_PERMISSIONS in shared, consumed
by the backend redirect and the frontend helper. Test pins the bitfield:
no Administrator, no Manage Messages, exactly the five needed bits, and
the applications.commands scope whose absence caused #1885.

vercel.json points at the backend too, so preview deploys log the same.

nginx -t clean against nginxinc/nginx-unprivileged:1.31-alpine. Suites
green: shared 1398, frontend 1019, backend 1350.
LucasSantana-Dev added a commit that referenced this pull request Jul 27, 2026
…ons (#1893)

nginx only proxied /api, so the canonical /invite URL never reached
backend/src/routes/invite.ts and fell through to the SPA catch-all, which
bounces unknown paths to the landing page. Every invite click from the
Top.gg listing, the README CTA and every tracked campaign link was
swallowed, and no utm_source ever recorded an install.

nginx now proxies the exact path to the backend rather than redirecting
at the edge, so the existing [invite] click attribution keeps working.
Raised by cubic on the first version of this PR, which redirected at the
edge and would have dropped it.

That surfaced a third permission value: the backend redirect shipped
36970496 (Manage Messages, Connect, Speak), over-scoped on message
deletion and missing View Channels / Send Messages, while the frontend
used 3165184 and the docs page had shipped 8 (Administrator) until #1889.
Single source of truth is now BOT_INVITE_PERMISSIONS in shared, with a
test pinning the bitfield.

Closes the production half of #1888.
LucasSantana-Dev added a commit that referenced this pull request Jul 27, 2026
I fixed this twice in the wrong layer. lucky.lucassantana.tech is served
by Cloudflare Pages (project lucky-webapp, deploy-frontend-cf.yml), not
by Vercel and not by the homelab nginx behind the tunnel. So neither the
vercel.json redirect (#1889) nor the nginx location block (#1893) applied
to the public site, and /invite kept returning 200 with the SPA.

What gave it away: the live response carries a CSP allowing
static.cloudflareinsights.com, which appears only in
packages/frontend/public/_headers. The nginx config serves a different
CSP, so the request was never reaching it.

Meanwhile lucky-api.lucassantana.tech DOES go through the tunnel, and
already returns the correct 302 with permissions=3165184, so the backend
handler and the nginx work from #1893 are both fine and stay.

_redirects rules are evaluated top to bottom, so /invite is placed above
the SPA catch-all, which is what was swallowing it. Points at the backend
rather than Discord directly so the utm_* attribution logging still runs.

Verified the file lands in packages/frontend/dist after a build, which is
the directory `wrangler pages deploy` uploads.
LucasSantana-Dev added a commit that referenced this pull request Jul 27, 2026
lucky.lucassantana.tech is served by Cloudflare Pages (project
lucky-webapp), not by Vercel and not by the homelab nginx behind the
tunnel. So neither the vercel.json redirect (#1889) nor the nginx
location block (#1893) applied to the public site, and /invite kept
returning 200 with the SPA, whose catch-all bounces to the landing page.

_redirects rules are evaluated top to bottom, so /invite now sits above
the SPA catch-all that was swallowing it. It points at the backend rather
than Discord directly, so the utm_* attribution logging still runs.

The nginx block and shared BOT_INVITE_PERMISSIONS from #1893 stay: they
are what make lucky-api.lucassantana.tech/invite return the correct 302.

Closes #1888.
LucasSantana-Dev added a commit that referenced this pull request Sep 8, 2026
## Summary
Nothing in the repo said which of the three independent hosting layers
(Cloudflare Pages, Cloudflare Tunnel + homelab nginx, legacy Vercel
preview) serves which host, or which config file governs each. Fixing
the /invite redirect took three PRs (#1889, #1893, #1895) before landing
in the right file.

- Added a "Deployment & hosting" section to docs/ARCHITECTURE.md: the
host/layer/config table, the CSP trick for identifying which layer
answered a request, and the nginx.conf/_redirects gotchas that cost time
before.
- Added a one-line pointer to that section at the top of nginx.conf and
_redirects.
- vercel.json and _headers intentionally left untouched: vercel.json is
strict JSON with no safe comment syntax, and _headers' Cloudflare Pages
comment support isn't proven in this repo the way _redirects' is (its
existing comment block already works in production - _headers has none
to point to as precedent). Not worth guessing on a live config file for
a one-line pointer when docs/ARCHITECTURE.md already names both files.

## Test plan
- [x] Read-only doc/comment change, no code paths touched
- [x] nginx.conf comment uses `#`, the format nginx already uses
throughout this file
- [x] _redirects comment extends the file's own pre-existing `#` comment
block, proven safe since it's already live in production

Closes #1924

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Documents which of the three hosting layers (Cloudflare Pages,
Cloudflare Tunnel + homelab nginx, legacy Vercel preview) serves which
host and which config file governs each, so fixes like `/invite` land in
the right file instead of taking three PRs.

- Adds a Deployment & hosting section to `docs/ARCHITECTURE.md` with the
host/layer/config table, the CSP trick for identifying which layer
answered, the `nginx.conf` `/api`-proxying rule plus its `/invite`,
`/webhook/`, `/webhooks/` exceptions, and the `_redirects` top-to-bottom
gotcha.
- Adds pointers to that section at the top of `nginx.conf` and
`_redirects`.
- Read-only change; leaves `vercel.json` and `_headers` untouched.
Closes #1924.

<sup>Written for commit 4b401a0.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/LucasSantana-Dev/Lucky/pull/2253?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
This was referenced Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(frontend): /invite link is dead and the docs invite requests Administrator

2 participants