Table of Contents
21 — Sleep-plan authoring and history
Technical reference. For task-based help, see the corresponding user guide.
Documentation status: complete for the current implementation
Reviewed: 2026-07-21
Primary users: subscribed coaches
Surfaces: coach admin portal, platform API and protected file storage
Purpose
Sleep-plan authoring gives an assigned coach one working plan for a family and a durable, read-only history of plans the coach deliberately archives. The working plan can combine structured paragraphs, bullet points, images and an optional PDF. A coach may keep it private as a draft or publish it into the family's mobile sleep-plan library.
The lifecycle distinguishes local edits, a durable draft, a family-visible publication and archived history. Those states matter: publishing is the only action that makes current content visible to the family, while archiving removes the current publication and starts a clean next version.
User outcomes
| Coach need | Outcome |
|---|---|
| Build a plan in the portal | Edit an ordered title/section structure with paragraphs, bullets and images. |
| Attach an existing document | Upload and retain a PDF alongside the structured plan. |
| Work privately | Save a durable draft that family endpoints do not return. |
| Share or revise guidance | Publish the current content, then publish later edits when ready. |
| Preserve a milestone | Archive the current plan and begin a new versioned draft. |
| Reuse older guidance | Copy any archived version back into the current draft without destroying history. |
| Retire obsolete history | Permanently remove a selected archived database record after confirmation. |
Entry points and visibility
| Surface | Entry point | Visible records |
|---|---|---|
| Coach portal | Family Session Prep → sleep-plan card/editor | One current non-archived plan plus every archived version for that coach-family assignment. |
| Family mobile | Sleep plans | Published plans only; never drafts or archived history. See Feature 13. |
| Coach mobile view-as | Family sleep plans | The same family-facing published collection, not the authoring editor or history. |
Opening Session Prep calls the singular coach read. If no current plan exists, that read creates version 1 as a draft titled Sleep plan with starter sections for Goals, Bedtime routine, Night wakes and Naps.
Plan data model
Each plan record is scoped to organization, coach and family, and contains:
- a stable record ID and positive version number;
- a title;
- 1–30 ordered sections;
- up to 80 blocks in each section;
- optional PDF asset metadata;
draft,publishedorarchivedstatus; and- creation, update, publication and archive timestamps as applicable.
Section titles and the plan title are limited to 180 characters. Paragraph and bullet text is limited to 4,000 characters per block. Image block identifiers are limited to 80 characters; filename, content type and asset path are validated, while alt text and caption are each limited to 500 characters.
Structured editor
Sections
- Add section appends a starter section with one paragraph.
- A section title is required by the server.
- The final remaining section cannot be removed.
- Sections can be reordered by dragging the handle.
- With the handle focused, Arrow Up and Arrow Down provide a keyboard reorder path.
- The saved section array is the authoritative display order in family and archived views.
Paragraphs and bullets
Each section has a plain-text area that maps non-empty lines to blocks:
- a line beginning exactly with ''- '' becomes a bullet block;
- every other non-empty line becomes a paragraph block; and
- blank lines are omitted.
Editing that text area rebuilds the section's text blocks, then retains its existing image blocks after them. The current editor therefore does not interleave images between individual paragraphs or expose block-by-block reordering inside a section. The 80-block server limit counts paragraphs, bullets and images together.
Images
Add image accepts JPEG, PNG, WebP or GIF files no larger than 10 MB before client-side compression.
- JPEG, PNG and WebP are resized to at most 2,000 pixels on their longest side and encoded at quality 0.82 only when the result is smaller than the original.
- The browser prefers WebP when supported, then falls back to the original compatible format.
- GIF files are uploaded unchanged so animation is not flattened.
- A successful upload creates an image block with asset path, original display filename, content type and upload time.
- The coach can edit a caption and accessibility alt text, preview the image or remove its reference from the draft.
Images use the sleep-plan-images bucket. Preview/download requests use short-lived signed URLs with a requested lifetime of 900 seconds; storage is not public.
PDF attachment
The editor accepts application/pdf or a .pdf filename up to 20 MB and uploads it to sleep-plan-pdfs. Once attached, the coach can download it through a fresh 900-second signed URL or remove its metadata from the plan draft.
The structured plan and PDF are complementary. A plan can have either or both; publishing does not require a PDF.
Upload and failure behavior
File upload is a two-stage process:
- the portal asks the platform for a signed upload URL scoped to the assigned family and permitted bucket/path;
- the browser uploads directly to protected storage; then
- the new asset metadata is placed only in the local plan draft.
After a successful upload, the portal explicitly says to Save draft or Publish to keep the attachment. If signing or upload fails, the plan is not given a durable asset reference and the coach can retry. If the upload succeeds but the coach abandons or removes the unsaved edit, the current UI does not issue a storage-object deletion; the uploaded object may remain unattached until storage lifecycle/cleanup handles it.
Signed URLs can expire while a screen is open. Reloading the preview or asking to download again obtains a new URL; applications must never persist a signed URL as plan content.
Draft and publication lifecycle
Unsaved local edits
Changing text, order, files or image metadata marks the plan dirty only in the current browser. If the durable record is already published, the family continues to see the last published server version until the coach saves or publishes.
There is no plan revision token or expectedUpdatedAt in the write contract. Concurrent coaches/tabs therefore use last-write-wins behavior, and a stale tab can replace newer content.
Save draft
Save draft writes the complete current plan with status draft.
- A new or existing draft remains hidden from family endpoints.
- Saving a previously published plan as draft withdraws it from the family's list and clears
publishedAt. - The same current record and version number are retained.
- A successful response replaces local state and clears the dirty marker.
Publish
Publish writes the complete plan with status published.
- The family can see the durable result only after this request succeeds.
- The first publication timestamp is retained across subsequent writes that keep the plan published.
- If the plan was saved back to draft,
publishedAtwas cleared and the next publication establishes a new publication timestamp. - A published plan with local dirty edits is labelled Published with unsaved edits; Publish changes replaces the family-visible version.
- When no edits are pending, the published action is disabled and labelled Published.
Archive and version history
Current/history invariant
The platform is designed to retain exactly one non-archived working record for the coach-family relationship. History contains records whose status is archived, sorted newest archive first and then by version.
Versions identify records, not every save. Saving draft or publishing edits keeps the current version. Archiving creates the next record/version.
Archive current
Archive current is disabled while local edits are dirty; the coach must save or publish first. The platform then performs one transaction:
- changes the current record to
archivedand recordsarchivedAt; - immediately removes it from family-visible published results; and
- creates a clean draft from the standard starter content using one more than the highest existing version.
The newly created draft becomes the editor's current plan. The archived record, including its status at the moment of archiving, structured content and asset metadata, remains in Plan history.
View history
Each history row shows version, title, archive date and section count. View opens a read-only rendering of its paragraphs, bullets, images and PDF filename. Archived image previews still require authorized signed-download requests.
Restore / copy to draft
Restore is deliberately a copy operation:
- the selected archived record remains unchanged in history;
- its title, sections, image metadata and PDF metadata replace the current working record;
- the current record becomes
draft, with no publication timestamp; and - family visibility is not restored until the coach explicitly publishes.
If a current record already exists, its identity and version stay the same. If none exists, the server creates a new next version. The portal asks before replacing unsaved local edits.
Permanently remove archived
Remove asks for confirmation and deletes only the selected archived database record. It does not alter the current plan. If an inconsistent state has no current record, the service repairs the invariant by creating a new draft. The operation cannot be undone from the UI; removal of the database record does not itself constitute a storage-bucket purge of referenced files.
Platform API contract
| Method and path | Role |
|---|---|
GET /coach/families/:familyId/sleep-plan | Read or create the one current working plan and return archived history. |
GET /coach/families/:familyId/sleep-plans | Return the coach's family-facing published collection without creating a draft. |
PUT /coach/families/:familyId/sleep-plan | Save the complete current plan as draft or published. |
POST /coach/families/:familyId/sleep-plan/archive | Archive current and atomically create the next clean draft. |
POST /coach/families/:familyId/sleep-plan/archived/:sleepPlanId/restore | Copy one authorized archived record into the current draft. |
DELETE /coach/families/:familyId/sleep-plan/archived/:sleepPlanId | Permanently remove one authorized archived record. |
POST /files/signed-upload-url | Authorize a short-lived PDF/image upload for the family scope. |
POST /files/signed-download-url | Authorize a short-lived PDF/image read for the family scope. |
Family-facing GET /families/me/sleep-plans returns only published records for the signed-in family's current assignment and is documented in Feature 13.
Authorization and privacy
- Every authoring/history route resolves the requested family through a matching active coach assignment rather than accepting arbitrary client scope.
- Access also requires active organization membership, an active/trialing organization, qualifying coach subscription and a support entitlement that permits record access.
- Paused support still permits record access; expired, revoked, canceled or inactive-billing access does not.
- Plan IDs and file paths are rechecked against coach, organization and family scope on every request.
- Family routes filter to
publishedand cannot ask for an archived ID or obtain a draft signed URL. - Storage buckets remain private; durable plan records contain asset paths, never permanent public URLs.
User-visible states and recovery
| State | Expected behavior |
|---|---|
| Initial read with no plan | Create and display the starter version 1 draft. |
| Draft | Explain that the family cannot view it yet. |
| Published and clean | Show publication time and disable duplicate publish. |
| Published and dirty | Explain that the family still sees the last published server version. |
| Validation/write failure | Keep local edits dirty and show the API error for correction/retry. |
| Asset upload failure | Do not attach metadata; keep the rest of the draft editable. |
| Dirty archive attempt | Disable archive and instruct the coach to save or publish first. |
| Dirty restore attempt | Confirm before replacing unsaved local edits. |
| Archived viewer | Remain read-only; return to the current editor to modify content. |
| Access ends | Deny authoring, history and signed files without exposing whether a plan exists. |
Acceptance checklist
- A family relationship has one current plan and may have multiple archived records.
- The editor round-trips ordered sections, paragraph/bullet/image blocks and optional PDF metadata within contract limits.
- Image/PDF type and size validation occurs before upload, and successful uploads still require a plan save.
- Draft records are absent from family responses; only a successful publish makes content visible.
- Saving a published plan as draft withdraws it and clears publication time.
- Archive is atomic and starts the next clean version without losing the archived source.
- Restore copies to a draft, preserves history and never republishes automatically.
- Permanent removal targets only an archived record in the authorized assignment.
- Stale-tab last-write-wins behavior and possible unattached uploads are treated as current operational caveats.
Source evidence
Coach portal
admin-web/src/routes/SessionPrepArtifactsPanel.tsxadmin-web/src/routes/SessionPrepPanel.tsxadmin-web/src/api/client.ts
Platform, storage and contracts
platform/apps/api/src/routes/coach-routes.tsplatform/apps/api/src/routes/family-routes.tsplatform/apps/api/src/routes/file-routes.tsplatform/apps/api/src/services/family-profile-artifact-service.tsplatform/packages/shared/src/family-profile-artifacts.tsplatform/packages/shared/src/files.ts
