Repository navigation
Consolidate monorepo - #31
Merged
Merged
Conversation
added 6 commits
September 3, 2026 14:36
.gitignore used root-anchored patterns (/.next/, /node_modules), so apps/web/.next and apps/game/.next were never ignored and 1,412 build artifacts were committed in 2aca4cc. Switch to **/ patterns and untrack them. Also drop the per-app package-lock.json files; the root lockfile is the only one that should exist.
Reverses the structural half of 2aca4cc, which split one working app into apps/web and apps/game by copying it. Of the 169 source files across the two apps, 114 were byte-identical duplicates and only 11 were genuinely game-specific. - Move the 11 game-only files into the web app (game-actions, the /game routes, the game components, the simulation engine, GameAnalytics and its API route). - Reconcile the 4 drifted files as unions: the admin dashboard now has all three tabs (Users, Ideas, Simulation Game) and the export route handles all three types. Previously each deployment had half. - Point both Growth Lab nav links at the internal /game route. The mobile link was a relative <Link href="/game"> into an app with no such route (a hard 404); the desktop link used NEXT_PUBLIC_GAME_APP_URL, which was declared in no .env.example and fell back to http://localhost:3001. Both NEXT_PUBLIC_GAME_APP_URL and NEXT_PUBLIC_WEB_APP_URL are now unused and gone. - Delete apps/game and flatten apps/web to the repo root. Drop turbo and the npm-style workspaces field: the root scripts called `turbo run` but no turbo.json ever existed, so the root build could not run at all. - Regenerate the lockfile for the single app. Drops apps/game/src/app/page.tsx, a second Growth Lab landing page added in 2aca4cc whose primary CTA linked to /game/1 (neither a valid 6-char code nor a UUID). The functional lobby at /game carries the same hero. Verified: `next build` succeeds and /game, /game/[gameId] and /game/admin now resolve in the same route table as /ideas and /api.
Sessions were being dropped rather than refreshed. Two defects, both of
which threw away rotated auth tokens:
- authMiddleware called updateSession() to refresh the session, then
returned NextResponse.redirect() on four paths. A redirect starts with
empty headers, so the Set-Cookie headers updateSession had written were
silently discarded.
- For protected and auth routes it then built a second Supabase client
whose cookie writer was `setAll() { }` and called getUser() again. That
is a second refresh per request whose tokens were written nowhere.
Supabase rotates refresh tokens, so the discarded token was the valid one
and the browser kept replaying the consumed one. The session did not just
go stale, it became unrecoverable — which is why the logout looked
permanent, and why createGame() reported "not authenticated" and the UI
surfaced it as a failure to create a game.
Now: one client and one getUser() per request, and a redirectTo() helper
that copies the refreshed cookies onto every redirect it builds.
Also:
- Add /game/admin to protectedRoutes and split out adminRoutes, so the
game console is guarded server-side. It sits outside the /admin prefix,
so neither the middleware nor the /admin layout covered it and the page
rendered for anyone.
- Exclude /auth/callback from the matcher; it performs the PKCE exchange
and writes session cookies itself.
- Use maybeSingle() for the app_admins lookup so a non-admin is a null
row rather than a thrown error.
fix(api): stop reporting every admin failure as a permission error
requireAdmin threw bare Errors and the /api/admin/admins handlers wrapped
every catch in a 403, so a duplicate-key violation was indistinguishable
from a genuine authorization failure. requireAdmin now throws the
UnauthorizedError/ForbiddenError types the codebase already defines, and
the handlers use withErrorHandling, so 401, 403, 400 and 500 are
distinct.
Verified: next build succeeds; eslint clean on all changed files.
Adds the per-game visibility control: only published games are visible to
players. Previously every game was world-readable from the moment it was
created — 011_fix_game_visibility granted `using (true)` on sim_games and
sim_teams so join-by-code would work, so draft, in-progress and finished
games were all equally public.
Migration 019:
- sim_games.is_published, defaulting to false so a new game is opened
deliberately. Existing rows are backfilled to true, since they were
already publicly visible and should not be retroactively hidden.
- public.is_app_admin(), a SECURITY DEFINER helper. app_admins lets a user
read only their own row, so a policy querying it directly is what caused
the recursion 017 had to unpick; SECURITY DEFINER breaks the cycle and
the helper is reusable wherever a policy needs an admin check.
- sim_games is readable when published, or by its creator, or by an admin.
sim_teams follows its parent game via EXISTS, which is itself filtered by
the sim_games policy — without it, team names leak from an unpublished
game.
Because the gate is RLS, /game/[gameId] already returns "Game Not Found"
for an unpublished game without any change to the page.
UI and action:
- setGamePublished() in game-actions, gated by requireAdmin and written
through the service-role client, matching the model 017 settled on.
- A Switch on each card in the admin console, labelled in the operator's
terms ("Visible to players" / "Hidden from players") with the state also
shown as an icon. Optimistic, and rolls back if the server rejects.
Deployment (none of this existed):
- vercel.json and a CI workflow running lint and build on PRs. Lint is
non-blocking for now; the codebase carries ~94 pre-existing eslint
errors, and the point of the workflow today is to catch the kind of
broken build that shipped in 2aca4cc.
- .env.example rewritten for the single app. Documents SUPABASE_SECRET_KEY
and NEXT_PUBLIC_APP_URL, both previously undocumented or unexplained,
and drops the two cross-app URL variables that no longer exist.
- supabase/migrations/README.md records the 008 and 011 number collisions
and warns against renumbering without first checking what the live
project has applied.
Also fixes a pre-existing race in the header's admin check: the result is
now guarded by a cancelled flag, so a slow response for a previous user
cannot overwrite the current one.
Verified: next build succeeds; eslint clean on every file touched here.
The README was duplicated verbatim into both apps and never mentioned the simulation. Add Growth Lab to the feature list now that it ships in the same app, and standardise setup on npm — the repo carries one package-lock.json and `bun install` would create a competing lockfile.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.