Skip to content

Stringer Onboarding + Offboarding

This page is the lifecycle contract for the Stringer entity: what fields are collected at first sign-in, what acceptance criteria the onboarding flow must meet, and what happens when a stringer leaves the platform. Cross-cuts: V2 scope M3, M21, data model — Stringer, use cases — UC-5, client identity & sharing, the receipt-content requirements (racket-book#82, parallel MR — business-identity fields backing receipts). Tracked in racket-book#84.

What this page does NOT cover: the auth mechanics (magic-link, password, JWT) — those are pinned by M3, M21, and the architecture's docs/architecture/auth-and-tenancy.md. This page picks up at the moment the stringer first lands in RBO post-magic-link (the "self-fill profile" step in M3) and ends at the moment the stringer's account is finalized after offboarding.

Onboarding

Recap of the entry point (from M3 + M21)

  1. Admin (Stefan) creates the Stringer row with email; clicks "send invite".
  2. RBO calls gotrue's invite endpoint; gotrue mints + emails a magic-link via Resend SMTP.
  3. Stringer clicks the link.
  4. gotrue verifies; issues a JWT; lands as HttpOnly cookie.
  5. (Iris's lane starts here) RBO renders the onboarding "self-fill profile" form.
  6. Stringer fills the form, saves, lands in their (empty) workspace.
  7. Stringer optionally sets a password from account settings (post-onboarding; M21 dual-method).

Profile fields collected at first sign-in

The onboarding form prompts for the field set below. The flow is single-page (no multi-step wizard); fields are grouped into "Required" and "Optional now / can-fill-later". Defaults are sensible-but-overridable.

Required fields

Field Default Notes
display_name None — must be entered. The "who strung this" name on every receipt (TL-2 per the receipt-content requirements). Validation: non-empty, ≤ 80 chars.
default_locale Browser Accept-Language if EN or DE; else EN. Drives both the UI locale and the receipt-template variant for clients who don't have their own Person.default_locale set. EN | DE only in V2 (M19).
Field Default Notes
business_name NULL Trading name when different from display_name (e.g. display_name = "Stefan Wagen", business_name = "RacketLab"). Rendered below display_name on receipts (TL-3).
business_address NULL Multi-line postal address. Rendered in receipt footer (F-2). The form prompts with the hint: "your customers will see this on every receipt". Onboarding should encourage but not require it.
phone NULL Rendered in receipt footer (F-3).
notification_template Platform-default localized string. Outbound message template for V3 notifications + (V2) receipt-email body. The stringer can edit at any time via account settings.
logo NULL Image asset. Onboarding does not collect the logo (per OQ-R-5 default in receipt-content); it is uploaded later via account settings. The onboarding form surfaces a "you can add a logo later in account settings" note.
receipt_thankyou_text Not present in V2 by default (per OQ-R-3 default). If Stefan flips OQ-R-3, this field appears as optional in onboarding.

Self-Profile (is_stringer_self) slot

The stringer's own ClientProfile (the slot used when they string for themselves — locked decision 1 from V2 scope) is created lazily, not at onboarding:

  • Onboarding does NOT auto-create a self-Person + self-ClientProfile.
  • The first time the stringer creates a self-job (an Order with no client picked, or an explicit "this is for me" affordance), RBO creates:
  • A Person record bound to the stringer's gotrue user ID (via Person.gotrue_user_id), with display_first_name / display_last_name extracted from Stringer.display_name (split on whitespace; stringer can correct in account settings).
  • A ClientProfile under the stringer with is_stringer_self = TRUE referencing that Person.
  • This avoids forcing every newly-onboarded stringer through a "create my own client record" step they may never need (a stringer who only strings for clients never needs a self-ClientProfile).

Acceptance: the lazy-creation path is one transaction; if the stringer creates two self-jobs in quick succession, only one self-Person + ClientProfile is created (idempotency keyed on gotrue_user_id).

Onboarding acceptance criteria

  • A1. The form refuses to save until display_name and default_locale are non-empty.
  • A2. default_locale defaults from the browser's Accept-Language if EN or DE; else EN. The stringer can override before save.
  • A3. Skipping all optional fields is allowed; the stringer lands in an otherwise-empty workspace.
  • A4. The form surfaces explicit guidance for business_address: "your customers will see this on every receipt".
  • A5. The form surfaces a "you can add a logo later in account settings" note where the logo upload would otherwise sit.
  • A6. After save, the stringer is redirected to the empty workspace (matching M3 / UC-5).
  • A7. The self-Person + self-ClientProfile is NOT created at onboarding; it is created lazily on the first self-job (idempotent on gotrue_user_id).
  • A8. Re-rendering the onboarding form after partial save (e.g. browser refresh mid-fill) MUST preserve the entered fields. (Implementation detail; called out so Pax / Mira don't ship a wipe-on-refresh flow.)
  • A9. All onboarding fields are individually editable post-onboarding via account settings.

Hidden requirements (UI affordances the rules imply)

  1. Single-page onboarding form with required fields visually distinguished from optional.
  2. Locale toggle at the top of the form (so the stringer can switch the form itself between EN and DE before completing it).
  3. "Add logo" prompt in account settings with file upload affordance (Mira owns the design).
  4. Self-job creation flow that detects "no ClientProfile picked → assume self" and triggers the lazy self-ClientProfile creation transparently.
  5. Account settings page allowing edit of all onboarding fields plus password set/change (M21).

UX surfacing of items 1–5 is owned by Mira — flagged as a follow-up requirement; not in scope for this requirements doc.

Offboarding

Triggers

Trigger Actor Notes
Admin (Stefan) deactivates a stringer Admin The escalation path for stringers who become inactive, abuse the platform, or otherwise need removing.
Stringer self-deactivates ("close my account") Stringer (any) Account-settings affordance; one-click + confirmation with reason field.
Email-bounce sustained beyond N retries System (advisory) NOT a hard offboarding trigger in V2. RBO marks the stringer's email status as "bouncing" and surfaces the alert in admin's view; admin decides whether to intervene. Documented here so it's not assumed-automatic.

Lifecycle states (offboarding)

active ─► deactivated ─► (90-day grace) ─► admin-finalized (hard delete)
                ▲                                    │
                │                                    │
                └──── re-activate (admin or self) ◄──┘
                                                  (only during grace)
State Description Read access Login Receives notifications?
active Normal operation. All own data + shared-in. Yes. Yes.
deactivated Stringer.deactivated_at set. The stringer's account is locked: cannot log in, cannot mutate data, cannot receive notifications. Their existing data (ClientProfiles, Orders, share grants issued, audit rows) is retained. None (account locked). Refused. Magic-link and password attempts both rejected with a "this account has been deactivated" message. Suppressed.
grace (90-day window after deactivation) The stringer (if self-deactivated) or admin can re-activate without admin-finalization. After 90 days from deactivated_at, the admin gains a "finalize (hard-delete)" action; before 90 days, finalize is hidden / refused with a "wait until grace ends" message. None. Refused. Suppressed.
admin-finalized (hard-deleted) Admin has clicked "finalize" after the grace window expires. The stringer's PII fields are scrubbed; data with retention obligations (orders, receipts, audit) survives in a redacted form (see Cascade rules). N/A — no account exists. N/A. N/A.

Re-activation during grace: clears Stringer.deactivated_at, clears any reactivation_blocker flag, restores normal operation. Magic-link login starts working again. The stringer's data was never moved or modified, so no restoration step is needed beyond flipping the flag.

Cascade rules — what happens to owned data on admin-finalize

This is the FADP-aligned part. Default policy: retention with PII-scrub, not bulk-cascade-delete. Rationale: orders, receipts, and the audit trail are platform-wide records (other stringers may have read shared rows; clients have already received emailed receipts; the platform must remain able to demonstrate FADP compliance via the audit log).

Owned entity Behaviour on admin-finalize
Stringer row PII fields scrubbed: email, phone, business_address, logo, display_name, business_name set to [redacted by request] placeholders. The row is not deleted — orphaning the FK from Orders, ClientProfiles, audit, etc. would be worse than a soft-redact. Stringer.deactivated_at and a new Stringer.finalized_at are set; is_active = FALSE.
ClientProfile rows the stringer owned Retained as historical records (orders reference them). ClientProfile.internal_notes, nickname, default_tension_memo are scrubbed (they are stringer-private data — the stringer chose to leave). The link to Person survives (the Person is platform-level identity, not the stringer's to delete).
Person rows referenced by the stringer's ClientProfiles Untouched. Persons are platform-level. A stringer leaving does NOT delete the human's identity record; other stringers may have ClientProfiles for the same Person, and the V3 portal still serves the Person's own history.
Order rows the stringer created Retained (financial-record / receipt-history reasons). Order.comments is scrubbed (free-text may contain stringer-internal notes). All other fields (lifecycle dates, pricing, racket FK, string spec, etc.) are retained — they are part of the receipt-emit history that other parties may have copies of.
Racket rows the stringer owned Catalogue-shared rackets are retained (other stringers depend on them). catalogue-private rackets owned only by this stringer's self-ClientProfile (i.e. Stefan's own rackets if Stefan offboards) are retained as orphaned by the redacted ClientProfile (no PII risk). one-off rackets attached to specific orders are retained with the order.
String rows the stringer created Same as Racket.
Share grants this stringer issued (order_shares rows where granter_kind = stringer AND granter_stringer_id = this) Auto-revoked (set revoked_at = finalized_at). The grantee no longer sees the shared orders from the next request. Audit-logged as event_kind = grant_revoked with actor_kind = system, meta = {"reason": "granter_offboarded"}.
Share grants this stringer received Auto-revoked from this stringer's side (the stringer can no longer read shared-in orders because their account is gone). The granter's order_shares row is marked revoked_at with actor_kind = system.
Catalogue submissions in pending state Marked rejected automatically by admin-finalize, with reason "submitter offboarded". (If they are in promoted state, the underlying Racket/String row is catalogue-shared and has its own life independent of the submitter.)
share_audit rows where this stringer is the actor Retained verbatim. The audit trail is append-only and a record of historical accesses; redacting it would defeat FADP positioning.
Order.receipt_emit_log rows Retained verbatim. A receipt was sent to a customer's inbox; the platform must remain able to answer "when did this happen?" via the log.

Offboarding acceptance criteria

  • B1. Self-deactivation is a one-click affordance + confirmation dialog with optional free-text reason; on confirm, Stringer.deactivated_at is set immediately and the stringer is logged out.
  • B2. Admin-deactivation is a one-click affordance from the admin's user-list view + confirmation with required reason; same effect.
  • B3. A deactivated stringer's login attempts (magic-link OR password) are rejected with a "this account has been deactivated" message.
  • B4. Re-activation (admin OR self via a "I changed my mind" magic-link path within the grace window) clears deactivated_at and restores normal operation. No data restoration step required.
  • B5. The 90-day grace clock starts at deactivated_at; the admin's "finalize" action is hidden / refused before grace ends.
  • B6. Admin-finalize is a one-click affordance from the admin's deactivated-stringers view + confirmation with required reason; on confirm, the cascade rules execute in a single transaction.
  • B7. Auto-revocation of share grants on finalize fires for both directions (issued by this stringer + received by this stringer), each writing one share_audit row with actor_kind = system.
  • B8. Admin-finalize is the only path to hard-deletion in V2. There is no time-bombed automatic finalize; admin must always click. (V2 keeps the human-in-the-loop; V2.x or V3 may add an auto-finalize for stringers that have been deactivated >> 90 days, with admin-notify, but that is not in V2.)
  • B9. Person rows linked to the offboarded stringer's ClientProfiles are NOT modified by offboarding.
  • B10. Every offboarding action (deactivate / re-activate / finalize) is audit-logged in share_audit with actor_kind = stringer | admin | system as appropriate, target_kind = stringer, and meta carrying the reason.

Hidden requirements (UI affordances the rules imply)

  1. "Close my account" affordance in stringer's account settings (Mira owns the design).
  2. Confirmation dialog with reason text field for both self-deactivate and admin-deactivate.
  3. Admin's deactivated-stringers view — list of stringers with deactivated_at set, sortable by date, with "re-activate" and (post-grace) "finalize" buttons per row.
  4. Re-activation magic-link flow — for the self-deactivated case during grace, a magic-link sent to the original email triggers re-activation. (Implementation detail: gotrue can mint the link; RBO's middleware checks deactivated_at and routes to the re-activate-confirmation page rather than the empty-workspace page.)
  5. Login-refused message — friendly localized message visible on both magic-link and password login attempts when the account is deactivated.
  6. Grace-window indicator in admin's deactivated-stringers view: countdown until finalize-eligible, plus an explicit "finalize-eligible" state once 90 days have elapsed.
  7. Finalize confirmation — destructive-action UI per Mira's design system (probably a typed-confirm pattern — "type the stringer's email to confirm").
  8. Offboarding-cascade dry-run preview — admin's finalize confirmation dialog lists affected entities (count of orders preserved, count of share grants auto-revoked, count of pending catalogue submissions auto-rejected) so the admin sees what will happen before clicking.

UX surfacing of items 1–8 is owned by Mira — flagged as a follow-up requirement; not in scope for this requirements doc.

Edge cases

Case Handling
Stringer deactivates themselves but their email later bounces (e.g. they let the mailbox lapse). Re-activation magic-link cannot reach them. Admin can re-activate manually from their deactivated-stringers view; the stringer can then update their email in account settings.
Stringer was the only admin (Stefan) attempts to self-deactivate. Refused with a clear message. The platform requires at least one admin; Stefan must promote another stringer to admin before self-deactivating. (V2 reality: there is only Stefan, so this case is essentially "Stefan cannot self-offboard"; a future stringer-with-admin-rights would unblock it.)
Admin attempts to finalize a stringer who has issued or received share grants currently being viewed by other stringers. Allowed. Auto-revocation happens in the cascade transaction; the next request from the affected grantee/granter sees the revocation. No mid-flight session disruption beyond that.
A Person had only one ClientProfile (under the offboarding stringer) and now has zero. The Person row is retained. If the Person never claimed an account (no email_verified_at), they remain a draft Person with no ClientProfiles — orphaned but still queryable. If the Person claimed and verified an email at some point, their V3 portal access is unaffected.
Offboarded stringer's email is later re-claimed by a new Person (gotrue invite to a different human at the same address — vanishingly unlikely but possible). Treated as a distinct identity (gotrue user IDs differ). The new Person and the redacted Stringer share an email value but not a gotrue_user_id; the chokepoint resolves on UUID, not email. No silent conflation. Documented here so a future debugger doesn't get confused.

FADP positioning

  • Lawful basis for retention: Orders + receipts are financial records (Swiss commercial-record retention practice; not VAT-required given the disclosure on every receipt, but kept as good-faith business records). Audit + receipt-emit logs are FADP-relevant proof of who-saw-what-and-when. Persons are retained because the human did not request their deletion (FADP grants the human the right to request erasure; the stringer's offboarding is not the human's request).
  • Right to erasure (Person side): unrelated to stringer offboarding; handled separately by ADR-0004's Person soft-delete on request (set Person.deleted_at, scrub PII). This page does not re-document that path.
  • Stringer-side data scrubbed on finalize: email, phone, business_address, logo, display_name, business_name, plus internal_notes / nickname / default_tension_memo on every ClientProfile they owned, plus comments on every Order they created. The remaining structural fields (FKs, dates, prices, racket/string spec) are retained as records.
  • No bulk hard-delete. Cascade-delete of Orders and ClientProfiles would orphan FKs in share_audit, order_shares, person_stringer_share, and Order.receipt_emit_log — defeating the audit trail. Soft-redact-with-retention is the only FADP-defensible posture.
  • The 90-day grace is a recover-from-mistake window; it is NOT a "you-have-90-days-to-export" window. Export is independent (M20 — per-stringer self-service XLSX + JSON export — works on any active account); a stringer wanting their data should export before deactivating, or admin can re-activate-then-export-then-re-deactivate during grace.

Open questions

  • OQ-S-1 (Stefan to confirm or flip): offboarding default — drafted as (b) deactivate + 90-day grace + admin-finalize, FADP-aligned. Alternatives: (a) deactivate + retain indefinitely (no auto-finalize); (c) immediate hard-delete with cascade (rejected by Iris on FADP/audit grounds). Default (b) applied unless Stefan specifies otherwise.
  • OQ-S-2 (Stefan to decide): is the self-deactivation re-activation magic-link path enabled in V2, or is re-activation admin-only?
  • Default applied: enabled in V2 (matches the M21 dual-method spirit — the stringer owns their own account).
  • One-flag flip if Stefan prefers admin-only.
  • OQ-S-3 (Stefan to decide): can a stringer who self-deactivated and was finalized later re-onboard with the same email?
  • Default applied: yes, treated as a new Stringer + new gotrue user ID (same email, distinct UUID). The redacted-but-retained old Stringer row keeps its FK references intact.
  • If Stefan prefers blocking re-onboard at the same email, a UNIQUE constraint on Stringer.email WHERE deleted_at IS NULL already gives that behaviour for an active row, but blocking against a finalized row needs an explicit policy.

Cross-references

  • Architecture / auth: docs/architecture/auth-and-tenancy.md, M3, M21.
  • Sibling requirements: the receipt-content requirements (racket-book#82, parallel MR — the business-identity field group is scoped there; this page references which fields onboarding collects); the order-lifecycle requirements (racket-book#80, parallel MR — admin-override on locked orders is a related admin-only mechanism).
  • Identity model: client identity & sharing — Person/ClientProfile split. Self-Person + self-ClientProfile are created lazily, not at onboarding.
  • Use cases: UC-5 — Multi-stringer admin — onboarding overview lives there; this page is the rules-layer detail behind it.
  • Data model: Stringer entity — gains the new business-identity fields (per the receipt-content requirements) plus deactivated_at, finalized_at, is_active for the offboarding lifecycle.
  • UX: Mira owns the surface design for items 1–5 (onboarding) and 1–8 (offboarding) in the hidden requirements sections.
  • Implementation: Pax (after Mira's design lands).
  • Issue tracking: racket-book#84 (this page).