Skip to content

Consolidate monorepo - #31

Merged
awindsr merged 6 commits into
mainfrom
consolidate-monorepo
Sep 3, 2026
Merged

awindsr merged 6 commits into
mainfrom
consolidate-monorepo

Conversation

@awindsr

@awindsr awindsr commented Sep 3, 2026

Copy link
Copy Markdown
Member

No description provided.

awindsr 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.
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debrief Error Error Sep 3, 2026 9:47am UTC
debrief-game Error Error Sep 3, 2026 9:47am UTC

@awindsr
awindsr merged commit ad4eb4e into main Sep 3, 2026
1 of 4 checks passed

This branch had an error being deployed

2 failed deployments
Preview – debrief-game — e4c179e4 Deployed Sep 3, 2026 by vercel[bot]
Preview – debrief — e4c179e4 Deployed Sep 3, 2026 by vercel[bot]
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