Preparing the canvas…
Self-contained context for a chat model with no repository access.
# Sign-in
This document is a self-contained specification. You do not have repository access. Treat every backticked span as a literal that must survive into the implementation unchanged.
## Brief
**Deliverable & format:** A complete sign-in flow for a web application, implemented exactly as specified in this document
**Audience & downstream use:** A single operator handing this prompt to a coding agent (Claude Code, Cursor, Lovable, v0, Bolt, or a generic chat LLM)
**Hard constraints:** No user accounts beyond this sign-in flow; session state lives in the session store; British English in all user-facing copy
**Definition of done:** An unauthenticated visitor can sign in with valid credentials and land on /dashboard, or see a precise error on failure, and can request a password reset
**Role & register:** Build as a senior front-end engineer would: typed, testable, no clever abstractions before the second use case appears
**Non-goals:** Social sign-in, multi-factor authentication, real-time collaboration, mobile-first layouts, a template marketplace
**Sources:** This FlowSpec graph and the nine-line brief. No other documents.
**Epistemic requirement:** Where this specification is silent, state the assumption rather than inventing a requirement. Do not add features that are not named above.
**Iteration mode:** Single delivery. Implement the graph as specified; do not start adjacent work.
## Derived facts
- Entry points: start-1
- Terminals: end-2, screen-2, end-1, screen-4
- Screens: Sign in `/sign-in`; Dashboard `/dashboard` (terminal); Forgot password `/forgot-password`; Reset sent `/reset-sent` (terminal)
- Entities: `User` identity `id`; `Session` identity `id`
## Graph
- start-1 Start “User opens the application”
- trigger: The visitor navigates to the product unauthenticated.
- preconditions: No session token is present in the session store.
- screen-1 Screen “Sign in”
- route: `/sign-in`
- purpose: Collect username and password and offer a password-reset path.
- layout regions: `header`, `form`, `footer`
- key components: `username field`, `password field`, `submit button`, `forgot-password link`
- states: empty=A blank form ready for credentials.; loading=The submit button shows a spinner and fields are locked.; error=A banner repeats the API error case.; success=The form yields to a redirect.
- requires authorisation: no
- terminal: no
- modal-1 Modal “Forgot password prompt”
- trigger: The user clicks Forgot password on the sign-in screen.
- purpose: Confirm that the user wants to reset their password.
- key components: `confirmation copy`, `continue button`, `cancel button`
- dismissal: The cancel button, the overlay click, or the Escape key.
- blocking: yes
- process-1 Process “Submit credentials”
- action: Validate the sign-in form locally and submit credentials to the authentication API.
- inputs: `username`, `password`
- outputs: `credentialsPayload`
- side effects: `records an authentication attempt`
- screen-3 Screen “Forgot password”
- route: `/forgot-password`
- purpose: Collect an email address and request a password-reset token.
- layout regions: `header`, `form`
- key components: `email field`, `send-reset button`
- states: empty=Email field only.; loading=Send-reset button pending.; error=Banner for unknown_email or reset_token_failed.; success=Redirects to the reset-sent screen.
- requires authorisation: no
- terminal: no
- decision-1 Decision “Credentials valid?”
- predicate: `authService.verify(username, password) returning {ok: true}`
- evaluated against: variable `credentials`
- exhaustiveness: The verify call returns either {ok: true} or {ok: false}; there is no third status.
- screen-4 Screen “Reset sent”
- route: `/reset-sent`
- purpose: Confirm that a reset email was dispatched.
- layout regions: `message`
- key components: `confirmation copy`, `back-to-sign-in link`
- states: empty=NONE; loading=NONE; error=NONE; success=A confirmation that the reset email is on its way.
- requires authorisation: no
- terminal: yes
- process-2 Process “Establish session”
- action: Write the session token into the session store and mark the user as authenticated.
- inputs: `sessionToken`, `user`
- outputs: `activeSession`
- side effects: `persists session keys on the client`
- end-2 End “Authentication failed”
- outcome: The user remains unauthenticated and sees the sign-in error.
- state left behind: The session store is empty; the sign-in form is still on screen.
- screen-2 Screen “Dashboard”
- route: `/dashboard`
- purpose: Show the signed-in home once a session exists.
- layout regions: `app shell`, `summary`, `recent activity`
- key components: `greeting`, `sign-out button`
- states: empty=NONE; loading=NONE; error=NONE; success=The signed-in dashboard with the user's email in the header.
- requires authorisation: yes
- terminal: yes
- end-1 End “Signed in”
- outcome: The user holds a valid session and is looking at the dashboard.
- state left behind: sessionToken and currentUserId are populated in the session store.
- entity-1 Entity “User”
- name: `User`
- identity field: `id`
- field `id`: type `string`, required yes, default `cuid()`
- field `email`: type `string`, required yes, default `none`
- field `passwordHash`: type `string`, required yes, default `none`
- field `createdAt`: type `Date`, required yes, default `now()`
- relation → node `entity-2` (1..n): A user holds many sessions.
- api-call-1 API Call “Sign-in API”
- method: `POST`
- path: `/auth/login`
- request shape: { username: string, password: string }
- response shape: { ok: true, token: string, user: User } | { ok: false, error: string }
- auth mode: none
- error cases: `invalid_credentials`, `account_locked`, `rate_limited`
- entity-2 Entity “Session”
- name: `Session`
- identity field: `id`
- field `id`: type `string`, required yes, default `cuid()`
- field `userId`: type `string`, required yes, default `none`
- field `token`: type `string`, required yes, default `none`
- field `expiresAt`: type `Date`, required yes, default `none`
- relation → node `entity-1` (1): Each session belongs to one user.
- api-call-2 API Call “Password reset API”
- method: `POST`
- path: `/auth/password-reset`
- request shape: { email: string }
- response shape: { ok: true } | { ok: false, error: string }
- auth mode: none
- error cases: `unknown_email`, `reset_token_failed`
- store-1 Store “Session store”
- scope: `session`
- keys held: `sessionToken`, `currentUserId`
- lifetime: Until the browser tab is closed or the user signs out.
- invalidation trigger: Explicit sign-out or a 401 from any authorised API call.
## Edges
- start-1 → screen-1 (control) when `user navigates to the sign-in page`
- screen-1 → process-1 (control) when `user submits the sign-in form`
- screen-1 → modal-1 (navigation) when `user clicks Forgot password`
- modal-1 → screen-3 (navigation) when `user confirms the password-reset prompt`
- process-1 → decision-1 (control)
- process-1 → api-call-1 (invocation)
- screen-3 → screen-4 (navigation) when `user submits the password-reset form`
- decision-1 → process-2 (control) when `authService.verify(username, password) returning {ok: true}`
- decision-1 → end-2 (control) when `authService.verify(username, password) returning {ok: false}`
- screen-3 → api-call-2 (invocation)
- process-2 → screen-2 (control)
- process-2 → store-1 (invocation)
- screen-2 → end-1 (control)
- api-call-1 → decision-1 (data)
- api-call-2 → screen-4 (data)
- entity-1 → screen-2 (data)
- store-1 → screen-2 (data)
- entity-1 → api-call-1 (data)
- entity-2 → store-1 (data)
- entity-2 → entity-1 (data)
## Known gaps
None.
Return A complete sign-in flow for a web application, implemented exactly as specified in this document, and nothing else. Ask no clarifying questions about the graph; every fact you need is stated above.