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>
3.2 KiB
3.2 KiB
| kind | slug | title | plan_slug | epic_slug | status | priority | tenant_id | owner | cursor_todo_id | updated_at |
|---|---|---|---|---|---|---|---|---|---|---|
| task | onboarding-zero-workspace-flow | Build a proper onboarding flow for users with zero workspaces | daily-driver-finish | shipping-the-shell | ready | P2 | global | unassigned | null | 2026-06-02 |
Task summary
When a signed-in user has no workspace membership, apps/web/app/page.tsx currently bounces them through /sign-in?error=no_workspace. This is a defensive fallback, not a user experience. Build a proper onboarding flow.
Description
Why this exists
apps/web/lib/auth.ts's ensureUserHasWorkspace provisions a personal workspace on first sign-in, which means the zero-workspace state should be effectively unreachable for new sign-ins. However:
- Existing JWTs minted before that provisioning logic was added do not trigger
ensureUserHasWorkspaceon refresh; only on a true sign-in event. - A user could conceivably leave or be removed from every workspace they belonged to (no UI for this yet, but the data model allows it).
- A workspace could be hard-deleted out from under a user (no UI yet, but again — data-model-allowed).
The current behavior — redirect to /sign-in?error=no_workspace — is acceptable as a hidden defensive branch but is jarring for any user who hits it. We want a deliberate flow.
The flow
- New route:
apps/web/app/(onboarding)/welcome/page.tsx. Server Component. - If user is signed in AND has zero memberships, render a one-step form: "Name your workspace". Default to
${displayName}'s workspace. - Form submit calls a server action that wraps
ensureUserHasWorkspace(lift it out ofauth.tsintoapps/web/server/lib/provision-workspace.tsso both the JWT callback and the server action share the same code). - On success, redirect to
/{newSlug}. apps/web/app/page.tsxredirects the zero-membership case to/welcomeinstead of/sign-in?error=no_workspace.
Out of scope
- Multi-workspace flow (joining an existing workspace by invite). Belongs to
Plan-multitenant-saas-hardening/Task-workspace-invites-and-roles. - Workspace switcher UI. Separate task; not blocking onboarding.
Subtasks
- Extract
ensureUserHasWorkspaceandslugifyForWorkspacefromapps/web/lib/auth.tsintoapps/web/server/lib/provision-workspace.ts. Updateauth.tsto import from there. - Add
apps/web/app/(onboarding)/welcome/page.tsxwith the one-step form. - Add the server action that calls the shared provisioner.
- Update
apps/web/app/page.tsxto redirect zero-membership users to/welcomeinstead of/sign-in.
Owner or assignee
Unassigned
Status
ready
Estimation
S
Acceptance criteria
- Signing in with a session that has no membership shows the welcome form, not a sign-in loop.
- Submitting the form creates a workspace and lands the user on its home page.
- The provisioning logic is shared between the JWT callback (first sign-in) and the welcome flow (recovery).
Links to related Epic / Plan
- Epic:
./Epic-shipping-the-shell.md - Plan:
../Plan-daily-driver-finish.md - Related:
../../Plan-multitenant-saas-hardening/Epic-tenant-lifecycle/Task-workspace-invites-and-roles.md