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>
4.4 KiB
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:
- Workspace home (current) — only good once
Task-wire-workspace-home-dashboardlands. Until then, it's a mockup. - Planner —
[workspaceSlug]/planner, the most "active" view. Lots of tenants will prefer this. - Last visited surface — store
last_visited_pathon the user (or inlocalStorage) 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
- The redirect target for an authenticated user with no specific URL is decided in
apps/web/middleware.ts(if it exists) or via the NextAuthpages.signInand the post-sign-incallbackUrl. - Verify
signIn(provider, { callbackUrl: "/" })lands somewhere sensible. The current top-level page should redirect to the user's first workspace's slug. Checkapps/web/app/page.tsx(the unauth root) andapps/web/app/(app)/page.tsx(the auth root). - The redirect must use
workspace_membersto 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_pathunless 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 (postTask-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 membershipcreatedAtascending (stable across page loads — users always hit the same workspace). - Removed the unused
objects/andimports that were noisy lint warnings. - Filed
Task-onboarding-zero-workspace-flow.mdas the follow-up that elevates the defensive/sign-in?error=no_workspacefallback 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 (becauseresolveWorkspaceaccepts 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_pathcolumn. 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 toworkspacesfor the slug) — no URL-trust. - Users with zero workspaces don't 404. They redirect through
/sign-in?error=no_workspaceas a defensive fallback. The proper onboarding flow is captured inTask-onboarding-zero-workspace-flow.mdas a P2 follow-up; this criterion is gated on that task per the scaffold's allowance.
Links to related Epic / Plan
- Epic:
./Epic-shipping-the-shell.md - Plan:
../Plan-daily-driver-finish.md