# Local accounts, businesses and setup — v0.7

Use invented businesses and .example account addresses. This checkpoint runs on this Mac. It does not create subscribers, verify email ownership, send messages, connect domains, charge money or approve a live launch. All new screens await browser, phone and accessibility checks.

## Create an empty business

1. Sign in with an existing local owner account. The two original demo owners remain available with their documented demo credentials.
2. Open **Account → Start another invented business**. Enter its name, a unique lowercase local website name such as `willow-repairs`, and its service area.
3. Choose **Create empty local business**. The existing account becomes its first owner. No second default password is created. Jobs, customers, documents, payments, materials, team resources, calendar bookings and work notes start empty. The website contains editable neutral sample wording and no supplied photographs or reviews.
4. Choose **Open its workspace**, or use **Active business**. The active business name is shown throughout the workspace. Switching ends the old active session and clears client context. Only memberships belonging to the signed-in account appear in the switcher.
5. Under **Setup & help**, return to the saved checklist as needed. Review and publish the website; confirm solo working or invite another fictional member; read the walkthrough. Jobs, shared quotes and issued invoices count only when actually saved. A pending invitation does not count as a joined member. Publishing a new website revision requires reviewing it again.

The local limit is 25 invented businesses. This is a tested prototype bound, not a subscription tier. Reserved names, traversal strings, uppercase names and ambiguous double hyphens are rejected. Creation reserves the registry, stages a new database/assets folder, and publishes it with one membership. Duplicate submissions with the same operation ID return the same business. A conflicting name or a different request using that ID fails. A generated storage identity collision fails without deleting the existing folder. Interrupted provisioning has a private journal: an uncommitted matching folder is cleaned up on restart; a committed business is retained. A missing registered database fails closed and requires recovery, rather than silently creating empty replacement records.

There is no business deletion, automatic ownership transfer, anonymous sign-up or live-user onboarding in this stage.

## Owners, staff and scheduling resources

| Access | Owner | Staff | Customer portal |
| --- | --- | --- | --- |
| Business customer directory, all jobs, quotes, invoices, payments and costs | Yes, in the active business | No | Only that exact customer’s permitted shared records |
| Website drafts, private image library, settings, domain simulator | Yes | No | No |
| Provision another business; manage members, invitations and setup | Yes | No | No |
| Assigned visits and limited name/address/phone needed for those jobs | Owner workspace has all jobs | Only through an explicit active resource link | No staff view |
| Submit work notes | Owner reads them inside Job details | Append notes to assigned jobs; read own notes | No |
| Own private display name, password and existing business switcher | Yes | Yes | Separate portal code/session |

**Team entries are scheduling resources, not login accounts.** Creating a Team resource grants no sign-in access. Names and email similarity never create a link. Under **Members**, explicitly choose a resource from the same business. One resource can have at most one membership link, including disabled memberships. Explicitly unlink before reusing it. An archived resource provides no assigned-work access. Resource costs and booking snapshots remain private to owners.

Staff work uses non-archived bookings for the linked active resource. Whole-team unavailable periods appear as unavailable dates without private titles/notes. Staff get the job service/stage and the customer name, address and phone needed for those assigned jobs; they do not receive customer email lists, enquiry/private owner notes, rates, costs or financial data. Assignment covers saved booking history until the booking is archived or the resource unlinked/archived; date-range and completion-based retention policies remain a later decision. The first staff view does not include timers, time entry, photos, offline work or billing.

Every registered method/route has an explicit policy in **access-policy.json**. Unlisted endpoints fail closed. Hiding a button is not an authorization control. Business/object checks, portal identity and same-origin/CSRF checks still apply inside the handlers. New routes must be added deliberately to the policy and tested.

## Local invitations

1. In **Members**, enter an invented email ending in `.example`, choose **Staff** or **Owner**, and optionally link an active scheduling resource. Owner access includes all customer and financial records and membership management.
2. Choose **Create local invitation**. The owner response displays a private test code once; it expires in 24 hours. Nothing is emailed. A replacement cancels the preceding code for that email and business. Copy it only for the intended local test session, then hide it.
3. Open **Account access** in a separate local session and choose **Accept an invitation**. Paste the code into the form, never a URL. For a first invitation, enter a display name and password of at least 12 characters. An existing global account must supply its existing password; accepting another business membership never overwrites that password or private name.
4. Sign in normally. Acceptance does not automatically sign anyone in. Each code can be used only once. Expired, revoked, reused or invalid codes fail. A resource must still be eligible at acceptance.

The registry stores a hash of each token, never its raw value. Invitation history shows status, scope and expiry, without hashes or codes. The raw invitation is not stored in retry results or files. If the one-time display is lost, issue a replacement. Existing or disabled memberships are managed in Members; an invitation cannot bypass them. Removing an owner’s authority also cancels their unaccepted invitations in that business.

Saving a role, enable/disable state or resource link ends that member’s sessions in this business. Other memberships are separate. The next request from an old session is rejected. The last active owner cannot be demoted or disabled, including simultaneous attempts; another owner must first accept an invitation. There is no owner deletion or automatic transfer. Changes have a business-scoped audit trail, available to owners at `GET /api/memberships/audit`; the audit has no passwords or token values.

## Password changes and mock recovery

**Account → Change your password** requires the current password and a new password of at least 12 characters. It ends every staff/owner session for that global account across all businesses and invalidates its outstanding reset codes. Customer portal grants remain a separate system.

For the local recovery rehearsal:

1. At **Account access**, choose **Request password recovery** and enter the invented account email. The response is identical whether an eligible account exists or not. It does not send email or establish ownership of the address.
2. The trusted Mac operator locates the private mock capture from the project folder:

   ```sh
   .venv/bin/python account_recovery.py --data-dir .local-data --email jamie@team.example
   ```

   The command prints only a private file path. It never prints the code. Open that local file manually to read its code. Directory permissions are 700 and capture files are 600. This is an operator test mechanism; someone with filesystem access already controls the local prototype. It is not a public retrieval endpoint or a production recovery method.
3. Choose **Use a recovery code**, paste the code and set a new password. Codes expire after 30 minutes and are single-use. A new request supersedes the prior code. Successful reset ends all account sessions and invalidates all outstanding reset codes; it never enables disabled memberships or disabled accounts. Sign in again.

Reset tokens are hashed in the registry. Only the private `.local-capture` mock-delivery folder contains raw reset codes. Codes are absent from URLs, application logs, recovery archives and release packages. Captures are removed on successful use/replacement/password change; an expired leftover file is unusable, and operators may delete it. Capture-write failure rolls back the new token while returning the same public response. Request/consume attempts are rate-limited. Recovery needs an active account with at least one enabled membership in an enabled business.

Real email delivery, verified address changes, MFA, production account recovery, support/operator roles, configurable permissions and production session policy remain unfinished.

## Preserve and recover

Before upgrading, retain the previous code archive and a matching stopped-data backup. v0.7 adds memberships once: each legacy owner becomes an owner membership for its existing business, with no resource link. Existing records and exact customer IDs remain intact. Login email/password and public business branding remain separate.

Format 4 recovery covers the complete dynamic business inventory and a sanitized identity registry. It preserves membership IDs, roles, enabled/disabled state, resource links, setup progress and work-note authors. Every restored eligible global account gets one fresh password, even if it owns several businesses. Disabled accounts or accounts with no enabled membership receive no working password. No old staff/customer session, invitation, reset or portal grant can revive. Formats 1–3 remain strictly validated and migrate only in a fresh restored copy. See [backup and recovery](BACKUP-AND-RECOVERY.md).

Filesystem backup is a separate trusted-operator CLI. No browser role receives backup-file access. Recovery is whole-dataset only; encryption, off-device storage, selective tenant export and production operations remain open.
