Skip to main content

Feature: Families & Sharing

Version: 1.0.0 Last Reviewed: 2026-02-25 Status: Draft Supersedes: share-links.md v1.2.0

Overview

Families are the organizational unit for Baby Basics. A family groups children and members together. Members have roles (parent or caregiver) that control what they can manage. Parents invite others via time-limited, single-use deep links shared over SMS.

Data Model

New Tables

Family
id UUID (PK)
name VARCHAR(100) -- e.g. "Johnny's Family"
created_by UUID (FK → users)
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

FamilyMember
id UUID (PK)
family_id UUID (FK → families, CASCADE)
user_id UUID (FK → users, CASCADE)
role VARCHAR(20) -- "parent" or "caregiver"
joined_at TIMESTAMPTZ
UNIQUE(family_id, user_id)

Modified Tables

Child
+ family_id UUID (FK → families, CASCADE) -- NEW, replaces ChildAccess
(all other fields unchanged)

ShareLink
- child_id REMOVED
+ family_id UUID (FK → families, CASCADE) -- invites are per-family now
- token REMOVED (was plaintext)
+ token_hash VARCHAR(64) UNIQUE -- SHA-256 hash of raw token
+ role VARCHAR(20) DEFAULT "caregiver" -- role granted on acceptance
+ expires_at TIMESTAMPTZ -- 7 days from creation
(id, created_by, used_at, used_by, created_at unchanged)

User
+ family_memberships FamilyMember[] -- relation

Dropped Tables

ChildAccess  -- replaced by FamilyMember

Migration Strategy

No production data exists yet. Dev has only test data. Migration is clean:

  1. Create families and family_members tables
  2. Add family_id to children (NOT NULL after backfill)
  3. For each existing user with ChildAccess.role = "owner": create a Family, create FamilyMember(parent), update their children's family_id
  4. For each existing ChildAccess.role = "caregiver": create FamilyMember(caregiver) in the child's family
  5. Modify share_links: add family_id, token_hash, role, expires_at; drop child_id, token
  6. Drop child_access table

Roles & Permissions

Two roles exist at the family level. All members of a family see all children in that family.

Operationparentcaregiver
View children + tracking datayesyes
Log events (feedings, diapers, sleeps, notes)yesyes
Edit/delete eventsyesyes
View dashboard & timelineyesyes
Add children to familyyesno
Edit/delete childrenyesno
Edit family nameyesno
Invite family membersyesno
Remove family membersyesno
Delete familyyesno

Future (post-MVP): Per-child restriction. An additive ChildRestriction(family_member_id, child_id) table would limit specific members to specific children. Absence of restrictions = full family access (current behavior).

Future (post-MVP): Viewer role (read-only). Defined but not implemented.

API Contract

Families

Create Family

POST /api/v1/families
Authorization: Bearer <token>

Request:
{
"name": "Johnny's Family"
}

Response 201:
{
"family": {
"id": "uuid",
"name": "Johnny's Family",
"created_at": "2026-02-25T12:00:00.000Z",
"updated_at": "2026-02-25T12:00:00.000Z"
}
}

Notes:
- Creates the family and adds the requesting user as a member with role "parent".
- Name is required, 1-100 chars, trimmed.
- A user can create multiple families.

List User's Families

GET /api/v1/families
Authorization: Bearer <token>

Response 200:
{
"families": [
{
"id": "uuid",
"name": "Johnny's Family",
"role": "parent",
"children_count": 1,
"members_count": 2,
"created_at": "2026-02-25T12:00:00.000Z"
}
],
"count": 1
}

Notes:
- Returns all families the user is a member of.
- `role` is the requesting user's role in each family.
- Sorted by created_at ascending (oldest first).

Get Family Details

GET /api/v1/families/:familyId
Authorization: Bearer <token>

Response 200:
{
"family": {
"id": "uuid",
"name": "Johnny's Family",
"role": "parent",
"members": [
{
"user_id": "uuid",
"name": "Johnny",
"email": "johnny@example.com",
"role": "parent",
"joined_at": "2026-02-25T12:00:00.000Z"
},
{
"user_id": "uuid",
"name": "Maria",
"email": "maria@example.com",
"role": "caregiver",
"joined_at": "2026-02-26T10:00:00.000Z"
}
],
"children": [
{
"id": "uuid",
"name": "Baby Bretz",
"date_of_birth": "2026-03-15"
}
],
"created_at": "2026-02-25T12:00:00.000Z",
"updated_at": "2026-02-25T12:00:00.000Z"
}
}

Response 403 (not a member):
{ "error": { "code": "FORBIDDEN", "message": "Not a member of this family", "details": [] } }

Update Family

PATCH /api/v1/families/:familyId
Authorization: Bearer <token>

Request:
{
"name": "The Bretz Family"
}

Response 200:
{
"family": {
"id": "uuid",
"name": "The Bretz Family",
"created_at": "...",
"updated_at": "..."
}
}

Response 403 (caregiver):
{ "error": { "code": "FORBIDDEN", "message": "Only parents can update family settings", "details": [] } }

Delete Family

DELETE /api/v1/families/:familyId
Authorization: Bearer <token>

Response 204: (No Content)

Response 403 (caregiver):
{ "error": { "code": "FORBIDDEN", "message": "Only parents can delete a family", "details": [] } }

Notes:
- Cascade deletes: all FamilyMembers, Children (and their Feedings, Diapers, Sleeps, Notes), ShareLinks.
- AuditLog entries remain.

Members

List Members

GET /api/v1/families/:familyId/members
Authorization: Bearer <token>

Response 200:
{
"members": [
{
"user_id": "uuid",
"name": "Johnny",
"email": "johnny@example.com",
"role": "parent",
"joined_at": "2026-02-25T12:00:00.000Z"
}
],
"count": 1
}

Notes:
- Any family member can list members (parents and caregivers both).
- Sorted by joined_at ascending.

Remove Member

DELETE /api/v1/families/:familyId/members/:userId
Authorization: Bearer <token>

Response 204: (No Content)

Response 400 (removing self):
{ "error": { "code": "VALIDATION_ERROR", "message": "Cannot remove yourself. Leave the family or delete it instead.", "details": [] } }

Response 403 (caregiver attempting removal):
{ "error": { "code": "FORBIDDEN", "message": "Only parents can remove family members", "details": [] } }

Response 404 (user not a member):
{ "error": { "code": "NOT_FOUND", "message": "Member not found", "details": [] } }

Notes:
- Only parents can remove members.
- Parents cannot remove themselves (must delete family or transfer ownership post-MVP).
- Events logged by the removed member are preserved (created_by still references them).

Invites

Create Invite

POST /api/v1/families/:familyId/invites
Authorization: Bearer <token>

Request:
{
"role": "caregiver"
}

Response 201:
{
"invite": {
"id": "uuid",
"join_url": "https://baby.bretzfam.com/join/xK9mLp2qR5tN8vW3yBcd4A",
"role": "caregiver",
"expires_at": "2026-03-04T12:00:00.000Z",
"created_at": "2026-02-25T12:00:00.000Z"
}
}

Response 403 (caregiver):
{ "error": { "code": "FORBIDDEN", "message": "Only parents can invite family members", "details": [] } }

Notes:
- `role` is required: "parent" or "caregiver".
- Token: 128 bits via crypto.randomBytes(16), encoded as base64url (22 chars).
- Token is returned ONCE in the join_url. Only the SHA-256 hash is stored in the DB.
- join_url format: {BASE_URL}/join/{token}
- Expires 7 days from creation.
- If an unused, non-expired invite with the same role exists, return it (idempotent).
- If the only existing invite is expired or used, generate a new one.
- Only parents can create invites.

Accept Invite

POST /api/v1/invites/accept
Authorization: Bearer <token>

Request:
{
"token": "xK9mLp2qR5tN8vW3yBcd4A"
}

Response 201:
{
"family": {
"id": "uuid",
"name": "Johnny's Family",
"role": "caregiver"
},
"invited_by": {
"name": "Johnny"
}
}

Response 404 (invalid, expired, or used token):
{ "error": { "code": "NOT_FOUND", "message": "Invalid or expired invite link", "details": [] } }

Response 409 (already a member):
{ "error": { "code": "CONFLICT", "message": "You are already a member of this family", "details": [] } }

Response 400 (accepting own invite):
{ "error": { "code": "VALIDATION_ERROR", "message": "Cannot accept your own invite", "details": [] } }

Notes:
- Server hashes the incoming token with SHA-256, looks up by token_hash.
- Checks: token exists, not used (used_at is null), not expired (expires_at > now).
- Invalid, used, and expired tokens all return 404 (no enumeration).
- Creates FamilyMember with the role stored on the ShareLink.
- Marks ShareLink as used: sets used_at and used_by.
- Rate limited: 5 requests/minute per IP.
- This endpoint is NOT nested under /families/:familyId because the accepting user doesn't know the family ID.

Children (Modified)

Create Child

POST /api/v1/families/:familyId/children
Authorization: Bearer <token>

Request:
{
"name": "Baby Bretz",
"date_of_birth": "2026-03-15"
}

Response 201:
{
"child": {
"id": "uuid",
"family_id": "uuid",
"name": "Baby Bretz",
"date_of_birth": "2026-03-15",
"created_at": "...",
"updated_at": "..."
}
}

Response 403 (caregiver):
{ "error": { "code": "FORBIDDEN", "message": "Only parents can add children", "details": [] } }

Notes:
- Replaces POST /api/v1/children. Child is created under the specified family.
- Only parents can add children.

List Children

GET /api/v1/children
Authorization: Bearer <token>

Response 200:
{
"children": [
{
"id": "uuid",
"family_id": "uuid",
"family_name": "Johnny's Family",
"name": "Baby Bretz",
"date_of_birth": "2026-03-15",
"role": "parent",
"created_at": "...",
"updated_at": "..."
}
],
"count": 1
}

Notes:
- Returns all children across all families the user belongs to.
- `role` is the user's role in the child's family.
- Used by the child switcher in the iOS app.

Get, Update, Delete Child

Existing endpoints (GET/PUT/DELETE /api/v1/children/:childId) remain unchanged in URL. Internally, access is verified via family membership instead of ChildAccess. Delete is parent-only.

Tracking Routes (Unchanged)

All tracking routes remain at their current URLs and are unaffected:

  • /api/v1/children/:childId/feedings
  • /api/v1/children/:childId/diapers
  • /api/v1/children/:childId/sleeps
  • /api/v1/children/:childId/notes
  • /api/v1/children/:childId/dashboard
  • /api/v1/children/:childId/timeline

Access verification changes internally (queries FamilyMember instead of ChildAccess) but the external contract is identical.

ParameterValue
Token entropy128 bits (crypto.randomBytes(16))
Token formatBase64url, 22 characters
Token storageSHA-256 hash in DB; raw token returned once at creation
Expiry7 days from creation (fixed, not configurable)
UsageSingle-use (used_at set on acceptance)
Error responses404 for invalid, expired, AND used tokens (no enumeration)
Rate limit5 req/min per IP on /invites/accept
HTTPSRequired (enforced at infrastructure level)
Referrer policyno-referrer on any web page serving the join URL

iOS Flows

Onboarding: New User (Organic)

1. Register (name, email, password)

2. Setup (one screen):
┌─────────────────────────────┐
│ Welcome, Johnny! │
│ │
│ Family: [Johnny's Family ] │
│ (pre-filled, editable) │
│ │
│ Baby's name: [ ] │
│ Date of birth: [ ] │
│ │
│ [Get Started] │
└─────────────────────────────┘

3. Dashboard (with new child active)
  • Family name defaults to "{First Name}'s Family"
  • User can edit the family name or just tap through
  • "Get Started" calls: POST /families → POST /families/:id/children → navigate to dashboard
1. User taps link: baby.bretzfam.com/join/xK9mLp2...

2. App opens (universal link), stores token in UserDefaults

3. Not authenticated → Register screen
(context banner: "You've been invited to a family!")

4. After registration, app detects pending token

5. POST /invites/accept with stored token

6. Success → navigate to shared family's dashboard
(skip family+child setup entirely)

7. Clear stored token
  • If accept fails (expired/used): show "This invite link is no longer valid" and continue to normal onboarding
  • Token stored in UserDefaults (temporary, low-sensitivity)
1. User taps link while logged in

2. App opens, detects authenticated user

3. POST /invites/accept immediately

4. Success → show banner: "You joined Johnny's Family!"
Navigate to/switch to the new family

Settings: Family Management

Settings > Family
┌─────────────────────────────┐
│ Johnny's Family [Edit]│
│ │
│ Members │
│ ┌─────────────────────────┐│
│ │ 👤 Johnny Parent ││
│ │ 👤 Sarah Parent ││
│ │ 👤 Maria Caregiver ││
│ │ [Remove] ││
│ └─────────────────────────┘│
│ │
│ Children │
│ ┌─────────────────────────┐│
│ │ 👶 Baby Bretz ││
│ │ Born: Mar 15, 2026 ││
│ └─────────────────────────┘│
│ │
│ [+ Invite Family Member] │
│ [+ Add Child] │
│ │
│ ─── Other Families ─── │
│ [+ Create New Family] │
└─────────────────────────────┘

Caregivers see the same screen but without Edit, Remove, Invite, Add Child, or Create New Family buttons.

Settings: Invite Creation Flow

Step 1: Choose role
┌─────────────────────────┐
│ Invite Family Member │
│ │
│ Role: │
│ [██ Parent] [Caregiver]│
│ │
│ Parents can invite and │
│ manage family settings. │
│ │
│ [Create Invite Link] │
└─────────────────────────┘

Step 2: Share link
┌─────────────────────────┐
│ ✅ Invite Created │
│ │
│ baby.bretzfam.com/join/ │
│ xK9mLp2qR5tN8vW3yBcd4A │
│ │
│ Role: Parent │
│ Expires: Mar 4, 2026 │
│ │
│ [📋 Copy] [📤 Share...]│
│ │
│ [Done] │
└─────────────────────────┘

Share sheet pre-fills: "Join Johnny's Family on Baby Basics! {join_url}"

Family Switching

When a user belongs to multiple families, the active family determines which children appear in the dashboard and child switcher. Switching is done via Settings > Families (tap a different family).

The app stores the active family ID in @AppStorage("activeFamilyId").

The iOS app registers for the /join/* path pattern in the AASA (Apple App Site Association) file.

When the app handles a universal link:

  1. Extract the token from the URL path (/join/{token})
  2. If authenticated: call POST /invites/accept immediately
  3. If not authenticated: store token in UserDefaults("pendingInviteToken"), show auth flow
  4. After auth: check for pending token, call accept, clear token

AASA update needed:

{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.com.bretzfam.babybasics",
"paths": ["/join/*"]
}
]
}
}

Audit Logging

Evententity_typeaction
Family createdfamilycreate
Family updatedfamilyupdate
Family deletedfamilydelete
Member joined (via invite)family_membercreate
Member removedfamily_memberdelete
Invite createdshare_linkcreate
Invite acceptedshare_linkupdate
Child created in familychildcreate

Acceptance Criteria

Families

  • Can create a family with a name (201, returns family object)
  • Can list all families the user belongs to (with role, counts)
  • Can get family details including members and children
  • Parents can update family name
  • Caregivers cannot update family name (403)
  • Parents can delete family (cascades all data)
  • Caregivers cannot delete family (403)

Members

  • Can list all members of a family (any member)
  • Parents can remove caregivers (204)
  • Parents cannot remove themselves (400)
  • Caregivers cannot remove anyone (403)
  • Removed member can no longer access family's children (returns 404, not 403 — no information disclosure)
  • Events logged by removed member are preserved

Invites

  • Parents can create invite with role selection (201)
  • Caregivers cannot create invites (403)
  • Invite token is 22-char base64url (128 bits entropy)
  • Token is SHA-256 hashed before storage; raw returned once
  • join_url uses BASE_URL from environment
  • Invite generation is idempotent (returns existing unused non-expired invite with same role)
  • Expired invite is skipped; new one generated
  • Invite expires after 7 days
  • Accept creates FamilyMember with specified role (201)
  • Accept marks ShareLink as used (used_at, used_by)
  • Expired token returns 404
  • Used token returns 404
  • Invalid token returns 404
  • Already a member returns 409
  • Own invite returns 400
  • Rate limited at 5 req/min per IP

Children (Modified)

  • POST /families/:familyId/children creates child in family (201)
  • Only parents can add children (403 for caregivers)
  • GET /children lists children across all user's families
  • Children include family_id and family_name in response
  • Existing tracking routes work with family-based access verification
  • verifyChildAccess checks FamilyMember instead of ChildAccess

iOS

  • New user onboarding: Register → Family+Child setup (one screen) → Dashboard
  • Family name defaults to "{First Name}'s Family"
  • Invited user (new): Register → auto-accept → shared family dashboard
  • Invited user (existing): auto-accept → banner + navigate to family
  • Universal link handles /join/{token} pattern
  • Pending token stored in UserDefaults for deferred accept
  • Settings shows family management (name, members, children)
  • Parents see invite/remove/edit controls; caregivers don't
  • Invite creation: role picker → create link → show with Copy + Share
  • Share sheet pre-fills "Join {Family} on Baby Basics! {url}"
  • Family switching via Settings (multi-family support)
  • Active family stored in @AppStorage

Boundaries (NOT in MVP)

  • Per-child access restrictions (post-MVP: additive ChildRestriction table)
  • Viewer role (read-only access)
  • Transfer family ownership
  • PIN/OTP verification on invite accept
  • Configurable invite expiry (fixed 7 days)
  • Multi-server account support (single server for MVP)
  • Email notifications on invite accept/member removal
  • Multiple active invites per family (one unused per role)
  • Leave family (self-removal, vs being removed by parent)

Test Cases

Family CRUD

  1. Create family → 201, user becomes parent member
  2. List families → returns all families with role and counts
  3. Get family details → includes members and children
  4. Update name (parent) → 200
  5. Update name (caregiver) → 403
  6. Delete family (parent) → 204, cascades everything
  7. Delete family (caregiver) → 403
  8. Non-member access → 403

Member Management

  1. List members → returns all with roles, sorted by joined_at
  2. Remove caregiver (parent) → 204
  3. Remove parent (parent) → 204 (can remove other parents)
  4. Remove self → 400
  5. Remove (caregiver attempting) → 403
  6. Removed user cannot access children → 403
  7. Removed user's events preserved in timeline

Invite Flow

  1. Create invite as parent → 201, join_url with token
  2. Create invite as caregiver → 403
  3. Idempotent: create same role invite twice → same token returned
  4. Create invite with different role → new token (both can coexist)
  5. Accept valid invite → 201, FamilyMember created with correct role
  6. Accept expired invite → 404
  7. Accept used invite → 404
  8. Accept invalid token → 404
  9. Accept own invite → 400
  10. Accept when already member → 409
  11. Token hash matches SHA-256 of raw token
  12. Expired invite regeneration: after old expires, new one created

Children Under Family

  1. Create child in family (parent) → 201
  2. Create child (caregiver) → 403
  3. List children → shows children across all families
  4. Existing CRUD/tracking routes work with family-based access
  5. verifyChildAccess correctly resolves family membership

Onboarding

  1. New user → register + setup → family and child created
  2. Invited user (new) → register → auto-accept → lands on shared family
  3. Invited user (existing) → accept → joins family
  4. Failed accept (expired) → friendly error, continues to normal onboarding

Migration

  1. Existing users with children → auto-migrated to families
  2. Existing caregivers → migrated to family members
  3. All tracking data preserved and accessible after migration