74 lines
3.2 KiB
Markdown
74 lines
3.2 KiB
Markdown
|
|
---
|
||
|
|
kind: task
|
||
|
|
slug: onboarding-zero-workspace-flow
|
||
|
|
title: Build a proper onboarding flow for users with zero workspaces
|
||
|
|
plan_slug: daily-driver-finish
|
||
|
|
epic_slug: shipping-the-shell
|
||
|
|
status: ready
|
||
|
|
priority: P2
|
||
|
|
tenant_id: global
|
||
|
|
owner: unassigned
|
||
|
|
cursor_todo_id: null
|
||
|
|
updated_at: "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 `ensureUserHasWorkspace` on 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
|
||
|
|
|
||
|
|
1. New route: `apps/web/app/(onboarding)/welcome/page.tsx`. Server Component.
|
||
|
|
2. If user is signed in AND has zero memberships, render a one-step form: "Name your workspace". Default to `${displayName}'s workspace`.
|
||
|
|
3. Form submit calls a server action that wraps `ensureUserHasWorkspace` (lift it out of `auth.ts` into `apps/web/server/lib/provision-workspace.ts` so both the JWT callback and the server action share the same code).
|
||
|
|
4. On success, redirect to `/{newSlug}`.
|
||
|
|
5. `apps/web/app/page.tsx` redirects the zero-membership case to `/welcome` instead 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 `ensureUserHasWorkspace` and `slugifyForWorkspace` from `apps/web/lib/auth.ts` into `apps/web/server/lib/provision-workspace.ts`. Update `auth.ts` to import from there.
|
||
|
|
- [ ] Add `apps/web/app/(onboarding)/welcome/page.tsx` with the one-step form.
|
||
|
|
- [ ] Add the server action that calls the shared provisioner.
|
||
|
|
- [ ] Update `apps/web/app/page.tsx` to redirect zero-membership users to `/welcome` instead 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`
|