# RecordBridge CoParent — build spec

A court-ready co-parenting messenger where basic messages and shared calendar access stay free, while paid records tools focus on dependable search, export packets, and professional sharing.

## Project context

This spec describes an independent, alternative Android app you are building from scratch to compete with an existing incumbent app on Google Play — not a modification, clone, or reskin of the incumbent's own code, assets, or branding. Use the incumbent only as a market reference (via the report data below), and design working_name/package_id/store_listing/design_system so the result is clearly its own product.

## Incumbent app

- **Name:** AppClose: Co-Parent Essentials
- **Package id:** `com.appclose.androidapp`
- **Google Play:** https://play.google.com/store/apps/details?id=com.appclose.androidapp
- **appy.fyi report:** https://appy.fyi/report/com.appclose.androidapp
- **Category:** Parenting

## Overview

- **Working name:** RecordBridge CoParent (trademark cleared: no)
- **Package id:** `fyi.appy.recordbridgecoparent`
- **Min / target SDK:** 26 / 35
- **Backend:** firebase
- **Estimated build time:** 16 weeks
- **Pricing:** subscription, $9 via `revenuecat`
- **Runtime AI:** yes (ai_gateway_llm): Semantic records search over message, calendar, and audit history for paid records users ≈ $0.01/call; Neutral chronology draft for a selected court-record export packet ≈ $0.05/call
- **Permissions:** `INTERNET`, `POST_NOTIFICATIONS`

## Non-goals (out of scope for v1)

- Real-time voice calling and video calling are out of scope for v1; v1 focuses on reliable asynchronous messaging, attachments, read state, calendar, search, and records.
- No child-support payments, expense reimbursement, escrow, or other financial transfer features.
- No legal advice, court filing, lawyer marketplace, or representation workflow.
- No attempt to clone a full co-parenting suite with pets, chores, check-ins, or broad family management modules.
- No offline-only mode; the app requires Firebase-backed accounts and sync because court records, invites, notifications, billing, and exports need server-side state.
- No claim that exports are court-certified until a human legal review approves the wording and workflow.

## Tech stack

- **Language / UI:** Kotlin, Jetpack Compose
- **Kotlin:** 2.0.21
- **Compose BOM:** 2024.10.01
- **Gradle:** 8.9

| Purpose | Gradle coordinate |
| --- | --- |
| Android Kotlin extensions and core compatibility APIs | `androidx.core:core-ktx:1.13.1` |
| Compose Activity host for MainActivity | `androidx.activity:activity-compose:1.9.3` |
| Compose dependency version alignment | `androidx.compose:compose-bom:2024.10.01` |
| Material 3 Compose components | `androidx.compose.material3:material3:1.3.0` |
| Compose UI primitives | `androidx.compose.ui:ui:1.7.5` |
| Compose tooling preview and debug inspection | `androidx.compose.ui:ui-tooling:1.7.5` |
| Compose Navigation graph | `androidx.navigation:navigation-compose:2.8.3` |
| Lifecycle-aware ViewModel state collection in Compose | `androidx.lifecycle:lifecycle-runtime-compose:2.8.6` |
| Compose ViewModel integration | `androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6` |
| Kotlin coroutines on Android | `org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0` |
| Concrete Instant and date/time types for messages, calendar events, and exports | `org.jetbrains.kotlinx:kotlinx-datetime:0.6.1` |
| Firebase Authentication for parent accounts | `com.google.firebase:firebase-auth-ktx:23.1.0` |
| Cloud Firestore for circles, messages, calendars, audit events, subscriptions, waivers, and export metadata | `com.google.firebase:firebase-firestore-ktx:25.1.1` |
| Firebase Cloud Storage for message attachments and generated export PDFs | `com.google.firebase:firebase-storage-ktx:21.0.1` |
| Firebase Cloud Messaging for message, calendar, and request notifications | `com.google.firebase:firebase-messaging-ktx:24.0.3` |
| Firebase Cloud Functions callable client for AI search, export chronology drafting, billing diagnostics, waiver admin actions, and support escalation | `com.google.firebase:firebase-functions-ktx:21.1.0` |
| Firebase App Check Play Integrity provider to reduce abuse of account, message, and export APIs | `com.google.firebase:firebase-appcheck-playintegrity:18.0.0` |
| RevenueCat subscription entitlement handling for the paid records plan | `com.revenuecat.purchases:purchases:8.9.0` |
| Image loading for attachment thumbnails from Firebase Storage download URLs | `io.coil-kt:coil-compose:2.7.0` |
| AndroidX test runner for instrumented tests | `androidx.test:runner:1.6.2` |
| Compose UI test APIs | `androidx.compose.ui:ui-test-junit4:1.7.5` |
| JUnit 4 unit tests | `junit:junit:4.13.2` |

## Design system

- **Primary color:** `#1B6E5C`
- **Background color:** `#F7F3EC`
- **Error color:** `#B3261E`
- **Typography:** Material 3 default type scale, no custom font
- **Launcher icon glyph:** Phosphor `chats` (regular weight)
- **Theme notes:** Use Material 3 light and dark themes. Light theme uses #F7F3EC background, #1B6E5C primary, #2F3A37 text on light surfaces, and #FFFFFF cards. Dark theme uses #101816 background, #7FD8C1 primary, #E3E8E5 text, and #18221F cards. Use rounded 16dp cards, 8dp spacing grid, and high-contrast status chips for free core access, records entitlement, waiver status, and export progress.

## Screens

### Auth
- **Route:** `auth`
- **Purpose:** Create or sign in to a secure parent account before accessing circles, messages, calendar, or records.
- **Reached via:** app launch when FirebaseAuth.currentUser is null; tap Sign out on SettingsSupportDiagnostics
- **Key UI elements:** App logo and title; Email text field; Password text field; Create account button; Sign in button; Password reset link; Inline Firebase Auth error banner
- **States:** signed_out, submitting, error

### CircleSetup
- **Route:** `circles`
- **Purpose:** Let a signed-in user create or join a co-parenting circle and manage parent, professional, and admin roles.
- **Reached via:** app launch after sign-in when user has no active circle; tap circle switcher from Home; tap Manage circle from SettingsSupportDiagnostics
- **Key UI elements:** Current circles list; Create circle button; Invite code entry field; Pending invites list; Member role chips; Copy invite link button; Empty state explaining that messaging starts after a circle exists
- **States:** loading, empty, populated, error, submitting

### Home
- **Route:** `home/{circleId}`
- **Purpose:** Top-level dashboard for the selected circle, showing free core access status, unread messages, upcoming calendar items, and records-plan status.
- **Reached via:** app launch after sign-in with an active circle; select a circle on CircleSetup
- **Key UI elements:** Free core access banner; Unread message count card; Upcoming calendar card; Records plan status card; Bottom navigation bar for Messages, Calendar, Records, Settings; Notification permission prompt card when POST_NOTIFICATIONS is not granted
- **States:** loading, populated, error

### MessageThread
- **Route:** `messages/{circleId}`
- **Purpose:** Free core messaging screen with immutable audit logging, attachments, read state, and stable scrolling history.
- **Reached via:** tap Messages tab on Home; tap a message notification; tap a search result on RecordsSearch
- **Key UI elements:** Message list grouped by date; Sender name and timestamp on each message; Read receipt indicator; Attachment thumbnail row; Message composer text field; Attachment picker button using Android Photo Picker; Send button; Offline/sending/error status chips; Jump to date button
- **States:** loading_initial, empty, populated, loading_older, sending, attachment_uploading, error

### SharedCalendar
- **Route:** `calendar/{circleId}`
- **Purpose:** Free shared calendar for parenting events and request workflows with notifications.
- **Reached via:** tap Calendar tab on Home; tap a calendar notification; tap upcoming calendar card on Home
- **Key UI elements:** Month header; Calendar list grouped by date; Add event button; Request change button; Pending requests section; Event detail bottom sheet; Accept request button; Decline request button
- **States:** loading, empty, populated, submitting, error

### RecordsSearch
- **Route:** `records/{circleId}/search`
- **Purpose:** Paid-records entry point for dependable message history retrieval by exact keyword, date range, participant, attachment presence, and AI semantic query.
- **Reached via:** tap Records tab on Home; tap Search by date from MessageThread; return from BillingRecordsPlan after entitlement becomes active
- **Key UI elements:** Records plan paywall card when entitlement is missing; Keyword search field; Semantic search toggle; Start date picker; End date picker; Participant filter chips; Has attachment filter chip; Search button; Search result list with matched snippet, sender, timestamp, and source type; Add result to export packet checkbox
- **States:** locked_free_tier, loading, empty, populated, ai_processing, error

### ExportBuilder
- **Route:** `records/{circleId}/export`
- **Purpose:** Build a court-record packet from selected messages, calendar events, attachments, immutable audit events, and an AI-drafted chronology.
- **Reached via:** tap Build export from RecordsSearch; tap Records plan status card on Home when entitlement is active
- **Key UI elements:** Selected record count; Date range summary; Include attachments checkbox; Include read receipts checkbox; Include audit trail checkbox; Generate AI chronology button; Chronology preview editor; Generate PDF button; Export progress indicator; Open PDF button; Share with professional button
- **States:** locked_free_tier, empty_selection, editing, ai_processing, generating_pdf, generated, error

### BillingRecordsPlan
- **Route:** `billing/{circleId}`
- **Purpose:** Subscribe to the $9/month records plan, confirm entitlement state, show cancellation route, and surface diagnostics so payment bugs do not block core contact.
- **Reached via:** tap Records plan status card on Home; tap locked paid feature on RecordsSearch; tap Billing diagnostics from SettingsSupportDiagnostics
- **Key UI elements:** $9/month records plan card; Subscribe button; Restore purchases button; Current RevenueCat entitlement status; Core contact remains free explanation; Cancellation instructions button; Payment diagnostics panel; Contact support button
- **States:** loading, free_core, purchase_in_progress, entitled, billing_error, diagnostics_error

### WaiverStatus
- **Route:** `waiver/{circleId}`
- **Purpose:** Show and submit financial-assistance waiver requests without making free messaging or free calendar access dependent on approval.
- **Reached via:** tap Waiver status from BillingRecordsPlan; tap Waiver status from SettingsSupportDiagnostics
- **Key UI elements:** Waiver status chip; Explanation that basic messages and calendar are free; Waiver request text field; Submit waiver button; Submitted timestamp; Admin response text
- **States:** loading, not_submitted, submitting, pending, approved, denied, error

### SettingsSupportDiagnostics
- **Route:** `settings/{circleId}`
- **Purpose:** Expose account settings, notification health, billing diagnostics, cancellation route, data export support, and human escalation.
- **Reached via:** tap Settings tab on Home; tap Contact support from BillingRecordsPlan
- **Key UI elements:** Signed-in email; Notification permission status row; FCM token registration status row; Billing entitlement status row; Open cancellation instructions button; Open support ticket button; Manage circle button; Privacy policy link; Sign out button
- **States:** loading, populated, support_submitting, error

### WaiverReviewAdmin
- **Route:** `admin/waivers`
- **Purpose:** Minimal admin screen for users with an admin custom claim to review waiver requests and support escalations tied to billing lockout complaints.
- **Reached via:** tap hidden Admin row in SettingsSupportDiagnostics when Firebase custom claim admin=true
- **Key UI elements:** Pending waiver list; User and circle identifiers; Submitted explanation; Approve button; Deny button; Support escalation list; Mark contacted button
- **States:** not_authorized, loading, empty, populated, submitting, error

## Data model

### UserProfile (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| uid | `String` | primary key; same as FirebaseAuth uid |
| email | `String` | required |
| displayName | `String` | required |
| createdAt | `Instant` | server timestamp |
| fcmToken | `String?` | nullable; latest device token for notifications |

### CoParentCircle (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| name | `String` | required |
| createdByUid | `String` | foreign key to UserProfile.uid |
| createdAt | `Instant` | server timestamp |
| archived | `Boolean` | false for active circles |

### CircleMember (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | compound logical key circleId_uid |
| circleId | `String` | foreign key to CoParentCircle.id |
| uid | `String` | foreign key to UserProfile.uid |
| role | `String` | one of parent, professional, admin |
| joinedAt | `Instant` | server timestamp |
| active | `Boolean` | false removes access without deleting audit history |

### CircleInvite (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id; also used as invite code |
| circleId | `String` | foreign key to CoParentCircle.id |
| createdByUid | `String` | foreign key to UserProfile.uid |
| roleToGrant | `String` | one of parent, professional |
| createdAt | `Instant` | server timestamp |
| expiresAt | `Instant` | invite cannot be accepted after this time |
| acceptedByUid | `String?` | nullable until accepted |

### Message (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| circleId | `String` | foreign key to CoParentCircle.id |
| senderUid | `String` | foreign key to UserProfile.uid |
| body | `String` | required; store immutable text as sent |
| createdAt | `Instant` | server timestamp; primary sort key descending |
| clientCreatedAt | `Instant` | device timestamp for diagnostics only |
| hasAttachments | `Boolean` | true when at least one Attachment exists |
| deleted | `Boolean` | v1 never hard-deletes; true only hides from normal thread if legally approved later |

### Attachment (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| messageId | `String` | foreign key to Message.id |
| circleId | `String` | foreign key to CoParentCircle.id |
| storagePath | `String` | Firebase Storage path |
| contentType | `String` | MIME type from upload |
| sizeBytes | `Long` | uploaded file size |
| uploadedAt | `Instant` | server timestamp |

### ReadReceipt (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | compound logical key messageId_uid |
| messageId | `String` | foreign key to Message.id |
| circleId | `String` | foreign key to CoParentCircle.id |
| uid | `String` | reader uid |
| readAt | `Instant` | server timestamp |

### CalendarEvent (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| circleId | `String` | foreign key to CoParentCircle.id |
| title | `String` | required |
| notes | `String` | empty string when no notes |
| startsAt | `Instant` | required |
| endsAt | `Instant` | required |
| createdByUid | `String` | foreign key to UserProfile.uid |
| createdAt | `Instant` | server timestamp |
| status | `String` | one of active, cancelled |

### CalendarRequest (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| circleId | `String` | foreign key to CoParentCircle.id |
| eventId | `String?` | nullable; set when request modifies an existing event |
| requestedByUid | `String` | foreign key to UserProfile.uid |
| requestType | `String` | one of create, change, cancel |
| proposedTitle | `String` | required for create/change |
| proposedStartsAt | `Instant` | required for create/change |
| proposedEndsAt | `Instant` | required for create/change |
| status | `String` | one of pending, accepted, declined |
| resolvedByUid | `String?` | nullable until accepted or declined |
| resolvedAt | `Instant?` | nullable until accepted or declined |

### AuditEvent (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| circleId | `String` | foreign key to CoParentCircle.id |
| actorUid | `String` | uid responsible for the event |
| eventType | `String` | examples: message_sent, attachment_uploaded, message_read, event_created, request_accepted, export_generated, subscription_restored, waiver_submitted |
| targetType | `String` | examples: message, attachment, calendar_event, calendar_request, export_packet |
| targetId | `String` | id of target entity |
| createdAt | `Instant` | server timestamp |
| metadataJson | `String` | compact JSON string with non-sensitive diagnostics |

### SubscriptionStatus (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| uid | `String` | primary key; same as UserProfile.uid |
| recordsPlanActive | `Boolean` | mirrored from RevenueCat entitlement |
| revenueCatCustomerId | `String` | required after first app launch with RevenueCat |
| lastCheckedAt | `Instant` | server timestamp |
| lastErrorCode | `String?` | nullable; shown in billing diagnostics |

### WaiverApplication (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| uid | `String` | foreign key to UserProfile.uid |
| circleId | `String` | foreign key to CoParentCircle.id |
| requestText | `String` | user-provided waiver explanation |
| status | `String` | one of pending, approved, denied |
| submittedAt | `Instant` | server timestamp |
| reviewedByUid | `String?` | nullable until reviewed by admin |
| reviewedAt | `Instant?` | nullable until reviewed |
| adminResponse | `String` | empty string until reviewed |

### ExportPacket (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| circleId | `String` | foreign key to CoParentCircle.id |
| createdByUid | `String` | foreign key to UserProfile.uid |
| startAt | `Instant` | packet date range start |
| endAt | `Instant` | packet date range end |
| includedMessageIds | `List<String>` | selected Message ids |
| includedCalendarEventIds | `List<String>` | selected CalendarEvent ids |
| includeAttachments | `Boolean` | controls whether attachment references are included |
| includeReadReceipts | `Boolean` | controls whether read receipt section is included |
| aiChronologyDraft | `String` | empty until generated; user-editable before PDF generation |
| pdfStoragePath | `String?` | nullable until PDF is generated |
| status | `String` | one of draft, generating, generated, error |
| createdAt | `Instant` | server timestamp |

### SearchDocument (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | same as sourceId for message and calendar documents |
| circleId | `String` | foreign key to CoParentCircle.id |
| sourceType | `String` | one of message, calendar_event, audit_event |
| sourceId | `String` | id of source entity |
| plainText | `String` | normalized text used for exact keyword and AI retrieval |
| createdAt | `Instant` | timestamp of source entity |
| participantUid | `String` | sender or creator uid |
| hasAttachment | `Boolean` | true for source messages with attachments |

### SupportTicket (`firestore`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | Firestore document id |
| uid | `String` | foreign key to UserProfile.uid |
| circleId | `String` | foreign key to CoParentCircle.id |
| category | `String` | one of billing_locked_out, export_problem, notification_problem, account_access, other |
| body | `String` | user-provided support request |
| diagnosticsJson | `String` | compact JSON with entitlement, app version, notification permission, and last Firebase error code |
| status | `String` | one of open, contacted, closed |
| createdAt | `Instant` | server timestamp |

## Features

### Secure accounts, circles, invites, and roles

Users can create accounts, create or join co-parenting circles, invite another parent or professional, and receive role-limited access.

- **Answers complaint:** baseline parity

- **Screens:** Auth, CircleSetup, Home, SettingsSupportDiagnostics, WaiverReviewAdmin

- **Estimated hours:** 80

**Implementation notes:** Use Firebase Authentication email/password for account creation, sign-in, password reset, and sign-out. After sign-in, create UserProfile at users/{uid}. Store circles at circles/{circleId}, members at circles/{circleId}/members/{uid}, and invites at circleInvites/{inviteId}. Generate invite ids with Firestore auto ids, store expiresAt as now plus 14 days, and accept an invite only in a Firebase callable transaction that verifies the invite exists, is unexpired, and has acceptedByUid == null. Roles are string values parent, professional, and admin. Parent members can read and write messages/calendar for their circle. Professional members can read only records explicitly shared through export packet links. Admin access to WaiverReviewAdmin is controlled by Firebase custom claim admin=true and must not be granted from the client.

**Acceptance criteria:**
- A new user can create an account with email, password, and display name and is routed to CircleSetup when no circle exists.
- A parent can create a circle and sees it in the populated CircleSetup state without restarting the app.
- A second signed-in user can enter a valid invite code and becomes an active CircleMember with role parent.
- An expired invite returns an inline error and does not create a CircleMember.
- A signed-in non-admin user navigating to admin/waivers sees the not_authorized state.

### Free core messaging that cannot be blocked by records billing

Parents can always send, receive, and read basic text messages in their circle without subscribing to the records plan.

- **Answers complaint:** Paywalled communication

- **Screens:** Home, MessageThread, BillingRecordsPlan, SettingsSupportDiagnostics

- **Estimated hours:** 70

**Implementation notes:** Keep MessageThread outside all records-plan entitlement checks. The send action writes a Message document and a matching AuditEvent in a Firestore batch; it must never call RevenueCat or SubscriptionStatus before sending. Firestore security rules should allow active parent members to create Message documents regardless of SubscriptionStatus.recordsPlanActive. The UI shows a persistent Home banner stating that messages and calendar remain free. If RevenueCat is unavailable, BillingRecordsPlan may show diagnostics_error, but MessageThread must remain usable as long as Firebase Auth and Firestore are available.

**Acceptance criteria:**
- With SubscriptionStatus.recordsPlanActive=false, a parent can send a text message and the other parent receives it in MessageThread.
- With RevenueCat initialization forced to fail, MessageThread still loads and sending a text message still attempts a Firestore write.
- Tapping Messages from Home never opens BillingRecordsPlan or a paywall.
- A sent message creates exactly one AuditEvent with eventType message_sent and targetId equal to the Message.id.

### Audit-logged messaging, attachments, read state, and stable history scrolling

Messages, attachments, read receipts, and older history load predictably with immutable audit events for later records use.

- **Answers complaint:** Reliability and usability gaps

- **Screens:** MessageThread, RecordsSearch, ExportBuilder

- **Estimated hours:** 90

**Implementation notes:** Use a reverse-chronological Firestore query on circles/{circleId}/messages ordered by createdAt descending with limit 50 for the initial page. For older history, call startAfter(lastVisibleSnapshot) and append the next 50 messages while preserving scroll position with LazyColumn keys set to Message.id. Use Android Photo Picker via ActivityResultContracts.PickVisualMedia so READ_MEDIA_IMAGES is not required. Upload selected media to Firebase Storage path circles/{circleId}/messages/{messageId}/{attachmentId}; after upload success, write Attachment and update Message.hasAttachments=true in a batch. When a message becomes visible for the signed-in user and senderUid != current uid, write ReadReceipt messageId_uid with server timestamp using set with merge so repeated visibility does not duplicate records. Never mutate Message.body after creation; correction or deletion workflows are not in v1.

**Acceptance criteria:**
- The first MessageThread load requests at most 50 messages ordered newest first and displays them grouped by local date.
- When the user scrolls to the top of the loaded list, the app loads the next page using the last document snapshot and does not duplicate message ids.
- Selecting an image with Android Photo Picker uploads it to Firebase Storage and displays a Coil-loaded thumbnail in the message row.
- Opening a message from another parent writes one ReadReceipt document for the current user and does not create duplicates on repeated opens.
- Attempting to edit an existing message body is impossible because no edit UI exists and the repository exposes no updateBody method.

### Free shared calendar, requests, and notifications

Parents can maintain a basic shared calendar and exchange event change requests without paying for the records plan.

- **Answers complaint:** Paywalled communication

- **Screens:** Home, SharedCalendar, SettingsSupportDiagnostics

- **Estimated hours:** 80

**Implementation notes:** Store CalendarEvent and CalendarRequest documents under circles/{circleId}. Active parent members can create CalendarEvent directly and can create CalendarRequest for create, change, or cancel actions. Accepting a request runs a callable function transaction: verify the resolver is an active parent, update CalendarRequest.status to accepted, create or update the CalendarEvent, and write AuditEvent request_accepted. Declining sets status to declined and writes AuditEvent request_declined. Register the device FCM token in UserProfile.fcmToken after notification permission is granted on Android 13+. Cloud Functions send FCM notifications to other active parent members when messages, calendar events, and requests are created or resolved. Calendar access must not check RevenueCat entitlement.

**Acceptance criteria:**
- With recordsPlanActive=false, a parent can create a CalendarEvent and it appears for the other parent.
- Creating a CalendarRequest shows it in the pending requests section for the other parent.
- Accepting a create request creates one CalendarEvent, changes the request status to accepted, and writes one AuditEvent.
- If POST_NOTIFICATIONS is denied, the calendar still works and SettingsSupportDiagnostics shows notification permission as not granted.
- When POST_NOTIFICATIONS is granted and fcmToken exists, creating a request triggers a notification to the other parent device in manual testing.

### Dependable records search with date filters and AI semantic retrieval

Paid records users can search history by date range, keyword, participant, attachment presence, and semantic meaning instead of relying on endless scrolling.

- **Answers complaint:** Court records are hard to retrieve

- **Screens:** RecordsSearch, MessageThread, ExportBuilder, BillingRecordsPlan, WaiverStatus

- **Estimated hours:** 85

**Implementation notes:** Gate RecordsSearch with records entitlement from RevenueCat or approved waiver; when locked, show locked_free_tier and route to BillingRecordsPlan. Maintain SearchDocument documents from messages, calendar events, and audit events through Cloud Functions triggered on source creation. Exact keyword search normalizes query and plainText to lowercase and tokenizes on non-letter/digit boundaries; filter client-side only within a server-limited date range of at most 366 days to avoid unbounded reads. Date search requires startAt and endAt and queries SearchDocument where circleId == current circle, createdAt >= startAt, createdAt <= endAt, ordered by createdAt descending. Participant and hasAttachment filters are Firestore where clauses. When Semantic search is enabled, call a Firebase callable function semanticSearch with circleId, query, startAt, endAt, and filters; the function calls the AI gateway to retrieve semantically related source ids and returns up to 50 ranked results with snippets. The Android client renders ai_processing until the callable returns and merges results by sourceId without duplicates.

**Acceptance criteria:**
- A free user opening RecordsSearch sees locked_free_tier and cannot run advanced search.
- An entitled user can search with only a date range and receives results sorted newest first without scrolling through MessageThread.
- Searching a 400-day date range shows a validation error requiring a range of 366 days or less.
- A keyword search for a lowercase term matches a message containing the same term with different capitalization.
- With Semantic search enabled, the UI shows ai_processing, calls semanticSearch once, and displays returned snippets with no duplicate source ids.

### Court-record export packets with PDF generation and AI chronology draft

Paid records users can turn selected messages, calendar items, attachments, read receipts, and audit events into a usable PDF packet.

- **Answers complaint:** Court records are hard to retrieve

- **Screens:** RecordsSearch, ExportBuilder, BillingRecordsPlan, WaiverStatus

- **Estimated hours:** 85

**Implementation notes:** Gate ExportBuilder with the same records entitlement or approved waiver used by RecordsSearch. Users select results in RecordsSearch and pass selected source ids through a shared ViewModel. ExportBuilder creates an ExportPacket draft with selected ids, date range, includeAttachments, and includeReadReceipts. The Generate AI chronology button calls draftChronology callable with exportPacketId; the function loads selected records in chronological order and asks the AI gateway for a neutral chronology draft using only record facts, no legal advice, and returns plain text. The user can edit aiChronologyDraft before PDF generation. Generate PDF uses Android graphics.pdf.PdfDocument on device: first page contains title, circle name, generation timestamp, date range, and disclaimer that it is a records packet; following pages list messages chronologically with sender, timestamp, body, attachment filenames, read receipts, and audit trail entries. Save the PDF to app cache, upload it to Firebase Storage at circles/{circleId}/exports/{exportPacketId}.pdf, update ExportPacket.pdfStoragePath, and write AuditEvent export_generated.

**Acceptance criteria:**
- A free user opening ExportBuilder sees locked_free_tier and cannot generate a PDF.
- An entitled user selecting three messages in RecordsSearch sees selected record count 3 in ExportBuilder.
- Generate AI chronology sets state ai_processing and stores non-empty aiChronologyDraft on success.
- Generate PDF creates a PDF with at least a title page and one chronological records page for a non-empty selection.
- After PDF generation, ExportPacket.status is generated, pdfStoragePath is non-null, and one AuditEvent with eventType export_generated exists.

### Records subscription, waiver flow, cancellation path, diagnostics, and admin review

The paid $9/month records plan unlocks advanced records tools while billing failures, cancellation confusion, and waiver status are visible and actionable.

- **Answers complaint:** Paid but still locked out

- **Screens:** BillingRecordsPlan, WaiverStatus, SettingsSupportDiagnostics, WaiverReviewAdmin, RecordsSearch, ExportBuilder

- **Estimated hours:** 65

**Implementation notes:** Use RevenueCat Purchases SDK with one monthly product representing the $9/month records plan and one entitlement named records_plan. On app start and after purchase/restore, read CustomerInfo and mirror entitlement status into SubscriptionStatus through a callable function so Firestore security rules and UI can use a server-written status. Never use the subscription to gate MessageThread or SharedCalendar. BillingRecordsPlan includes Subscribe, Restore purchases, entitlement status, cancellation instructions that deep-link to Google Play subscriptions with Intent.ACTION_VIEW and URI https://play.google.com/store/account/subscriptions, and a diagnostics panel showing RevenueCat customer id, entitlement active boolean, last checked time, and last error code. WaiverStatus writes WaiverApplication with status pending. WaiverReviewAdmin lists pending applications for admin users and approve/deny actions run callables that update status and write AuditEvent waiver_approved or waiver_denied. Approved waiver must be treated as records entitlement for RecordsSearch and ExportBuilder.

**Acceptance criteria:**
- A successful RevenueCat purchase changes BillingRecordsPlan state to entitled and SubscriptionStatus.recordsPlanActive=true after refresh.
- Restore purchases calls RevenueCat restorePurchases and updates the displayed entitlement without reinstalling.
- If RevenueCat returns an error, BillingRecordsPlan shows billing_error with the error code and MessageThread remains accessible.
- Tapping cancellation instructions opens the Google Play subscriptions URL.
- Submitting a waiver creates WaiverApplication.status=pending, and an admin approval lets the user access RecordsSearch without an active RevenueCat entitlement.

### Reliability, privacy, support escalation, and Play-launch hardening

The app exposes notification health, billing diagnostics, support escalation, privacy links, and tests for the failures that made the incumbent feel unreliable.

- **Answers complaint:** Reliability and usability gaps

- **Screens:** SettingsSupportDiagnostics, Home, MessageThread, SharedCalendar, RecordsSearch, ExportBuilder, BillingRecordsPlan

- **Estimated hours:** 85

**Implementation notes:** SettingsSupportDiagnostics reads notification permission through NotificationManagerCompat.areNotificationsEnabled and, on Android 13+, permission grant state for POST_NOTIFICATIONS. It reads the last stored UserProfile.fcmToken and displays whether the token is registered. Support tickets are written to SupportTicket with category, body, and diagnosticsJson containing app version, uid, circleId, RevenueCat entitlement active boolean, last billing error code, notification permission status, and last Firebase exception class. Use Crashlytics only if added later; v1 must not collect crash analytics because it is not in the dependency list or data disclosures. Add repository-level timeout handling with kotlinx.coroutines.withTimeout for Firestore calls that back explicit loading indicators: 10 seconds for search callables, 30 seconds for export generation, and no artificial timeout for real-time message listeners. Privacy policy link opens the suggested URL in a browser. Manual launch QA must verify free core access, notification permission, billing diagnostics, waiver flow, and export generation.

**Acceptance criteria:**
- SettingsSupportDiagnostics shows notification permission as granted or not granted based on the device setting.
- Opening a support ticket writes SupportTicket.category, body, diagnosticsJson, status=open, and createdAt.
- A semantic search callable taking longer than 10 seconds returns an error state instead of leaving an infinite spinner.
- An export generation operation taking longer than 30 seconds returns an error state with a retry button.
- The privacy policy link opens the configured privacy_policy_url in an external browser.

## Store listing

- **Title:** RecordBridge CoParent
- **Short description:** Free co-parent messages and calendar, paid court-ready records.
- **Category:** Parenting
- **Keywords:** co-parenting, parenting messenger, shared calendar, court records, message export, custody communication, parent communication, audit trail, records search
- **Icon prompt:** Create a modern Android app icon for a co-parenting records messenger named RecordBridge CoParent. Use a rounded square background in deep teal #1B6E5C with a clean white line-art chat-bubbles glyph that subtly includes a small document corner or record line inside one bubble. Style should be flat vector, Material-inspired, high contrast, no text, no scales of justice, no courthouse, no people faces, and readable at small launcher sizes.

**Long description:**

RecordBridge CoParent is a focused communication and records app for co-parents who need dependable access to messages, calendar history, and exportable documentation. Basic messages and shared calendar access stay free, so contact is not blocked by a records subscription. The paid records plan adds date search, semantic search, export packets, read receipts, audit trails, waiver support, billing diagnostics, and professional sharing workflows designed around retrieval and documentation.

## Legal

- **Regulated category:** legal_evidence
- **Privacy policy URL:** https://recordbridge.example.com/privacy (privacy claims verified: no)
- **Data collected:** Email address; Display name; Firebase user id; Co-parenting circle membership and roles; Invite codes and invite acceptance status; Messages; Message attachments and attachment metadata; Read receipts; Shared calendar events; Calendar requests and request resolution status; Immutable audit events; Search queries for records retrieval; Selected records and AI chronology drafts for export packets; Generated PDF export packet metadata and storage path; RevenueCat customer id and records-plan entitlement status; Waiver application text and status; Support ticket text and diagnostics; Notification permission status; Firebase Cloud Messaging token

## Test plan

### 1. Paywalled communication (instrumented)

1. Sign in as parent A with an active circle and set fake SubscriptionStatus.recordsPlanActive=false.
2. Navigate to home/{circleId}.
3. Tap the Messages tab.
4. Enter text 'Pickup is at 5 today' in the MessageThread composer.
5. Tap Send.
6. Query the fake Firestore repository for messages in the circle.

**Expected:** MessageThread does not show a paywall, one Message with body 'Pickup is at 5 today' is created, and one AuditEvent with eventType message_sent is created.

### 2. Paid but still locked out (instrumented)

1. Configure the fake RevenueCat wrapper to throw an initialization error.
2. Sign in as parent A with recordsPlanActive=false.
3. Open BillingRecordsPlan and observe the billing_error state.
4. Navigate back to Home.
5. Open MessageThread.
6. Send text 'Can you confirm homework folder?'

**Expected:** BillingRecordsPlan displays the RevenueCat error code, but MessageThread remains accessible and the message send operation is attempted against Firestore.

### 3. Court records are hard to retrieve (unit)

1. Create five SearchDocument objects with createdAt values spanning January 1 through March 1 and mixed-case plainText values.
2. Run the local exact keyword filter with query 'school' and date range January 15 through February 15.
3. Run the same filter with participantUid set to one matching sender.
4. Run the same filter with hasAttachment=true.

**Expected:** Only documents inside the date range whose normalized plainText contains 'school' are returned, participant filtering removes other senders, attachment filtering removes documents without attachments, and results are sorted newest first.

### 4. can't search anything it just buffs,won't load (instrumented)

1. Inject a semanticSearch callable fake that suspends longer than 10 seconds.
2. Open RecordsSearch as an entitled user.
3. Enable Semantic search.
4. Enter query 'missed pickup discussion'.
5. Tap Search.
6. Advance the test coroutine clock past 10 seconds.

**Expected:** RecordsSearch leaves ai_processing and shows an error state with a retry action instead of an infinite loading indicator.

### 5. search by date as well (instrumented)

1. Seed fake SearchDocument records on 2025-01-01, 2025-01-15, and 2025-02-01.
2. Open RecordsSearch as an entitled user.
3. Set start date to 2025-01-10.
4. Set end date to 2025-01-31.
5. Leave keyword empty.
6. Tap Search.

**Expected:** The result list contains the 2025-01-15 record only and does not require opening or scrolling MessageThread.

### 6. Court records are hard to retrieve (instrumented)

1. Seed three messages, one calendar event, two read receipts, and matching AuditEvent records in a fake repository.
2. Open RecordsSearch as an entitled user.
3. Select all three message results.
4. Tap Build export.
5. In ExportBuilder, enable Include read receipts and Include audit trail.
6. Tap Generate PDF.

**Expected:** ExportBuilder reaches generated state, the generated PDF file has non-zero bytes, ExportPacket.status is generated, pdfStoragePath is non-null, and an export_generated AuditEvent is written.

### 7. Reliability and usability gaps (manual)

1. Install the debug build on two Android 13 or newer devices.
2. Sign in as parent A on device 1 and parent B on device 2 in the same circle.
3. Grant POST_NOTIFICATIONS on device 2.
4. From device 1, create a CalendarRequest.
5. Wait up to 30 seconds on device 2.
6. Tap the notification on device 2.

**Expected:** Device 2 receives a calendar request notification, tapping it opens SharedCalendar for the correct circle, and the request appears in the pending requests section.

### 8. Paid but still locked out (manual)

1. Sign in as a test user with no active records entitlement.
2. Open BillingRecordsPlan.
3. Tap Restore purchases.
4. Tap cancellation instructions.
5. Return to the app and open SettingsSupportDiagnostics.
6. Create a support ticket with category billing_locked_out and body 'Paid but still cannot access records'.

**Expected:** Restore purchases completes or shows a specific error, cancellation instructions open Google Play subscriptions in a browser or Play Store, and a SupportTicket with category billing_locked_out and diagnosticsJson is created.

### 9. baseline parity (manual)

1. Sign in as parent A and create a circle.
2. Generate an invite code.
3. Sign in as parent B on another device.
4. Accept the invite code.
5. Parent A sends a text message.
6. Parent B opens MessageThread and reads it.

**Expected:** Both users remain in the same circle, parent B sees the message, and a ReadReceipt for parent B is created.

## Build instructions

```sh
./gradlew --no-daemon clean
./gradlew --no-daemon testDebugUnitTest
./gradlew --no-daemon connectedDebugAndroidTest
./gradlew --no-daemon :app:bundleRelease -Pandroid.injected.signing.store.file=$PWD/upload-keystore.jks -Pandroid.injected.signing.store.password=$ANDROID_KEYSTORE_PASSWORD -Pandroid.injected.signing.key.alias=recordbridge-upload -Pandroid.injected.signing.key.password=$ANDROID_KEY_PASSWORD
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
- `regulated_category_go_no_go`
