Feature: Share Links (Multi-Caregiver Access)
Version: 1.2.0 Last Reviewed: 2026-02-25 Status: Approved
User Story
As a parent, I can share a link with my partner/nanny so they can also track our baby.
MVP Scope
- Generate a share link for a child (unique token URL)
- Anyone with the link can create an account (or link existing) and get access
- All shared users have equal read/write access (no role differences for data operations)
- Owner can revoke access
- List who has access to a child
NOT in MVP
- Role-based permissions (Post-MVP #9)
- Invite expiration (Post-MVP)
- Email invites (Post-MVP)
- Transfer ownership (Post-MVP)
- Share link analytics (view count, click tracking)
- Multiple active share links per child
API Contract
Generate Invite Link
POST /api/v1/children/:childId/invites
Authorization: Bearer <token>
Request: (empty body)
Response 201:
{
"invite": {
"id": "uuid",
"share_url": "https://BASE_URL/share/a1b2c3d4e5f6...",
"token": "a1b2c3d4e5f6...",
"created_at": "2026-02-10T12:00:00.000Z"
}
}
Note: The share_url is built from the server's BASE_URL environment variable.
Format: {BASE_URL}/share/<token>
Self-hosters get correct URLs automatically.
Token is generated using crypto.randomBytes(32).toString('hex') (64 hex chars).
The iOS app detects this URL pattern, extracts the token, and calls the accept endpoint.
If an unused invite already exists for this child, returns it (idempotent).
Accept Invite
POST /api/v1/invites/accept
Authorization: Bearer <token>
Note: This is intentionally NOT nested under /children/:childId because the
accepting user doesn't know the childId — they only have the invite token.
The server resolves childId from the token internally.
Request:
{
"token": "a1b2c3d4e5f6..."
}
Response 201:
{
"child": {
"id": "uuid",
"name": "Baby Bretz",
"date_of_birth": "2026-03-15",
"role": "caregiver",
"created_at": "2026-01-15T08:00:00.000Z",
"updated_at": "2026-01-15T08:00:00.000Z"
},
"granted_by": {
"name": "Johnny",
"email": "parent@example.com"
}
}
Response 409 (already have access):
{ "error": { "code": "CONFLICT", "message": "You already have access to this child", "details": [] } }
Response 404 (invalid, used, or non-existent token):
{ "error": { "code": "NOT_FOUND", "message": "Invalid or expired invite link", "details": [] } }
Response 400 (accepting own invite):
{ "error": { "code": "VALIDATION_ERROR", "message": "Cannot accept your own invite link", "details": [] } }
Note: After successful acceptance:
- Creates a ChildAccess record with role "caregiver"
- Marks the ShareLink as used (sets used_at and used_by)
- Returns the full child object with the new user's role
- 201 because a ChildAccess resource is created
List Users with Access
GET /api/v1/children/:childId/access
Authorization: Bearer <token>
Response 200:
{
"access": [
{
"user_id": "uuid-owner",
"name": "Johnny",
"email": "parent@example.com",
"role": "owner",
"granted_at": "2026-01-15T08:00:00.000Z"
},
{
"user_id": "uuid-caregiver",
"name": "Maria",
"email": "nanny@example.com",
"role": "caregiver",
"granted_at": "2026-02-10T12:30:00.000Z"
}
],
"count": 2
}
Note: Returns all users with ChildAccess records for this child, sorted by granted_at ascending (oldest first).
Revoke User Access
DELETE /api/v1/children/:childId/access/:userId
Authorization: Bearer <token>
Response 204: (No Content)
Response 400 (owner trying to revoke self):
{ "error": { "code": "VALIDATION_ERROR", "message": "Cannot revoke your own access. Delete the child instead.", "details": [] } }
Response 403 (non-owner attempting revocation):
{ "error": { "code": "FORBIDDEN", "message": "Only the owner can revoke access", "details": [] } }
Response 404 (user not found or no access):
{ "error": { "code": "NOT_FOUND", "message": "User access not found", "details": [] } }
Note: Deletes the ChildAccess record for the specified user.
After revocation, the user can no longer see or log data for that child.
Error Responses
Standard error format. Error codes: 400 VALIDATION_ERROR, 401 UNAUTHORIZED, 403 FORBIDDEN, 404 NOT_FOUND, 409 CONFLICT
Business Rules
Invite Link Generation
- Any user with access to a child (owner or caregiver) can generate an invite link.
- Only one unused invite link can exist at a time per child.
- If an unused invite link already exists, return it (idempotent). Same 201 response.
- If all existing links are used, generate a new one.
- Token is 64 hex characters (
crypto.randomBytes(32).toString('hex')). - Share URL format:
{BASE_URL}/share/<token>where BASE_URL is from server environment. - ShareLink record stores: child_id, token, created_by, used_at (null), used_by (null).
Invite Link Acceptance
- Invite links are single-use only.
- Once accepted (used_at is set), the token cannot be reused.
- Attempting to reuse a token returns 404 (same as invalid token - don't reveal that the token existed).
- User cannot accept an invite for a child they already have access to (409 CONFLICT).
- User cannot accept their own invite link (400 VALIDATION_ERROR).
- Accepting creates a ChildAccess record with role "caregiver".
- The ShareLink is marked as used: used_at = current timestamp, used_by = accepting user's ID.
- ShareLink records remain in the database after use (audit trail) but cannot be reused.
Access Control
- Any user with a ChildAccess record (owner or caregiver) can read and write all data for that child.
- In MVP, all caregivers have equal permissions for data operations (feedings, diapers, sleep, notes).
- Only the owner can revoke another user's access.
- Only the owner can delete the child entirely.
- Owner cannot revoke their own access (must delete the child instead).
Access Revocation
- Revocation immediately deletes the ChildAccess record.
- After revocation, the user can no longer: see the child in their list, access child details, create or view tracking data, generate invite links.
- Existing tracking data created by the revoked user remains (created_by still references them).
- AuditLog entries for the revoked user's actions remain intact.
- ShareLink records created by the revoked user remain (not deleted on revocation).
Cascade on Child Deletion
- When a child is deleted (owner only), all related records are cascade-deleted: ChildAccess, ShareLink, Feeding, Diaper, Sleep, Note.
- AuditLog entries remain (they reference user_id, not child_id).
Audit Logging
- AuditLog entry on invite generation (entity_type: "share_link", action: "create").
- AuditLog entry on invite acceptance (entity_type: "share_link", action: "update" with used_at/used_by; also entity_type: "child_access", action: "create").
- AuditLog entry on access revocation (entity_type: "child_access", action: "delete").
Acceptance Criteria
- Any user with access can generate an invite link for a child
- Invite generation is idempotent (returns existing unused link if present)
- Share URL uses BASE_URL from server environment (not hardcoded)
- Token is 64 hex characters
- Can list all users with access (wrapped response with count)
- List includes user_id, name, email, role, and granted_at
- Can accept a valid, unused invite link (returns 201)
- Accepting creates ChildAccess with role "caregiver"
- Accepting marks the ShareLink as used (used_at and used_by set)
- Invite link is single-use (second acceptance returns 404)
- Cannot accept invite for child you already have access to (409)
- Cannot accept your own invite link (400)
- Owner can revoke another user's access (returns 204)
- Non-owner cannot revoke access (403)
- Owner cannot revoke their own access (400)
- After revocation, user cannot access child data
- Invalid/non-existent token returns 404
- Used token returns 404 (same as invalid)
- All operations create AuditLog entries
Test Cases
Invite Generation
- Generate link (first time): Owner creates invite → 201, {invite: {share_url, token, ...}}
- Generate link (idempotent): Create invite, don't accept, create again → returns same token
- Generate link (non-owner): Caregiver creates invite → 201
- Generate link (no access): User without access → 403
- Generate link (child not found): Invalid child ID → 404
- Generate after acceptance: Accept invite, generate new → new token created
- Share URL uses BASE_URL: Verify share_url starts with configured BASE_URL
- Audit log: AuditLog entry with action=create, entity_type=share_link
Invite Acceptance
- Accept valid invite: New user accepts → 201, {child: {..., role: "caregiver"}, granted_by: {email}}
- Verify access after accept: After accepting, GET /children → child appears in list
- Single-use: Accept, try same token again → 404
- Already have access: User with access tries to accept → 409
- Own invite: Owner generates, tries to accept → 400
- Invalid token: Random/non-existent token → 404
- Audit log: AuditLog entries for share_link update AND child_access create
List Access
- Owner + caregiver: Share and accept → list shows 2 users with correct roles, count: 2
- Owner only: New child → list shows 1 user (owner), count: 1
- No access: User without access → 403
- Multiple caregivers: Share with User B and C → list shows 3 users, count: 3
- Sorted by granted_at: Owner first (earliest), then caregivers in order
Revoke Access
- Owner revokes caregiver: → 204, ChildAccess deleted
- Verify removal: After revocation, caregiver GET /children → child not in list
- Verify data block: After revocation, caregiver GET /children/:id → 403
- Non-owner attempts revocation: Caregiver tries to revoke another caregiver → 403
- Owner revokes self: → 400
- User not found: Revoke non-existent user → 404
- Child not found: Invalid child ID → 404
- Audit log: AuditLog entry with action=delete, entity_type=child_access
Cascade
- Child delete cascades: Create child, share, delete child → all ShareLinks and ChildAccess deleted
- Audit logs survive: AuditLog entries remain after child deletion
Deferred Accept Flow (iOS-Only)
This flow handles the case where someone opens an invite link before they have an account. No API contract changes are needed — this is entirely iOS client logic.
Flow
- User taps a share URL (
{BASE_URL}/share/<token>). The iOS app handles this as a universal link. - The app checks whether the user is already authenticated.
- If authenticated: Call
POST /api/v1/invites/acceptimmediately with the token. Navigate to the newly shared child. - If not authenticated:
a. Store the invite token locally (e.g., in
UserDefaultsor an in-memory pending state). b. Show the registration/login screen with optional context: "You were invited to track a baby." c. After successful registration or login, check for a pending invite token. d. If found, automatically callPOST /api/v1/invites/acceptwith the stored token. e. Clear the stored token. Navigate to the newly shared child.
Notes
- The token is stored only until the first successful post-auth accept attempt.
- If accept fails (expired/used token), show a friendly error: "This invite link is no longer valid." Allow the user to continue normally.
- If the user dismisses the registration flow without completing it, the stored token remains available for the next session.
- The invite token should NOT be stored in Keychain — it is temporary and low-sensitivity.
Roles
Role Definitions
Three roles exist in the system. All role distinctions are enforced server-side by the ChildAccess.role field.
owner
- Created automatically when a user creates a child.
- Exactly one owner per child (cannot be transferred in MVP).
- Full access to all operations.
caregiver
- Assigned when a user accepts an invite link.
- Can read and write all tracking data (feedings, diapers, sleeps, notes).
- Can generate new invite links.
- Cannot manage access (revoke other users, view access list).
- Cannot delete the child.
viewer (Post-MVP — defined here for planning purposes)
- Would be assigned via a separate invite type (not implemented yet).
- Read-only access to all tracking data.
- Cannot create, update, or delete any entries.
- Cannot generate invite links or manage access.
- Cannot delete the child.
Permissions Matrix
| Operation | owner | caregiver | viewer (post-MVP) |
|---|---|---|---|
| View child details | yes | yes | yes |
| Create feedings / diapers / sleeps / notes | yes | yes | no |
| Update feedings / diapers / sleeps / notes | yes | yes | no |
| Delete feedings / diapers / sleeps / notes | yes | yes | no |
| View timeline / dashboard | yes | yes | yes |
| Generate invite link | yes | yes | no |
| List users with access | yes | no | no |
| Revoke user access | yes (others only) | no | no |
| Delete child | yes | no | no |
Current MVP Behavior
In MVP only owner and caregiver roles exist. All caregivers have equal data access. The permissions matrix row for "viewer" is documented for future reference but NOT enforced. The ChildAccess.role field stores "owner" or "caregiver" only.
Boundaries
- No role-based permissions (all caregivers have equal data access in MVP)
- No invite link expiration (links are permanent until used)
- No email invitations (link is shared manually via SMS, messaging, etc.)
- No transfer of ownership (owner role cannot be reassigned)
- No limit on number of caregivers per child
- No limit on number of children a user can have access to
- No invite link analytics (click tracking, view counts)
- Assumes children CRUD and authentication are already implemented
- iOS app must handle the share URL deep link and extract the token
- Viewer role is defined in this spec but NOT implemented in MVP