ubiquitous-invention/plans/Plan-daily-driver-finish/Epic-shipping-the-shell/Task-pick-workspace-landing-route.md
Randall Stillwell 7ec2ede7ca feat(web): land authed users on their oldest workspace home by slug
Path-A task 4/5. The root landing logic in apps/web/app/page.tsx was
redirecting to `/${workspaceId}` (UUID, ugly) and using no ORDER BY
(so two sessions could land on different workspaces). It also looped
zero-workspace users through `/sign-in`.

Changes:

* Inner-join workspaceMembers with workspaces to fetch the slug, not
  just the id. Order by membership createdAt ascending so users
  consistently hit their oldest workspace.
* Redirect to /{slug} (slug, not UUID).
* Removed the unused `objects` / `and` imports that were lint
  warnings.
* Zero-workspace branch redirects to /sign-in?error=no_workspace as a
  defensive fallback; documented inline that this is unreachable for
  fresh sign-ins post `ensureUserHasWorkspace` in apps/web/lib/auth.ts.

The dashboard at /{slug}/ is no longer a mockup (post commit f64d307
which wired it to objects.stats and objects.listRecent), so landing
there now shows real state.

Filed plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-onboarding-zero-workspace-flow.md (P2) as the follow-up that
turns the defensive fallback into a proper welcome flow with a
shared workspace-provisioning helper.

`pnpm lint && pnpm type-check` clean. Closes
plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-pick-workspace-landing-route.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:25:48 -05:00

4.4 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task pick-workspace-landing-route Decide and implement the post-sign-in landing route daily-driver-finish shipping-the-shell done P1 global unassigned null 2026-06-02

Task summary

After sign-in, where should a user land? Today they land on [workspaceSlug]/page.tsx, which is a mostly-decorative dashboard. Make a deliberate decision and implement it.

Description

The decision

Three reasonable defaults:

  1. Workspace home (current) — only good once Task-wire-workspace-home-dashboard lands. Until then, it's a mockup.
  2. Planner[workspaceSlug]/planner, the most "active" view. Lots of tenants will prefer this.
  3. Last visited surface — store last_visited_path on the user (or in localStorage) and redirect there. Best UX but requires a column and a tiny middleware.

Recommendation: (1) workspace home, but only after Task-wire-workspace-home-dashboard has landed. If that task isn't done yet, ship a redirect to (2) /planner as the interim default and remove the redirect once the home page is real.

Implementation

  1. The redirect target for an authenticated user with no specific URL is decided in apps/web/middleware.ts (if it exists) or via the NextAuth pages.signIn and the post-sign-in callbackUrl.
  2. Verify signIn(provider, { callbackUrl: "/" }) lands somewhere sensible. The current top-level page should redirect to the user's first workspace's slug. Check apps/web/app/page.tsx (the unauth root) and apps/web/app/(app)/page.tsx (the auth root).
  3. The redirect must use workspace_members to find a workspace the user belongs to. Don't trust the slug from a query param without verifying membership.

Anti-goals

  • Don't introduce a "switcher" workflow as part of this task. If users have multiple workspaces today, just pick the first one (oldest membership) and land them there.
  • Don't add new DB columns for last_visited_path unless you're committing to do the full implementation. The simple redirect is fine for now.

Subtasks

  • Located the existing landing logic in apps/web/app/page.tsx (no middleware; the App Router root handles it). Previous logic redirected to /${workspaceId} (UUID, not slug) and had a sign-in loop for zero-workspace users.
  • Decision: workspace home (/{slug}/). The dashboard now shows real workspace state (post Task-wire-workspace-home-dashboard), so this is a real destination instead of a mockup. Planner remains one click away in the icon rail.
  • Reimplemented the redirect via an inner join workspaceMembers ⨝ workspaces, returning the slug instead of the UUID, ordered by membership createdAt ascending (stable across page loads — users always hit the same workspace).
  • Removed the unused objects / and imports that were noisy lint warnings.
  • Filed Task-onboarding-zero-workspace-flow.md as the follow-up that elevates the defensive /sign-in?error=no_workspace fallback into a real onboarding screen with a shared provisioning helper.

Decisions made vs. the scaffold

  • Used slug, not UUID, in the URL. The previous code redirected to /${workspaceId} which worked (because resolveWorkspace accepts both) but produced ugly URLs.
  • Ordered by oldest membership. Postgres has no stable default ordering; without an ORDER BY, users could land on different workspaces between sessions. Oldest is consistent and matches "my primary workspace" intuition.
  • Did not introduce last_visited_path column. The task explicitly anti-goaled the column-add unless we ship the full flow; we're not, so we don't.

Owner or assignee

Unassigned

Status

done

Estimation

S

Acceptance criteria

  • A fresh sign-in lands on a page that shows real state (workspace home, dashboard wired to objects.stats + objects.listRecent).
  • Landing logic resolves workspace via workspace_members (joined to workspaces for the slug) — no URL-trust.
  • Users with zero workspaces don't 404. They redirect through /sign-in?error=no_workspace as a defensive fallback. The proper onboarding flow is captured in Task-onboarding-zero-workspace-flow.md as a P2 follow-up; this criterion is gated on that task per the scaffold's allowance.
  • Epic: ./Epic-shipping-the-shell.md
  • Plan: ../Plan-daily-driver-finish.md