Software Requirements Specification v3.0

TEAM MOVE — Community Mobility Platform — June 2026 — CTO-reviewed, production-ready

Team Move — Software Requirements Specification

Version 3.0 | June 2026 | Confidential — Production Grade


Document Status: Official — Production Ready for External Mobile Development
Version: 3.0 (CTO-reviewed, implementation-ready)
Prepared by: Team Move CTO / Product Engineering
Target audience: Senior iOS / Android development team
Language: English (Professional Software Engineering)
Supersedes: SRS v1.0, v2.0


Table of Contents

  1. Introduction & Product Vision
  2. Functional Architecture
  3. User Roles & Permissions
  4. Authentication & Onboarding
  5. Community Hubs Module
  6. Events Module
  7. Mobility Coordination Module
  8. Journey Mutualization (Community Trip Board)
  9. Environmental Impact Module
  10. Discovery Module
  11. Notifications System
  12. Subscription & Billing
  13. Administration & Super Admin
  14. Rewards & Gamification
  15. API Specifications
  16. Database & Data Model
  17. iOS Requirements
  18. Android Requirements
  19. Security
  20. Performance & Scalability
  21. Accessibility
  22. Non-Functional Requirements
  23. GDPR & Privacy
  24. Error Handling Reference
  25. Development Backlog

1. Introduction & Product Vision

1.1 Document Purpose

This SRS v3.0 is the single authoritative specification governing the Team Move native mobile applications for iOS and Android. It is written to be implementation-complete: every section must be buildable without requiring functional clarification from the product team.

How to read this document:

  • Each module section is self-contained and cross-references others where dependencies exist.
  • Business rules are numbered within each module (e.g., BR-HUB-01) for traceability.
  • Every user flow is described as a numbered step sequence.
  • Edge cases and error states are explicitly enumerated — they are not optional.
  • Sections marked [SCREENSHOT REQUIRED] require the dev team to capture the equivalent web UI before mobile implementation.

1.2 Product Overview

Team Move is a community mobility platform that enables organizations and individuals to create community hubs, organize events, coordinate multimodal journeys, manage multiple transportation options, discover events, and monitor environmental impact.

| Dimension | Description | |-----------|-------------| | Social mobility | Community-first, trust-based carpooling between people who know each other | | Environmental accountability | ADEME-certified CO₂ calculations with RSE/CSR export-grade reporting | | Event logistics | Full participant lifecycle from invitation to post-event return journey | | Gamified engagement | Impact points, level system, badges, challenges, rewards shop | | B2B SaaS | Tiered subscription plans for structures with admin dashboard |

1.3 Scope of Mobile Application

In scope (must implement):

  • 100% of participant-facing features from the web platform
  • 100% of organizer event management features
  • 100% of community hub features (chat, DMs, trip board, events)
  • Native capabilities: push notifications, QR scanning, native share, GPS access

Out of scope for mobile v1.0 (web-only):

  • Super Admin full dashboard
  • PDF report generation (trigger via web; download link in app)
  • Admin bulk data operations
  • Stripe webhook processing
  • Business plan editor

1.4 Technical Context

The mobile app communicates exclusively with the Base44 backend-as-a-service platform. The mobile team does not build or manage any backend infrastructure. All persistence, auth, real-time, storage, and function invocation is via the Base44 SDK and REST API.

Base44 SDK: npm install @base44/sdk (React Native). For Swift/Kotlin native: implement HTTP calls against the Base44 REST API. Base44 will provide staging environment credentials.

1.5 Definitions & Acronyms

| Term | Definition | |------|-----------| | Hub | A community group within Team Move (equiv. "Communauté" in French UI) | | Trip Offer | CommunityTrip with type = "offer" — user offering seats | | Trip Request | CommunityTrip with type = "request" — user seeking transport | | Carpool | Event-linked (Carpool entity) ride-share linked to a specific event | | Shuttle | Organizer-managed collective vehicle for an event | | Impact Points | Gamification currency in ImpactPoint entity | | Structure | An organization owning a subscription | | Organizer | User whose email = event.organizer_email | | Hub Admin | CommunityMember with role = "admin" | | ADEME | Agence de la transition écologique — French official CO₂ reference | | RSE | Responsabilité Sociétale des Entreprises (CSR) | | RLS | Row-Level Security — server-side data access filter | | pseudo | Anonymous display name chosen by user when joining a Hub | | J-N | N days before event date | | APNs | Apple Push Notification service | | FCM | Firebase Cloud Messaging | | BR | Business Rule (numbered within each module) |


2. Functional Architecture

2.1 Module Map

Platform organized into four primary modules (Community Hubs, Events, Mobility Coordination, Impact) and four cross-cutting modules (Discovery, Notifications, Subscription & Billing, Administration). All communicate via the Base44 platform layer, which abstracts Entity API, Backend Functions, Auth, and Storage.

2.2 Reference Technology Stack

| Layer | Web (Reference) | Mobile Equivalent | |-------|----------------|------------------| | Frontend | React 18, Vite, TailwindCSS | React Native or Swift/Kotlin native | | State | TanStack Query v5 | React Query or platform equivalent | | BaaS | Base44 | Base44 REST API or SDK | | Backend functions | Deno (JavaScript) on Base44 | Server-side only — no mobile equivalent | | Payments | Stripe JS | Stripe Mobile SDK | | Maps | Google Maps JS API | Google Maps SDK for iOS/Android | | Auth | Base44 magic link | Base44 REST auth endpoints | | Push | Base44 sendPushNotification | APNs (iOS) + FCM (Android) | | Real-time | Base44 entity.subscribe() | WebSocket or long-poll |

2.3 Base44 API Communication Protocol

Base URL: Provided by Base44 during onboarding (staging + production endpoints).

Authentication headers:

Authorization: Bearer <session_token>
X-App-ID: <app_id>
Content-Type: application/json

Entity operations:

GET    /entities/{Name}              → list()
GET    /entities/{Name}?filter=...   → filter()
GET    /entities/{Name}/{id}         → get()
POST   /entities/{Name}              → create()
PUT    /entities/{Name}/{id}         → update()
DELETE /entities/{Name}/{id}         → delete()
POST   /entities/{Name}/bulk         → bulkCreate()

Backend function invocation:

POST /functions/{functionName}
Body: { ...params }
Response: { data: ..., status: 200 }

Standard HTTP status codes:

| Code | Meaning | |------|---------| | 200 | Success | | 201 | Created | | 400 | Bad Request (validation error) | | 401 | Unauthorized (no/invalid token) | | 403 | Forbidden (insufficient permissions) | | 404 | Not Found | | 409 | Conflict (duplicate) | | 429 | Rate Limited | | 500 | Internal Server Error |

Error response format:

{ "error": "human-readable message", "code": "ERROR_CODE", "statusCode": 400 }

2.4 Real-Time Architecture

Base44 entity subscriptions use SSE or WebSocket. Mobile implementation requirements:

  1. Establish subscription on screen focus.
  2. Unsubscribe on screen blur or app background.
  3. Re-subscribe on app foreground resume.
  4. Handle subscription errors with exponential backoff (1s, 2s, 4s, 8s, max 30s).
  5. Fallback to polling every 5s for chat, 30s for notifications if subscriptions unavailable.

2.5 File Upload Architecture

1. POST /storage/upload-url  { filename, content_type, is_private: false }
   → { upload_url, file_url }
2. PUT file to upload_url (S3-compatible, Content-Type header required)
3. Store returned file_url in entity field

For private files:
1. POST /storage/private-upload-url → { upload_url, file_uri }
2. PUT file to upload_url
3. POST /storage/signed-url { file_uri } → { signed_url } (time-limited access)

Size limits: Images: 10 MB | Audio: 25 MB | Documents: 20 MB.

2.6 Key Architectural Constraints

  • BR-ARCH-01: RLS enforced server-side. Client cannot bypass access control.
  • BR-ARCH-02: Admin functions verify user.role === "admin" on every invocation. 403 returned otherwise.
  • BR-ARCH-03: No business logic may exist solely in the mobile client. Server is the source of truth.
  • BR-ARCH-04: Cache invalidation must be triggered on mutation success.
  • BR-ARCH-05: Push notification tokens must be re-registered on every app launch and after login.

3. User Roles & Permissions

3.1 Platform-Level Roles

| Role | Value | Description | How Assigned | |------|-------|-------------|-------------| | Platform Admin | "admin" | Full access to all data and Super Admin | Manually by Team Move ops | | Standard User | "user" | Default for all registered accounts | Auto on first login |

BR-ROLE-01: Role is stored on the User entity. Any value other than "admin" is treated as standard user. BR-ROLE-02: Mobile client uses local role value only for UI visibility. Security enforcement is server-side only.

3.2 Hub-Level Roles

| Role | Value | Assigned when | |------|-------|--------------| | Hub Admin | "admin" | Creator of hub (auto) | | Hub Member | "member" | Default for all who join |

BR-ROLE-03: Hub Admin status determined by: community.owner_email === currentUser.email OR CommunityMember.role === "admin" for current user in that hub. BR-ROLE-04: Only one Hub Admin per hub (v1.0). No member promotion. BR-ROLE-05: Platform Admins have Hub Admin privileges in all hubs without needing to be members.

3.3 Event-Level Roles

| Role | Participation.role value | Description | |------|--------------------------|-------------| | Organizer | N/A (determined by event.organizer_email) | Full event management | | Participant | "participant" | Standard attendee | | Volunteer | "bénévole" | Helper/staff | | Companion | "accompagnant" | Non-participant companion | | Other | "autre" | Catch-all |

BR-ROLE-06: Organizer = event.organizer_email === currentUser.email OR currentUser.role === "admin".

3.4 Permissions Matrix

| Action | Platform Admin | Hub Admin | Hub Member | Organizer | Participant | |--------|:-:|:-:|:-:|:-:|:-:| | View public events | ✓ | ✓ | ✓ | ✓ | ✓ | | Create event (quota) | ✓ | ✓ | ✓ | ✓ | ✓ | | Edit/delete event | ✓ | — | — | ✓ | — | | Export participants | ✓ | — | — | ✓ | — | | Check-in participants | ✓ | — | — | ✓ | — | | Create carpool | ✓ | ✓ | ✓ | ✓ | ✓* | | Manage own carpool | ✓ | — | — | ✓ | ✓** | | Create shuttle | ✓ | — | — | ✓ | — | | Create hub | ✓ | ✓ | ✓ | ✓ | ✓ | | Edit hub | ✓ | ✓ | — | — | — | | Remove hub member | ✓ | ✓ | — | — | — | | Delete hub | ✓ | ✓ | — | — | — | | Post trip offer/request | ✓ | ✓ | ✓ | ✓ | ✓ | | Delete own trip/message | ✓ | ✓ | ✓ | ✓ | ✓ | | Delete any hub message | ✓ | ✓ | — | — | — | | Report content | ✓ | ✓ | ✓ | ✓ | ✓ | | Block user | ✓ | ✓ | ✓ | ✓ | ✓ | | Access Super Admin | ✓ | — | — | — | — |

*Participant must have a Participation record for the event.
**Own carpool driver only.

Event deletion rule: Only allowed if zero Participation records exist. Otherwise: archive only (archived = true).

3.5 Role Resolution Logic

function resolveRole(currentUser, context):
  if currentUser.role === "admin": return PLATFORM_ADMIN

  if context.type === "hub":
    member = CommunityMember.where(community_id=context.hub.id, user_email=currentUser.email)
    if member AND (member.role === "admin" OR context.hub.owner_email === currentUser.email):
      return HUB_ADMIN
    if member: return HUB_MEMBER
    return NONE

  if context.type === "event":
    if context.event.organizer_email === currentUser.email: return EVENT_ORGANIZER
    participation = Participation.where(event_id=context.event.id, user_email=currentUser.email)
    if participation: return PARTICIPANT
    return NONE

4. Authentication & Onboarding

4.1 Magic Link Authentication Flow

  1. User opens app → check Keychain/Keystore for session token.
  2. If no valid token: navigate to Login screen.
  3. User enters email (RFC 5321 format, max 254 chars).
  4. App calls: POST /auth/magic-link { email }
  5. Platform sends OTP link valid for 15 minutes, single-use.
  6. User taps link in email → deep link opens app.
  7. App extracts token: /auth/verify?token=<token>
  8. App calls: POST /auth/verify { token }
  9. Response: { session_token, user: { id, email, full_name, role } }
  10. Store session_token in Keychain (iOS) / Keystore (Android).
  11. Call GET /auth/me to fetch full user profile.
  12. If full_name is null/empty: navigate to Profile Setup (4.4).
  13. Otherwise: navigate to Home dashboard.

Deep link format: https://teammoveapp.com/auth/verify?token=xxx

4.2 Session Token Security

BR-AUTH-01: Store ONLY in:

  • iOS: kSecClassGenericPassword Keychain with kSecAttrAccessibleAfterFirstUnlock
  • Android: EncryptedSharedPreferences backed by Android Keystore

BR-AUTH-02: NEVER store in: UserDefaults, unencrypted SharedPreferences, logs, or crash reports.

BR-AUTH-03: On 401 received: attempt silent token refresh (one attempt). If refresh fails: clear token, navigate to login, preserve pending navigation intent.

BR-AUTH-04: Logout: clear token from Keychain/Keystore, clear in-memory caches, navigate to login.

BR-AUTH-05: On cold start: verify token by calling GET /auth/me. Do not assume local token validity.

4.3 Push Token Registration

BR-AUTH-06: On every login and every app foreground resume:

POST /functions/registerPushToken
{ token, platform: "ios"|"android", user_email }

BR-AUTH-07: Re-register whenever token rotates (iOS: didRegisterForRemoteNotificationsWithDeviceToken; Android: onTokenRefresh).

BR-AUTH-08: If push permission denied: do not re-request for 30 days.

4.4 First-Time User Profile Setup

Triggered when user.full_name is null or empty.

  1. "Welcome to Team Move" screen.
  2. Required: Display name (full_name). Validation: 2–100 chars, at least one printable character.
  3. Optional: Preferred transport mode.
  4. Optional: City of residence (max 100 chars).
  5. Optional: Avatar — AI-generated (calls generateAvatar; 5–10s wait) or emoji set.
  6. PATCH /auth/me with { full_name, transport_mode, city, avatar_url }.
  7. Avatar generation failure: silently fall back to default avatar. Do not block onboarding.
  8. Network error on profile save: offer retry or "Complete later" option.

4.5 Event Participant Onboarding (No Account)

Invited person with no account: /event-invite?token=<invitation_token>

  1. App calls: POST /functions/validateEventInvitation { token }
  2. If expired: "This invitation has expired. Contact the organizer." END.
  3. If already accepted: "You already accepted this invitation." with link to event. END.
  4. Display: event name, date, location, organizer name.
  5. User enters name (required) + transport mode.
  6. Tap "Accept": POST /functions/acceptEventInvitation { token, name, transport_mode }
  7. Backend creates/links Participation record, marks invitation "accepted."
  8. Prompt: "Create a Team Move account to access your event details anytime."
  9. If user skips: show confirmation screen (no persistent session).

[SCREENSHOT REQUIRED: Event invite acceptance — event preview screen] [SCREENSHOT REQUIRED: Event invite acceptance — name + transport input]

4.6 Authentication Edge Cases

| Scenario | Detection | Resolution | |----------|-----------|-----------| | Magic link expired (> 15 min) | 401 on /auth/verify | "This link has expired." + "Resend link" button | | Magic link used twice | 401 on second /auth/verify | "This link has already been used." + resend option | | User not registered | /auth/me returns user_not_registered | "Account not found" screen with support email | | Token expired mid-session | 401 on any API call | Silent refresh → if fails, redirect to login | | Deep link with wrong format | 400 from /auth/verify | "Invalid link. Please request a new login link." | | App opened via deep link while logged in | Token valid | Skip auth, navigate to destination | | Network error on /auth/verify | Connection error | "Could not connect. Check internet and try again." + retry |


5. Community Hubs Module

5.1 Data Models

Community entity:

Community {
  id, name (req, 3–100), description (max 1000),
  owner_email (req), owner_name,
  invite_code (req, 8-char alphanumeric, server-generated, globally unique),
  cover_image_url, members_count (default: 1), event_id (optional)
}

CommunityMember entity:

CommunityMember {
  id, community_id, user_email, pseudo (req, 2–30 chars),
  role: "admin"|"member" (default: "member")
}

5.2 Hub Creation

  1. User taps "Create a Hub."
  2. Enters name (required), description (optional), cover image (optional, jpg/png/webp, max 10 MB).
  3. Taps "Create Hub."
  4. POST /entities/Community { name, description, owner_email, owner_name }
  5. Server generates invite_code (globally unique), creates CommunityMember for creator (role="admin"), sets members_count = 1.
  6. Navigate to Hub Detail with created hub.
  7. Toast: "Hub created! Share your invite code: XXXXXXXX."

Business rules:

  • BR-HUB-01: Name: 3–100 chars, printable characters only.
  • BR-HUB-02: invite_code generated server-side. Client never generates codes.
  • BR-HUB-03: members_count maintained server-side. Client does not update it directly.
  • BR-HUB-04: No hub creation quota.
  • BR-HUB-05: Image upload failure: non-blocking. Hub created without cover image.

[SCREENSHOT REQUIRED: Hub creation form]

5.3 Joining a Hub

Via invite code (/join-community?code=XXXXXXXX):

  1. Auto-uppercase input, 8-char alphanumeric filter.
  2. POST /functions/getCommunityByCode { code }
  3. Display hub preview: name, cover image, member count.
  4. Enter pseudo (2–30 chars, alphanumeric + space/hyphen/underscore, no pure whitespace).
  5. POST /entities/CommunityMember { community_id, user_email, pseudo, role: "member" }
  6. Server increments members_count, dispatches community_new_member notification.
  7. ImpactPoint: community_join (+5 pts).
  8. Navigate to Hub Detail.

Business rules:

  • BR-HUB-06: Pseudo must be unique within the hub (per community_id).
  • BR-HUB-07: Duplicate join rejected (409): "You are already a member of this hub."
  • BR-HUB-08: Pseudo validation: 2–30 chars.
  • BR-HUB-09: Joining is instant — no approval workflow.

Error states:

  • Code not found: "Hub not found. Check the code and try again."
  • Already a member: "You are already a member of this hub." + "Go to hub" button.
  • Pseudo taken: "This name is already taken. Please choose another."

[SCREENSHOT REQUIRED: Join hub — code entry] [SCREENSHOT REQUIRED: Join hub — hub preview with member count] [SCREENSHOT REQUIRED: Join hub — pseudo selection]

5.4 Hub Detail — Tab Structure

| Tab | Access | Entity/data source | |-----|--------|--------------------| | Dashboard | All members | Community, CommunityMember, CommunityTrip, CommunityEvent | | Events | All members | CommunityEvent | | Trips | All members | CommunityTrip | | Move Room | All members | CommunityMessage | | DMs | All members | CommunityDirectMessage | | Members | All members | CommunityMember | | Admin | Hub Admin only | Community (edit), CommunityMember (manage) |

BR-HUB-11: Admin tab completely hidden (not just disabled) from non-admin members. BR-HUB-12: Non-member accessing Hub Detail: show "You are not a member of this hub" + "Join Hub" button.

On load:

  1. Fetch Community by ID.
  2. Fetch current user's CommunityMember record for this hub.
  3. If no record: show non-member state.
  4. Load default tab (Dashboard).

[SCREENSHOT REQUIRED: Hub detail — Dashboard tab, mobile] [SCREENSHOT REQUIRED: Hub detail — tab navigation bar]

5.5 Hub Dashboard Tab

Content blocks:

  1. Hub cover image + name + member count.
  2. RSE stats: total CO₂ saved by hub members (sum ImpactPoint.co2_saved_kg for community_id), total trips coordinated.
  3. Quick actions: [Post a trip] [Create an event] [Invite a member].
  4. Upcoming events: next 3 CommunityEvent records, future dates.
  5. Recent trips: last 3 CommunityTrip records by created_date desc.

BR-HUB-13: "Invite a member" opens native share sheet with: invite code + pre-formatted message.

5.6 Move Room (Group Chat)

CommunityMessage entity:

CommunityMessage {
  id, community_id, sender_email, sender_pseudo,
  message (optional if media, max 4000 chars),
  type: "text"|"image"|"file"|"voice"|"poll",
  file_url, file_name, file_size,
  poll: { question, options: [{ text, voters: string[] }] },
  mentions: string[] (pseudos)
}

Loading: Last 50 messages on open. Scroll to top: load 50 older (prepend). Subscribe to real-time.

Business rules:

  • BR-CHAT-01: Max 4000 chars. Character counter shown at > 3500.
  • BR-CHAT-02: @mention: autocomplete of hub member pseudos. Adds pseudo to mentions[].
  • BR-CHAT-03: Messages with mentions: notifyHubNewMessage called for each mentioned user.
  • BR-CHAT-04: Empty messages cannot be sent.
  • BR-CHAT-05: Images: max 10 MB, jpg/png/webp/gif. Show upload progress; optimistic thumbnail.
  • BR-CHAT-06: Files: max 20 MB, pdf/docx/xlsx/pptx/txt/zip. Tap to download.
  • BR-CHAT-07: Voice: max 3 min. Show timer. After recording: preview waveform + play/stop. iOS: m4a; Android: ogg/webm.
  • BR-CHAT-08: Polls: question (req, max 200 chars), 2–6 options (each max 100 chars). One vote per member. Results real-time. No edit after creation.
  • BR-CHAT-09: Text messages: editable up to 15 min after send. Show "(edited)" label.
  • BR-CHAT-10: Author can delete own messages anytime. Shows "[Message deleted]" placeholder.
  • BR-CHAT-11: Hub Admin can delete any message. Shows "[Message removed by admin]" placeholder.
  • BR-CHAT-12: Long-press context menu: [Reply] [Copy] [Report] [Edit (own)] [Delete (own/admin)].

Content reporting:

  • Reason selector: Harassment / Spam / Inappropriate / Hate speech / Violence / Other.
  • Creates ContentReport: { reporter_email, reported_user_email, message_id, message_content (snapshot), community_id, context: "move_room", reason, details }.
  • Confirmation: "Report submitted. Our team will review it."

[SCREENSHOT REQUIRED: Move Room — mixed message types (text, image, voice, poll)] [SCREENSHOT REQUIRED: Move Room — poll with live vote results] [SCREENSHOT REQUIRED: Move Room — message context menu (long press)]

5.7 Direct Messages

CommunityDirectMessage entity:

CommunityDirectMessage {
  id, community_id, sender_email, sender_pseudo,
  recipient_email, recipient_pseudo,
  message, type: "text"|"image"|"file"|"voice",
  file_url, file_name, file_size, read (default: false)
}

Business rules:

  • BR-DM-01: DMs only between members sharing a hub (community_id).
  • BR-DM-02: DMs scoped to community_id. Separate threads per hub.
  • BR-DM-03: read = true when recipient opens conversation.
  • BR-DM-04: Unread badge: count CommunityDirectMessage where recipient_email = user AND read = false.
  • BR-DM-05: Blocked users cannot send DMs. If blocked user attempts: "You cannot send a message to this user." (without revealing block).
  • BR-DM-06: Message types and size limits identical to Move Room.
  • BR-DM-07: DM history persists after either user leaves the hub.

Push notification for DMs: Include sender pseudo + hub name, NOT message content (privacy).

[SCREENSHOT REQUIRED: DM inbox — conversation list with unread badges] [SCREENSHOT REQUIRED: DM conversation view — message thread]

5.8 Hub Administration (Hub Admin Only)

1. Edit Hub Name/Description: 3–100 chars.

2. Change Cover Image: 10 MB max, jpg/png/webp.

3. Regenerate Invite Code:

  • Confirmation dialog: "This will invalidate the current invite code (XXXXXXXX). Continue?"
  • Call: POST /functions/communityAdminActions { action: "regenerate_invite_code", community_id }
  • BR-HUB-14: Old code immediately invalid on server.

4. Remove a Member:

  • BR-HUB-15: Admin cannot remove themselves (action hidden for own CommunityMember record).
  • DELETE /entities/CommunityMember/<id>. Server decrements members_count.
  • Posts (chat, trips) remain as orphaned records.

5. Delete Hub:

  • Requires typing exact hub name to confirm.
  • Call: POST /functions/communityAdminActions { action: "delete_hub", community_id }
  • Cascade: deletes all CommunityMember, CommunityMessage, CommunityDirectMessage, CommunityTrip, CommunityEvent.
  • BR-HUB-17: Irreversible. No recovery.

5.9 Member Blocking

  • Any member can block another. Creates BlockedUser: { blocker_email, blocked_email, community_id }.
  • BR-BLOCK-01: Blocked user's messages hidden from blocker's view ("[Message hidden]" placeholder).
  • BR-BLOCK-02: Blocked user cannot DM blocker.
  • BR-BLOCK-03: Does not remove either user from hub.
  • BR-BLOCK-04: One-directional: blocking does not affect blocked user's view of blocker's messages.
  • BR-BLOCK-05: Block list visible in Profile → Privacy → Blocked users.
  • BR-BLOCK-06: Unblock: DELETE /entities/BlockedUser/<id>. Effects revert immediately.

5.10 Leaving a Hub

  1. Member taps "Leave this hub" (in hub settings or profile).
  2. Confirmation: "Leave [Hub name]? You will lose access to all hub content."
  3. DELETE /entities/CommunityMember/<id>. Server decrements members_count.
  4. Navigate to Communities list.

BR-HUB-18: Hub Admin cannot leave (option hidden). Must delete hub or contact support. BR-HUB-19: After leaving, user can rejoin via invite code. BR-HUB-20: Leaving does not delete any posted content.


6. Events Module

6.1 Event Entity — Complete Schema

Event {
  id, title (req, 3–200), event_type, visibility: "public"|"private",
  access_type: "free"|"private_code"|"paid", access_code (4–20 if private_code),
  is_paid, price (> 0 if paid, EUR), max_participants (null = unlimited),
  description (max 5000), date (req, YYYY-MM-DD), end_date (>= date),
  time (HH:MM), end_time, daily_schedules[],
  location (req), city (req), gps_coordinates { latitude, longitude },
  expected_public { participants, volunteers, companions, others },
  mobility_settings {
    enabled_modes[], incentive_message,
    carpool_policy: "none"|"recommended"|"mandatory", min_carpool_fill_rate
  },
  payment_settings { enabled, pricing_type, base_price, currency,
    group_pricing[], discounts[] },
  image_url, organizer_email (req), structure_id, structure_name,
  status: "brouillon"|"publié"|"clôturé" (default: "publié"), archived (default: false)
}

Event types: familial | ludique | festif | associatif | sportif | éducatif | professionnel

6.2 Subscription Quota Enforcement

Before allowing event creation:

1. Fetch active Subscription for user (status: "actif") or trial (status: "trial").
2. If no subscription: BLOCK — show UpgradeModal.
3. If trial AND trial_event_used = true: BLOCK.
4. If active AND events_quota is not null:
   Count events where organizer_email = user AND created_date >= subscription.start_date.
   If count >= events_quota: BLOCK.
5. If events_quota = null: unlimited — allow.

BR-EVT-01: Quota check enforced server-side in createEvent backend function. Client-side check is UX only. BR-EVT-02: UpgradeModal links to Subscription selection screen.

6.3 Event Creation — Multi-Step Form

Step 1 — Basic Info: Title (req), event_type (req), date (req, >= today), end_date, time, end_time, description, cover image (10 MB max).

Step 2 — Location: Address (Google Places Autocomplete, req), city (auto-filled, editable), GPS (auto-resolved via Geocoding API), expected attendee breakdown.

Step 3 — Access & Visibility: Visibility (public/private), access_type (free/code/paid), access_code (4–20 chars if code), price (> 0 if paid), max_participants.

Step 4 — Mobility Settings: Enable/disable transport modes, carpool policy, incentive message.

Step 5 — Review & Publish: Full summary. Status toggle: publish/draft. "Create event" button.

Validation:

  • date >= today (Europe/Paris timezone).
  • access_type = paid: price required > 0.
  • access_type = private_code: access_code required.
  • GPS resolution failure: show warning "Could not resolve GPS coordinates. Distance calculations may be less accurate." Allow proceed.
  • Image upload failure: "Event will be created without cover image." Non-blocking.
  • Network error: show retry. Preserve form data.

[SCREENSHOT REQUIRED: Event creation — Step 1 basic info] [SCREENSHOT REQUIRED: Event creation — Step 2 location with Places autocomplete] [SCREENSHOT REQUIRED: Event creation — Step 4 mobility settings] [SCREENSHOT REQUIRED: Event creation — Step 5 review and publish]

6.4 Participant Registration Flow

  1. Navigate to event (from listing, invite link, or direct URL).
  2. If access_type = "private_code": validate code before form (client-side; server re-validates on submit).
  3. If access_type = "paid": initiate Stripe payment before form (section 6.5).
  4. Registration form: role, departure city, departure address (Places autocomplete), transport mode outbound, transport mode return.
  5. Submit: POST /entities/Participation { event_id, user_email, user_name, role, transport_mode, transport_mode_return, departure_city, departure_address, status: "à_compléter" }
  6. If transport involves distance (carpool_driver, car_solo): call calculateRouteDistance → store distance_km, carbon_saved_kg.
  7. Call notifyOrganizerOnRegistration.
  8. ImpactPoint: event_participation (+5). If bike/walk/train/public_transport/shuttle: low_carbon_transport (+15).
  9. Show success: "You're registered!" with event summary and next steps.

Transport modes: car_solo | carpool_driver | carpool_passenger | public_transport | bike | walk | scooter | shuttle | not_set

Validation rules:

  • BR-REG-01: max_participants check: if Participation count >= max_participants (when not null): 409 "This event is full."
  • BR-REG-02: Duplicate: 409 "You are already registered for this event."
  • BR-REG-03: Departure city: required when mode is carpool_driver, carpool_passenger, or shuttle.
  • BR-REG-04: Departure address: required when mode is carpool_driver.
  • BR-REG-05: carpool_policy = "mandatory" + car_solo chosen: soft warning, allow override.
  • BR-REG-06: Free events: transport mode updatable anytime. Paid: immutable after payment.

[SCREENSHOT REQUIRED: Event registration — role + transport mode selection] [SCREENSHOT REQUIRED: Event registration — departure address with autocomplete] [SCREENSHOT REQUIRED: Event registration — success confirmation]

6.5 Paid Event Registration — Stripe Flow

BR-PAY-01: Payment must complete before Participation is created.

  1. Call: POST /functions/createEventPaymentIntent { event_id, user_email }
  2. Response: { client_secret, amount, currency }
  3. Present Stripe PaymentSheet (native SDK: STPPaymentSheet iOS / PaymentSheet Android).
  4. On PaymentSheetResult.completed: create Participation record.
  5. On PaymentSheetResult.failed: display Stripe error message.
  6. On PaymentSheetResult.canceled: return to event detail.
  7. Network error after payment but before Participation creation: "Payment processed. Contact support@teammoveapp.com with ref: [payment_intent_id]."

6.6 Event Listing & Discovery

  • Filter by: city, event_type, date range, visibility = "public", status = "publié", archived = false.
  • Sort: date ascending (default).
  • Pagination: 20 per page.
  • Search: full-text on title, city, location (client-side on fetched results, max 100 events for search).

[SCREENSHOT REQUIRED: Event listing — grid/list view with filter chips]

6.7 Organizer Dashboard Sections

| Section | Content | |---------|---------| | Overview | Registrations count, transport breakdown chart, CO₂ saved, carpool fill rate | | Participants | Full list: name, role, transport, city, status. Filter, search, export CSV/PDF. | | Carpools | List, seat management, passenger assignment | | Shuttles | Manage shuttles, stops, passengers | | Messages | Compose + send bulk email to all or filtered segments | | Check-in | QR scanner + manual name search | | Return | Return transport confirmation dashboard | | Labels | RSE transport breakdown labels |

BR-ORG-01: Organizer can update any Participation field for any participant. BR-ORG-02: Removing a participant does not auto-refund paid tickets.

[SCREENSHOT REQUIRED: Organizer dashboard — overview tab with charts] [SCREENSHOT REQUIRED: Organizer dashboard — participants tab]

6.8 Event Check-in

Participant side — QR code:

  • Format: TEAMM:{participation_id}
  • Must be displayable offline (generated client-side from stored participation_id).

Organizer side — scan flow:

  1. Open Check-in tab. Tap "Scan QR code."
  2. Decode QR: extract participation_id.
  3. PATCH /entities/Participation/<participation_id> { checked_in: true, checked_in_at: now }.
  4. Show: participant name, role, transport mode.
  5. Haptic success + optional audio beep (respects silent mode on iOS).
  6. Auto-return to scan mode after 2 seconds.

Business rules:

  • BR-CHKIN-01: Available only: event.date - 1 day to event.date + 1 day. Outside window: appropriate message.
  • BR-CHKIN-02: Idempotent: second scan shows "Already checked in at [time]" without modifying record.
  • BR-CHKIN-03: Invalid QR (wrong event or not found): "Invalid QR code. Not for this event."
  • BR-CHKIN-04: Real-time count: "X / Y checked in" displayed on organizer's tab.

[SCREENSHOT REQUIRED: Organizer — QR scanner view] [SCREENSHOT REQUIRED: Organizer — check-in success feedback] [SCREENSHOT REQUIRED: Participant — QR code display screen]

6.9 Event Status Lifecycle

ALLOWED:
brouillon → publié (organizer publishes)
brouillon → deleted (only if zero participants)
publié → brouillon (unpublish — registrations preserved)
publié → clôturé (organizer closes OR auto at end_date + 24h)
clôturé → archived (set archived = true)

FORBIDDEN:
clôturé → publié (cannot reopen)
deleted → any

BR-EVT-03: On clôturé: all open carpools set available_seats = 0. BR-EVT-04: Auto-closure: daily scheduled function. If date + 1 day < today AND status = "publié": auto-set "clôturé," trigger eventReturnReminder.

6.10 Automated Reminder Schedule

| Timing | Function | Recipients | Content | |--------|---------|-----------|---------| | J-7 | organizerReminderJ7 | Organizer | Participant count, transport summary | | J-1 | eventReminderJ1 | All participants | Event details, transport confirmation | | J-0 08:00 | eventCheckInJ0 | All participants | QR code reminder | | Event end + 2h | eventReturnReminder | return_confirmed = false | Return transport prompt | | J+1 | Feedback email | All participants | 1-click satisfaction rating |

BR-EVT-05: All reminders check status = "publié" or "clôturé" before sending. BR-EVT-06: Participants who opted out of notifications excluded from automated emails.


7. Mobility Coordination Module

7.1 Carpool — Complete Data Model

Carpool {
  id, event_id, driver_email, driver_name,
  trip_type: "aller"|"retour"|"aller_retour",
  departure_city, departure_address, departure_time (HH:MM),
  return_departure_time (HH:MM, required if trip_type includes retour),
  available_seats (1–7), available_seats_return (1–7),
  total_seats, distance_km,
  passengers: [{ email, name, confirmed, confirmed_at }],
  passengers_return: [{ email, name, confirmed, confirmed_at }],
  contribution_enabled, price_per_km (max 0.32 EUR/km)
}

7.2 Creating a Carpool

Pre-conditions: User has a Participation record for the event. No existing Carpool as driver for this event.

Business rules:

  • BR-CAR-01: Cannot be both driver and passenger for the same direction.
  • BR-CAR-02: available_seats: 1–7 inclusive.
  • BR-CAR-03: departure_address required; validated when calculateRouteDistance is called.
  • BR-CAR-04: price_per_km legal max: 0.32 EUR/km. Warning shown if exceeded. Not hard-blocked.
  • BR-CAR-05: distance_km auto-calculated via calculateRouteDistance. If fails: store 0, show warning.
  • BR-CAR-06: cost per passenger = distance_km × price_per_km. Displayed to potential passengers.
  • BR-CAR-07: trip_type = "aller_retour" requires both departure_time AND return_departure_time.

On creation: POST /entities/Carpool → calculateRouteDistance → update distance_km → update Participation.transport_mode = "carpool_driver" → notifyNewCarpool.

[SCREENSHOT REQUIRED: Create carpool form — all fields]

7.3 Joining a Carpool

  1. Tap "Join this carpool."
  2. Confirm modal: driver name, departure address, time, cost per person.
  3. PATCH /entities/Carpool/<id>: add to passengers[], decrement available_seats.
  4. PATCH /entities/Participation/<id>: set transport_mode = "carpool_passenger", carpool_id.
  5. Call notifyCarpoolPassengerJoined. ImpactPoint: carpool_join (+10 pts).

Business rules:

  • BR-CAR-08: Full (available_seats = 0): button shows "Full," disabled.
  • BR-CAR-09: Own carpool: Join button hidden.
  • BR-CAR-10: Already in another carpool for same direction: "You are already in a carpool for this trip."
  • BR-CAR-11: available_seats never goes negative. Server-side atomic check on concurrent joins.

7.4 J-2 Carpool Confirmation

  • carpoolConfirmationReminder dispatches 2 days before event.
  • Passenger receives: "Confirm your seat in [driver]'s carpool."
  • PATCH Participation: confirmed = true, confirmed_at = now.
  • Driver receives summary of confirmed vs unconfirmed.
  • BR-CAR-12: If passenger does not confirm by J-1: driver can remove them without notification.

7.5 Carpool Card Display

Shows: driver first name + last initial, departure address (street-level), departure time, direction badge, seat indicator (X/Y), cost per passenger (if contribution_enabled), distance km, Join button (states: active / Full / Own carpool / Already joined).

[SCREENSHOT REQUIRED: Carpool list for an event — all card states] [SCREENSHOT REQUIRED: Carpool join confirmation modal] [SCREENSHOT REQUIRED: Carpool detail — driver's passenger management]

7.6 Shuttle Management

Access: Organizer and platform admin only.

Shuttle {
  id, event_id, name, departure_location, departure_time, return_time,
  capacity (1–100), available_seats (computed: capacity - passengers.length),
  passengers: [{ email, name, assigned_manually }],
  stops: [{ location, time }], driver_name, driver_contact, vehicle_info,
  status: "planifié"|"en_cours"|"terminé"|"annulé",
  contribution_enabled, suggested_contribution
}

BR-SHU-01: Participant in shuttle cannot simultaneously be in a carpool for the same direction. BR-SHU-02: Cancelling shuttle (status = "annulé"): push + email to all assigned passengers. Participation.transport_mode NOT auto-updated. BR-SHU-03: suggested_contribution: cash on event day, not processed online.

[SCREENSHOT REQUIRED: Shuttle management — organizer view, shuttle list]

7.7 Return Journey Dashboard

  • Lists all participants with return transport status.
  • Filter: unconfirmed only, by transport mode.
  • Manual override: organizer updates Participation.transport_mode_return + return_confirmed = true.

BR-RET-01: return_confirmed defaults to false. BR-RET-02: eventReturnReminder targets: return_confirmed = false AND (transport_mode_return = "not_set" OR null). BR-RET-03: Dashboard shows real-time confirmation rate.

7.8 Train Search

SNCF (France): POST /functions/searchTrains { origin_city, destination_city, date, time } Response: { trains: [{ departure_time, arrival_time, duration_minutes, changes, train_number, is_direct }] }

Luxembourg: POST /functions/searchLuxembourgTrains { origin, destination, date }

Informational only — no booking through platform.

BR-TRAIN-01: Available only if event.city is in France or Luxembourg. BR-TRAIN-02: Pre-filled with participant's departure city and event city. Error states: API unavailable → "Check SNCF.com directly." No results → "No trains found." Invalid city → "City not recognized."

[SCREENSHOT REQUIRED: Train search results list]


8. Journey Mutualization (Community Trip Board)

8.1 CommunityTrip Entity — Complete Schema

CommunityTrip {
  id, community_id, author_email, author_pseudo,
  type: "offer"|"request",
  transport_mode: "voiture"|"train"|"bus"|"velo"|"marche"|"vtc"|"avion",
  direction: "aller"|"retour"|"aller_retour",
  departure_city (req), destination_city (req),
  date (req, >= today), time (HH:MM),
  return_date (req if direction includes retour), return_time,
  seats_available (1–7, req for car offers), seats_available_return (1–7),
  event_title (optional), notes (max 500),
  status: "open"|"closed",
  cost_sharing, distance_km, cost_per_person, payment_mode: "especes"|"virement"|"autre",
  auto_accept (default: false),
  interested: [{
    email, pseudo, direction, pickup_address,
    status: "pending"|"accepted"|"declined",
    expressed_at
  }]
}

8.2 Posting a Trip Offer

Validation rules:

  • BR-TRIP-01: date >= today (Europe/Paris).
  • BR-TRIP-02: destination_city != departure_city.
  • BR-TRIP-03: seats_available (1–7) required for voiture/vtc. Field hidden for other modes.
  • BR-TRIP-04: direction = "aller_retour": return_date required (>= date).
  • BR-TRIP-05: cost_sharing = true + distance_km: auto-calculate cost_per_person (default 0.20 EUR/km, editable).
  • BR-TRIP-06: auto_accept = true: time field becomes required.
  • BR-TRIP-07: Author cannot appear in interested[].

Impact points: community_trip_offer (+10 pts). One award per CommunityTrip.id. Post-create: notifyCommunityActivity to all hub members.

8.3 Posting a Trip Request

Same date/city/direction validation. seats_available not applicable. Impact points: community_trip_request (+5 pts). Post-create: notifyCommunityActivity to all hub members.

8.4 Expressing Interest

For offers — "Join this trip":

  1. Direction selector (if aller_retour), pickup address (required for car modes), notes.
  2. PATCH /entities/CommunityTrip/<id>: append to interested[].
  3. If auto_accept: status = "accepted," decrement seats_available.
  4. If not auto_accept: status = "pending," notify author.
  5. ImpactPoint: community_trip_interest (+5 pts).

For requests — "I can drive on this route":

  1. Opens TripOffer creation form pre-filled with destination, date.
  2. Context banner: "Responding to [pseudo]'s request for [route]."
  3. On submit: creates new CommunityTrip offer + notifies original request author.

Business rules:

  • BR-TRIP-09: Users cannot express interest in their own trips.
  • BR-TRIP-10: Duplicate interest rejected: "You have already expressed interest in this trip."
  • BR-TRIP-11: auto_accept + race condition: if seats reach 0 before server processes: 409 "This trip is now full."
  • BR-TRIP-12: User can withdraw interest (PATCH — remove from interested[]).
  • BR-TRIP-13: Accepted: push to user "Your request to join [pseudo]'s trip has been accepted!"
  • BR-TRIP-14: Declined: push to user "[pseudo]'s trip is not available for your request."

8.5 Trip Card Display Specification

Offer card: Transport icon, direction badge, author pseudo, [departure] → [destination], date/time, seats (X available), cost/person, auto-accept badge, CO₂ badge, interested count. Owner: [Manage trip]. Non-owner: [Join this trip] (disabled if full/already interested).

Request card: Same header. "LOOKING FOR" label. Notes excerpt (2 lines). Non-owner with car: [I can drive on this route]. Non-owner other modes: [Contact via DM].

Empty state: "No trips posted yet. Be the first to share a trip offer or request!" + [Post a trip offer] [Request a trip] buttons.

[SCREENSHOT REQUIRED: Trip board — offers and requests mixed] [SCREENSHOT REQUIRED: Trip offer card — all badge states] [SCREENSHOT REQUIRED: Trip request card — "I can drive" button] [SCREENSHOT REQUIRED: Interest expression modal — pickup address] [SCREENSHOT REQUIRED: Driver — manage trip, accept/decline view]

8.6 CO₂ Per Transport Mode

| Mode | kg CO₂e/km/pax | |------|----------------| | Car solo | 0.217 | | Car shared (2 pax) | 0.108 | | Car shared (3 pax) | 0.072 | | Car shared (4 pax) | 0.054 | | Train (France) | 0.0023 | | Bus interurbain | 0.0298 | | Bike / Walk | 0.000 | | VTC | 0.230 | | Plane (short-haul) | 0.255 |

8.7 Trip Lifecycle

  • BR-TRIP-15: Author closes trip anytime: PATCH status = "closed." Join button removed.
  • BR-TRIP-16: Past trips (date < today - 1): filtered from active board, shown in "Past trips" section.
  • BR-TRIP-17: Closed trips remain visible for historical context, sorted below open trips.

9. Environmental Impact Module

9.1 CO₂ Calculation — Authoritative Specification

Reference: ADEME Base Carbone® 2024 — Facteurs d'émission des transports.

Emission factors (kg CO₂e per passenger per km):

| Transport mode | Factor | Notes | |----------------|--------|-------| | Car solo (thermique) | 0.217 | ADEME moyenne véhicule particulier | | Car partagé (2 pax) | 0.1085 | 0.217 / 2 | | Car partagé (3 pax) | 0.0723 | 0.217 / 3 | | Car partagé (4 pax) | 0.0543 | 0.217 / 4 | | Train TER/TGV | 0.0023 | ADEME France | | Train (European avg) | 0.006 | Non-France | | Bus interurbain | 0.0298 | ADEME autocar | | Bus urbain | 0.0890 | ADEME bus urbain | | Vélo électrique | 0.0027 | ADEME VAE | | Vélo / marche | 0.0000 | Zero emission | | Trottinette électrique | 0.0055 | ADEME | | VTC | 0.2300 | Assimilé voiture solo + deadhead | | Avion court-courrier | 0.2550 | ADEME < 1000 km |

Calculation formulas:

// Baseline: user drives solo
baseline_co2 = distance_km × 0.217

// Carpool driver with N passengers:
co2_per_person = distance_km × (0.217 / (N + 1))

// Other modes:
co2_per_person = distance_km × emission_factor

// CO₂ saved:
co2_saved_kg = max(0, baseline_co2 - co2_per_person)

Business rules:

  • BR-CO2-01: distance_km = 0 or null: display "Distance unknown — CO₂ impact not calculated." Never show a false 0.
  • BR-CO2-02: Prefix all displayed values with "~" (approximate). Never present as scientific measurement.
  • BR-CO2-03: Aller_retour: store carbon_saved_kg (outbound) and carbon_saved_kg_return separately. Total = sum.
  • BR-CO2-04: Haversine fallback: distance_km × 1.25 road correction. Used silently on Google Maps API failure.
  • BR-CO2-05: If address cannot be geocoded: store distance_km = null, co2_saved_kg = null.

Precision: Store to 2 decimal places. Display with 1 decimal (e.g., "1.4 kg CO₂").

When to calculate:

  • Participation creation: if distance_km known.
  • Carpool creation: after calculateRouteDistance.
  • CommunityTrip creation: if distance_km provided.
  • Store in: Participation.carbon_saved_kg, ImpactPoint.co2_saved_kg.

9.2 Distance Calculation

Primary — Google Directions API:

POST /functions/calculateRouteDistance
{ origin: "address or city, country", destination: "address or city, country" }
→ { distance_km, duration_minutes, status: "ok" }
Errors: { error: "GEOCODE_FAILED" | "API_UNAVAILABLE" }

Fallback — Haversine: Straight-line × 1.25 road factor. Used silently.

9.3 Impact Points — Complete Specification

ImpactPoint entity:

ImpactPoint {
  id, user_email, user_name, action_type,
  points (positive = earned, negative = spent),
  description, event_id, event_title, community_id,
  structure_id, reference_id (for deduplication), co2_saved_kg
}

Full action_type table:

| action_type | Points | Deduplication key | |-------------|--------|------------------| | carpool_offer | +20 | Carpool.id | | carpool_join | +10 | Carpool.id + user_email | | trip_request_response | +15 | CommunityTrip offer.id | | event_participation | +5 | Participation.id | | low_carbon_transport | +15 | Participation.id + "low_carbon" | | profile_complete | +10 | User.id + "profile_complete" | | reward_redemption | -(cost_points) | RewardRedemption.id | | bonus | variable | AuditLog.id | | community_join | +5 | CommunityMember.id | | community_trip_offer | +10 | CommunityTrip.id | | community_trip_request | +5 | CommunityTrip.id | | community_trip_interest | +5 | CommunityTrip.id + user_email | | community_event_create | +10 | CommunityEvent.id | | community_event_join | +5 | CommunityEvent.id + user_email | | community_low_carbon | +15 | CommunityEvent.id + user_email + "low_carbon" |

Business rules:

  • BR-PTS-01: Before creating any ImpactPoint: query filter({ user_email, action_type, reference_id }). If exists: skip silently.
  • BR-PTS-02: Total balance = SUM(ImpactPoint.points) for user_email. Always computed from DB — never denormalized.
  • BR-PTS-03: Points cannot go negative. Verify sum >= cost_points before reward_redemption.
  • BR-PTS-04: bonus type: only creatable by platform admin.

9.4 Level System

| Level | Min Points | Display Name | Badge Color | |-------|-----------|--------------|-------------| | 1 | 0 | Bronze | #CD7F32 | | 2 | 200 | Argent (Silver) | #C0C0C0 | | 3 | 500 | Or (Gold) | #FFD700 | | 4 | 1 000 | Platine | #E5E4E2 | | 5 | 2 000 | Titane | #878681 |

BR-LVL-01: Level computed on every Impact Dashboard load. Not cached separately. BR-LVL-02: Level-up detection: compare before and after each point award. If level increases: trigger celebration. BR-LVL-03: Rewards with min_level > current level: displayed locked (greyed out, lock icon). Cannot redeem.

9.5 Challenges

Challenge {
  id, title, description, emoji, type: "co2_saved"|"carpools_offered"|"low_carbon_trips"|"participants",
  target_value, current_value (server-maintained), start_date, end_date,
  reward_points, status: "active"|"completed"|"expired", participants_count
}

BR-CHL-01: current_value maintained server-side. Client does not update it. BR-CHL-02: On end_date: if current_value >= target_value → status = "completed," distribute reward_points. Else → "expired." BR-CHL-03: Joining is implicit — all users with eligible actions during the period are counted. BR-CHL-04: Progress bar = (current_value / target_value) × 100, capped at 100%. BR-CHL-05: Past challenges visible in "Past Challenges" section for 30 days after expiry.

9.6 Rewards Shop

Reward {
  id, title, description,
  category: "boisson"|"evenement"|"goodies"|"vip"|"bon_reduction"|"autre",
  cost_points (> 0), stock (null = unlimited), image_emoji, active, min_level
}

Redemption flow:

  1. Browse shop (active = true items only). Locked items shown if min_level not met.
  2. Tap reward → detail sheet.
  3. If eligible: "Redeem for X points." Confirmation: "You have Y points. Remaining: Y - X."
  4. Server: verify points >= cost_points AND (stock > 0 OR stock = null). Atomic: create RewardRedemption + ImpactPoint(-(cost_points)).
  5. If stock: decrement Reward.stock by 1.
  6. Success: "Reward redeemed! Contact rewards@teammoveapp.com with ref: [RewardRedemption.id]."

BR-REW-01: Redemption is atomic. Both records created or neither. BR-REW-02: Concurrent redemptions exhausting stock: first come, first served. Race condition handled server-side. BR-REW-03: No self-service delivery — manual fulfillment via email. BR-REW-04: Redemption cannot be cancelled after confirmation.

9.7 Leaderboard

Computed: SUM(ImpactPoint.points) grouped by user_email. Views: Global (top 100), Hub-specific, Monthly.

Display: Rank, pseudo (hub context) or first name + last initial (global), level badge, points, CO₂ saved.

BR-LDR-01: Current user's rank always shown, even if outside top 100. BR-LDR-02: Ties: sorted alphabetically by user_name (deterministic). BR-LDR-03: Leaderboard cached for 5 minutes (eventually consistent).

9.8 RSE / CSR Reporting

Organizer-generated reports (from web, download link in mobile app):

  • Total CO₂ saved per event.
  • Transport mode breakdown (pie chart + table).
  • Carpool fill rate.
  • Modal share comparison vs. car-solo baseline.
  • Exportable as PDF (jsPDF on web).

[SCREENSHOT REQUIRED: Impact dashboard — full screen with level, badges, points] [SCREENSHOT REQUIRED: Badge grid — locked and unlocked states] [SCREENSHOT REQUIRED: Rewards shop listing] [SCREENSHOT REQUIRED: Level-up celebration modal] [SCREENSHOT REQUIRED: RSE report — PDF preview on web]


10. Discovery Module

10.1 NewsEvent Data Model

NewsEvent {
  id, title, description (max 2000), date, end_date,
  location, city, region, category: "sports"|"gastronomie"|"boites_de_nuit"|"culture"|"musique",
  content_type: "event"|"permanent_venue",
  venue_type: "restaurant"|"museum"|"gallery"|"park"|"bar"|"cinema"|"theatre"|"stadium"|"market"|"other",
  tags[], source_url, image_url, emoji,
  mobility_friendly (default: true), ai_generated, is_active (default: true)
}

Regions: Île-de-France | Auvergne-Rhône-Alpes | Nouvelle-Aquitaine | Occitanie | Hauts-de-France | Grand Est | PACA | Pays de la Loire | Normandie | Bretagne | Bourgogne-Franche-Comté | Centre-Val de Loire | Corse | National

10.2 Content Curation

  • AI-generated via generateMoveEvents (InvokeLLM with internet context).
  • refreshCitiesEvents scheduled function updates periodically.
  • Platform admins add/edit/deactivate items manually.
  • is_active = false hides from user views.

10.3 UI Specification

Events tab: Grid (2 columns mobile). Category chip filters (multi-select), Region dropdown, Date range picker, Mobility-friendly toggle. Pagination: 20/page.

Venues tab: Same grid. venue_type filter instead of date. "Always open" label.

Map view (toggle):

  • Native maps SDK (Google Maps iOS/Android).
  • Cluster when > 20 pins in view.
  • Tap pin: bottom sheet with event card.
  • BR-DISC-01: Map uses default center (Paris) if location permission denied. No force-prompt.

Event detail: Description, image, date, time, location, [Get directions] → native Maps, [Share] → native share sheet, [View on web] → in-app browser.

Business rules:

  • BR-DISC-02: Only is_active = true displayed to users.
  • BR-DISC-03: Events with date < today - 1 auto-hidden from Events tab.
  • BR-DISC-04: AI-generated label shown only in admin view, not user view.

[SCREENSHOT REQUIRED: Move Events — grid view with filter chips] [SCREENSHOT REQUIRED: Move Events — map view with clustered pins] [SCREENSHOT REQUIRED: Event detail bottom sheet from map pin]


11. Notifications System

11.1 Notification Entity Schema

Notification {
  id, user_email, type, title (max 100), message (max 500),
  event_id, event_title, carpool_id, community_id, community_name,
  read (default: false), action_url (deep link path)
}

11.2 Complete Notification Type Reference

| Type | Trigger | Title template | action_url | |------|---------|----------------|-----------| | message_organisateur | Bulk email sent | "Message from organizer" | /event-landing?event_id={id} | | carpool_request | Passenger joins | "New passenger" | /event-landing?event_id={id}&tab=carpools | | carpool_confirmation | J-2 reminder | "Confirm your carpool" | /event-landing?event_id={id}&tab=carpools | | carpool_modification | Details changed | "Carpool updated" | /event-landing?event_id={id}&tab=carpools | | event_reminder | J-1 and J-0 | "Event tomorrow!" | /event-landing?event_id={id} | | participation_update | Organizer edits participant | "Registration updated" | /event-landing?event_id={id} | | community_event | New CommunityEvent | "New event in [hub]" | /community/{id}?tab=events | | community_trip | New CommunityTrip | "New trip in [hub]" | /community/{id}?tab=trips | | community_trip_interest | Interest in trip | "Someone wants to join" | /community/{id}?tab=trips | | community_event_join | Member joins event | "New attendee" | /community/{id}?tab=events | | community_new_member | User joins hub | "New member" | /community/{id}?tab=members | | community_dm | New DM received | "Message from [pseudo]" | /community/{id}?tab=dms |

11.3 In-App Notification Center

Load: GET /entities/Notification?filter={user_email: currentUser.email}&sort=-created_date&limit=50

Mark one read: PATCH /entities/Notification/<id> { read: true }

Mark all read: POST /entities/Notification/updateMany (filter: {user_email, read: false}) { $set: { read: true } }

Unread badge: count Notification where user_email = user AND read = false. Refresh every 60s background. Display capped at 99+.

Deep link navigation: On tap: mark read + navigate to action_url. If content deleted: navigate to list screen + toast "Content no longer available."

Business rules:

  • BR-NOTIF-01: Notifications retained 90 days; older ones archived. Max 1000 per user in active list.
  • BR-NOTIF-02: Pull-to-refresh reloads from server.
  • BR-NOTIF-03: If action_url points to deleted content: navigate to parent list screen.

11.4 Push Notification Payload Formats

iOS (APNs):

{
  "aps": {
    "alert": { "title": "New trip in [hub]", "body": "[pseudo] posted: Paris → Lyon" },
    "badge": 3,
    "sound": "default",
    "category": "community_trip"
  },
  "notification_id": "abc123",
  "action_url": "/community/xyz?tab=trips"
}

Android (FCM):

{
  "notification": { "title": "New trip in [hub]", "body": "[pseudo] posted: Paris → Lyon" },
  "data": { "notification_id": "abc123", "action_url": "/community/xyz?tab=trips" },
  "android": { "channel_id": "community_trip", "priority": "high" }
}

Android notification channels:

| Channel ID | Name | Importance | Sound | Vibrate | |-----------|------|-----------|-------|---------| | carpool | Carpool Updates | HIGH | Yes | Yes | | messages | Messages | HIGH | Yes | Yes | | reminders | Event Reminders | DEFAULT | Yes | No | | community | Community Activity | DEFAULT | Yes | No | | general | General | LOW | No | No |

11.5 Push Business Rules

  • BR-PUSH-01: Push only sent if Notification.read = false after 5 minutes of creation.
  • BR-PUSH-02: Quiet hours: no pushes 22:00–08:00 Europe/Paris. Queue and send at 08:01.
  • BR-PUSH-03: Re-register token on login and every foreground resume.
  • BR-PUSH-04: Failed delivery (invalid token): mark invalid, remove from active push list.
  • BR-PUSH-05: DM push: do NOT include message content. Use: "[pseudo] sent you a message in [hub name]."

11.6 Notification Preferences

Stored in User.notification_preferences (JSON). Backend checks before dispatching.

| Category | Default | Channels | |----------|---------|---------| | Event reminders | Enabled | Push + Email | | Carpool updates | Enabled | Push + Email | | Community messages | Enabled | Push only | | DMs | Enabled | Push + Email | | New hub members | Enabled | Push | | Challenges & rewards | Enabled | Push |

User-configurable in Profile → Notifications. Per-category toggle.


12. Subscription & Billing

12.1 Plan Specification

| Plan | events_quota | participants_quota | billing | price/month | |------|-------------|-------------------|---------|------------| | starter | 1 | 50 | — | Free | | essentiel | 12/year | 150 | monthly/annual | 9.90 EUR | | pro | unlimited | 500 | monthly/annual | 29.90 EUR | | premium | unlimited | unlimited | monthly/annual | 79.90 EUR | | evenement_ponctuel | 1 | 200 | one-time | 49 EUR | | club_association | 24/year | 200 | monthly/annual | 19.90 EUR | | start | 6/year | 100 | monthly/annual | 14.90 EUR |

Annual billing: 2 months free (pay 10, get 12) → "Save 17%."

12.2 Subscription Entity (Key Fields)

user_email, structure_id, plan_type, billing_cycle, status, start_date, end_date, events_quota, participants_quota, stripe_customer_id, stripe_subscription_id, trial_event_used, payment_proof_url, payment_validated_by, payment_validated_at.

Status values: actif | en_attente | en_attente_virement | expire | suspendu | trial

12.3 Subscription Lifecycle

[no subscription] → trial (auto-created on first login)
trial → actif (payment received)
trial → expire (no payment after 30 days)
actif → suspendu (invoice_payment_failed webhook)
actif → expire (end_date reached + subscription.deleted webhook)
en_attente → actif (Stripe checkout completed)
en_attente_virement → actif (admin validates bank transfer)
en_attente_virement → expire (admin rejects)
suspendu → actif (payment retry success)
suspendu → expire (final Stripe failure)

FORBIDDEN: expire → any (must re-subscribe from scratch)

BR-SUB-01: On expiry: existing events remain published. New event creation blocked. Participant registration to existing events remains open. BR-SUB-02: On suspension: same as expiry for new creation blocks. BR-SUB-03: Trial auto-created on first login if no Subscription record exists.

12.4 Stripe Mobile Integration

SDK versions: iOS: Stripe iOS SDK 23.x+. Android: stripe-android 20.x+.

Payment Sheet flow:

1. POST /functions/createStripeSubscription { plan_type, billing_cycle }
   → { client_secret, customer_id, ephemeral_key }

2. Configure PaymentSheet:
   merchantDisplayName: "Team Move"
   customer: CustomerConfiguration(id, ephemeralKeySecret)
   allowsDelayedPaymentMethods: true

3. Present PaymentSheet with client_secret.
4. On completed: poll subscription status until actif.
5. On failed: display error.message.
6. On canceled: return to plan selection.

Bank transfer flow:

1. User selects "Virement bancaire."
2. POST /entities/Subscription { plan_type, billing_cycle, status: "en_attente_virement", user_email }
3. Display IBAN, BIC, reference code (provided by Team Move ops).
4. User photographs bank transfer confirmation.
5. Upload to private storage: POST /storage/private-upload-url
6. PATCH Subscription: { payment_proof_url: file_uri, payment_proof_uploaded_at: now }
7. Show: "Payment proof submitted. Activation within 2 business days."

Security rules:

  • BR-PAY-02: Never store card numbers, CVV, or PAN. All via Stripe SDK (PCI DSS SAQ-A).
  • BR-PAY-03: Mobile polls subscription status after payment — does not rely on webhooks.
  • BR-PAY-04: Apple IAP risk: Team Move uses Stripe, not Apple IAP. Legal review required before App Store submission. Recommended mitigation: remove pricing from iOS app, link to web for subscription management.

12.5 Quota Enforcement Logic

// Event creation quota:
async function canCreateEvent(userEmail) {
  const sub = await getActiveSubscription(userEmail); // actif or trial
  if (!sub) return { allowed: false, reason: "no_subscription" };
  if (sub.status === "trial") {
    if (sub.trial_event_used) return { allowed: false, reason: "trial_used" };
    return { allowed: true, remaining: 1 };
  }
  if (sub.events_quota === null) return { allowed: true, remaining: null }; // unlimited
  const count = await countEventsInPeriod(userEmail, sub.start_date);
  if (count >= sub.events_quota) return { allowed: false, reason: "quota_exceeded" };
  return { allowed: true, remaining: sub.events_quota - count };
}

// Participant registration quota:
async function canRegisterParticipant(eventId) {
  const event = await Event.get(eventId);
  const sub = await getActiveSubscription(event.organizer_email);
  if (!sub || sub.participants_quota === null) return { allowed: true };
  const count = await Participation.filter({ event_id: eventId }).length;
  return count < sub.participants_quota
    ? { allowed: true }
    : { allowed: false, reason: "participant_quota_exceeded" };
}

BR-SUB-04: Organizer's subscription expires mid-event: existing registrations preserved. New registrations blocked with "This event's registration is temporarily unavailable."


13. Administration & Super Admin

13.1 Access Control

BR-ADMIN-01: Route /super-admin and all admin API calls require currentUser.role === "admin". Returns HTTP 403 otherwise.

BR-ADMIN-02: Mobile v1.0 does not implement the full Super Admin dashboard (web-only). Platform admins on mobile inherit organizer rights for any event and hub admin rights for any hub without being members.

13.2 Super Admin Dashboard (Web Only — Reference)

| Section | Description | |---------|-------------| | Global Search | Users, events, structures, subscriptions | | User Management | List, change roles, view stats | | Event Management | Edit, archive, force-close | | Subscription Management | Activate, suspend, change plans | | Payment Validation | Validate bank transfer proofs | | RSE Impact | Platform-wide CO₂ aggregate | | Email Campaigns | Bulk emails to user segments | | Moderation | Review ContentReport records | | Compliance | GDPR requests, data export/deletion | | Challenges | Create/manage global challenges | | Rewards | Create/manage reward items | | Move Events | Curate NewsEvent content | | Activity Log | AuditLog entries |

13.3 Content Moderation — Full Flow

Mobile (reporting side):

  1. Long-press message → "Report."
  2. Reason: Harassment / Spam / Inappropriate / Hate speech / Violence / Other.
  3. Optional details (max 500 chars).
  4. POST /entities/ContentReport { reporter_email, reported_user_email, reported_user_pseudo, message_id, message_content (snapshot), community_id, context, reason, details, status: "pending" }.
  5. Confirmation: "Report submitted. Our moderation team will review within 48 hours."

BR-MOD-01: One ContentReport per (reporter_email, message_id) — server enforces uniqueness. BR-MOD-02: Reporter identity never revealed to reported user.

Admin resolution (web): status updated to reviewed/resolved/dismissed. reviewed_by, reviewed_at, admin_notes recorded.


14. Rewards & Gamification

14.1 Impact Dashboard — Complete Screen Specification

Header: User avatar, name, current level badge (large, with color + name), animated points total, progress bar to next level: "(X / Y points to [next level])."

Stats row: Total CO₂ saved (kg), total carpools offered, total events attended, total km coordinated.

Badges section: Grid, locked and unlocked. Locked: greyed, lock icon, tap shows "How to unlock: [description]." Unlocked: colored, tap shows unlock date.

Minimum badge set:

| Badge ID | Name | Unlock condition | |----------|------|----------------| | first_carpool | First Carpool | First carpool_offer | | five_trips | 5 Trips | 5 trips offered or joined total | | ten_kg_co2 | Green 10 | 10 kg CO₂ saved total | | five_low_carbon | Green Commuter | 5 low_carbon_transport actions | | community_builder | Community Builder | Member of 3+ hubs | | event_organizer | Event Organizer | First event created | | hub_creator | Hub Creator | First hub created | | carpooler_pro | Carpooler Pro | 10 carpool_offer actions | | eco_champion | Eco Champion | 50 kg CO₂ saved total |

History section: Chronological ImpactPoint list. Each: action type (human-readable), points (+/-), description, date. Paginated (20/page).

[SCREENSHOT REQUIRED: Impact dashboard — all sections] [SCREENSHOT REQUIRED: Badge grid — locked and unlocked] [SCREENSHOT REQUIRED: Points history feed]

14.2 Level-Up Celebration

Trigger: Point award causes total to cross a level threshold.

Animation sequence:

  1. Dimmed overlay.
  2. Level badge zooms in from center.
  3. Confetti burst.
  4. Text: "Level up! You reached [Level Name]."
  5. Animated points counter.
  6. "X more points to [next level]."
  7. [Awesome!] dismisses.

BR-CELEB-01: Reduced motion enabled: skip confetti and zoom. Static modal only. BR-CELEB-02: Multiple level-ups in one session: show celebrations sequentially.

14.3 Shareable Impact Badge

  1. User taps "Share my impact" on Impact Dashboard.
  2. Render native bitmap (512×512 px): Team Move logo, level badge (centered), stats ("X kg CO₂ saved • Y km shared • [Level] member"), current date, "teammoveapp.com."
  3. Save to camera roll (with write permission) + open native share sheet.
  4. Share pre-populated text: "I saved X kg of CO₂ with Team Move! Join: https://teammoveapp.com/join-community?code=XXXX."

BR-BADGE-01: Camera roll permission required: NSPhotoLibraryAddUsageDescription (iOS); no permission on Android API 29+ (MediaStore). BR-BADGE-02: Use most active hub invite code, if member of any hub.


15. API Specifications

15.1 Backend Function Call Reference

POST https://api.base44.com/v1/functions/{functionName}
Authorization: Bearer {session_token}
Content-Type: application/json

calculateRouteDistance

Request:  { "origin": "12 rue de la Paix, Paris", "destination": "Lyon, France" }
Response: { "distance_km": 465.2, "duration_minutes": 245, "status": "ok" }
Errors:   { "error": "GEOCODE_FAILED" } | { "error": "API_UNAVAILABLE" }

getCommunityByCode

Request:  { "code": "ABCD1234" }
Response: { "community": { "id", "name", "description", "members_count", "cover_image_url" } }
404:      { "error": "NOT_FOUND", "message": "Hub not found" }

searchTrains

Request:  { "origin_city": "Paris", "destination_city": "Lyon", "date": "2026-07-15", "time": "08:00" }
Response: { "trains": [{ "departure_time", "arrival_time", "duration_minutes", "changes", "train_number", "is_direct" }] }
Errors:   { "error": "API_UNAVAILABLE" } | { "error": "NO_RESULTS" }

searchLuxembourgTrains

Request:  { "origin": "Luxembourg Gare", "destination": "Esch-sur-Alzette", "date": "2026-07-15" }
Response: { "journeys": [{ "departure", "arrival", "duration_minutes", "legs": [] }] }

notifyCommunityActivity

Request:  { "community_id", "type", "title", "message", "action_url", "exclude_email" }
Response: { "sent_count": 11 }

sendEventInvitations

Request:  { "event_id", "recipients": [{ "email", "name" }], "subject", "body" }
Response: { "sent": 45, "failed": 2, "failed_emails": ["invalid@x"] }

generateAvatar

Request:  { "user_email", "style": "cartoon" }
Response: { "avatar_url": "https://storage.base44.com/..." }
Error:    HTTP 500 { "error": "GENERATION_FAILED" }

createStripeSubscription

Request:  { "plan_type": "essentiel", "billing_cycle": "annual" }
Response: { "client_secret", "customer_id", "ephemeral_key" }
400:      { "error": "INVALID_PLAN" }
409:      { "error": "SUBSCRIPTION_EXISTS" }

createEventPaymentIntent

Request:  { "event_id", "user_email" }
Response: { "client_secret", "amount": 2500, "currency": "eur" }
404:      { "error": "EVENT_NOT_FOUND" }
400:      { "error": "EVENT_NOT_PAID" }

communityAdminActions

Regenerate code:
Request:  { "action": "regenerate_invite_code", "community_id" }
Response: { "new_invite_code": "XYZ12345" }

Delete hub:
Request:  { "action": "delete_hub", "community_id" }
Response: { "deleted": true, "records_deleted": { "members", "messages", "trips" } }

403:      { "error": "FORBIDDEN", "message": "Only hub admin or platform admin can perform this action" }

15.2 Entity Filter Query Syntax

filter({ status: "actif" })                                    // equality
filter({ events_quota: { $gte: 5 } })                         // comparison
filter({ plan_type: { $in: ["essentiel", "pro"] } })           // in array
filter({ "passengers.email": "user@example.com" })             // nested array
filter({ date: { $gte: "2026-07-01", $lte: "2026-07-31" } })  // date range
filter({ $or: [{ driver_email: "u@e" }, { "passengers.email": "u@e" }] })
list("-created_date", 50)  // sort desc, limit 50
filter({ community_id: "abc", status: "open" }, "-date", 20)

15.3 Rate Limits

| Endpoint type | Limit | Window | |--------------|-------|--------| | Entity reads | 1000 req | 1 minute | | Entity writes | 200 req | 1 minute | | Backend functions | 100 req | 1 minute | | File uploads | 20 req | 1 minute | | Auth endpoints | 10 req | 1 minute |

BR-API-01: On HTTP 429: exponential backoff (1s, 2s, 4s, 8s). After 4 retries: surface error to user. BR-API-02: Auth: client-side rate limit max 3 magic link requests per 10 min. Show countdown.


16. Database & Data Model

16.1 Entity Relationship Diagram (Text)

User
  ├── 1:N → Participation (user_email)
  ├── 1:N → ImpactPoint (user_email)
  ├── 1:N → Notification (user_email)
  ├── 1:N → CommunityMember (user_email)
  ├── 1:N → CommunityMessage (sender_email)
  ├── 1:N → CommunityDirectMessage (sender OR recipient email)
  ├── 1:N → CommunityTrip (author_email)
  ├── 1:N → Carpool (driver_email)
  ├── 1:N → Event (organizer_email)
  └── 0:1 → Subscription (user_email)

Event
  ├── 1:N → Participation (event_id)
  ├── 1:N → Carpool (event_id)
  ├── 1:N → Shuttle (event_id)
  ├── 1:N → EventInvitation (event_id)
  └── 1:N → EventMessage (event_id)

Community
  ├── 1:N → CommunityMember (community_id)
  ├── 1:N → CommunityMessage (community_id)
  ├── 1:N → CommunityDirectMessage (community_id)
  ├── 1:N → CommunityTrip (community_id)
  └── 1:N → CommunityEvent (community_id)

Structure
  ├── 1:N → StructureMember (structure_id)
  └── 0:1 → Subscription (structure_id)

Reward → 1:N → RewardRedemption (reward_id)

16.2 Denormalized Fields — Rationale

| Field | Entity | Source | Rationale | |-------|--------|--------|-----------| | driver_name | Carpool | User.full_name | Snapshot at creation time | | author_pseudo | CommunityTrip | CommunityMember.pseudo | Pseudo can change | | sender_pseudo | CommunityMessage | CommunityMember.pseudo | Historical message context | | event_title | Notification | Event.title | Fast rendering | | community_name | Notification | Community.name | Fast rendering | | members_count | Community | COUNT(CommunityMember) | Avoid aggregate on list |

BR-DB-01: Denormalized fields set at creation, NOT updated if source changes (intentional). BR-DB-02: members_count is the only counter that must stay in sync (increment on join, decrement on leave/remove).

16.3 Row-Level Security Policy Table

| Entity | Create | Read | Update | Delete | |--------|--------|------|--------|--------| | Event | organizer = user | public OR organizer OR admin | organizer OR admin | organizer OR admin | | Participation | user_email = user | own OR admin | own OR admin | own OR admin | | EventInvitation | organizer = user | organizer OR invited OR admin | organizer OR invited OR admin | organizer OR admin | | Carpool | driver = user | driver OR passenger OR admin | driver OR admin | driver OR admin | | CarpoolMessage | sender = user | carpool members OR admin | own OR admin | own OR admin | | Shuttle | admin only | admin only | admin only | admin only | | Community | owner = user | public (all) | owner OR admin | owner OR admin | | CommunityMember | user_email = user | public (all) | own OR admin | own OR admin | | CommunityMessage | sender = user | public (all) | own OR admin | own OR admin | | CommunityDirectMessage | sender = user | sender OR recipient OR admin | sender OR recipient OR admin | sender OR admin | | CommunityEvent | organizer = user | public (all) | organizer OR admin | organizer OR admin | | CommunityTrip | author = user | public (all) | author OR admin | author OR admin | | ImpactPoint | user_email = user | own OR admin | admin only | admin only | | Challenge | admin only | public (all) | admin only | admin only | | Reward | admin only | public (all) | admin only | admin only | | RewardRedemption | user_email = user | own OR admin | admin only | admin only | | Notification | system only | own OR admin | own OR admin | own OR admin | | Subscription | user_email = user | own OR admin | admin only | admin only | | Structure | admin only | admin only | admin only | admin only | | ContentReport | reporter = user | own OR admin | admin only | admin only | | BlockedUser | blocker = user | own OR admin | own OR admin | own OR admin | | NewsEvent | admin only | is_active=true OR admin | admin only | admin only | | AuditLog | system only | admin only | admin only | admin only | | Consent | user_email = user | own OR admin | own OR admin | own OR admin |


17. iOS Requirements

17.1 Deployment Targets

| Requirement | Value | |------------|-------| | Minimum iOS | 16.0 | | Devices | iPhone 12+ (primary), iPad (same codebase) | | Orientations | Portrait primary; Landscape on iPad | | Dark mode | Required |

17.2 Required Entitlements & Capabilities

| Entitlement | Key | Required for | |------------|-----|-------------| | Push Notifications | aps-environment | All push notifications | | Associated Domains | applinks:teammoveapp.com | Universal Links | | Background Modes | fetch, remote-notification | Background refresh | | Location When In Use | NSLocationWhenInUseUsageDescription | GPS features | | Camera | NSCameraUsageDescription | QR, photo capture | | Photo Library Read | NSPhotoLibraryUsageDescription | Image upload | | Photo Library Add | NSPhotoLibraryAddUsageDescription | Save impact badge | | Microphone | NSMicrophoneUsageDescription | Voice messages | | Face ID | NSFaceIDUsageDescription | Biometric (v2) |

17.3 Info.plist Privacy Strings (All Required for App Review)

<key>NSLocationWhenInUseUsageDescription</key>
<string>Team Move uses your location to suggest nearby pickup points for carpooling and to calculate trip distances for CO₂ impact tracking.</string>

<key>NSCameraUsageDescription</key>
<string>Team Move uses your camera to scan event check-in QR codes, photograph event images, and record voice messages.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>Team Move accesses your photo library to upload profile pictures, hub cover images, and attach images to chat messages.</string>

<key>NSPhotoLibraryAddUsageDescription</key>
<string>Team Move saves your impact badge to your photo library so you can share it.</string>

<key>NSMicrophoneUsageDescription</key>
<string>Team Move uses the microphone to record voice messages in community chats and direct messages.</string>

<key>NSFaceIDUsageDescription</key>
<string>Team Move can use Face ID to unlock the app quickly and securely.</string>

17.4 Universal Links — Deep Link Routes

App must register and handle:

| Path pattern | Destination | |-------------|------------| | /auth/verify?token=* | Auth verify screen | | /join-community?code=* | Hub join screen | | /event-invite?token=* | Event invitation screen | | /event-landing?event_id=* | Event detail | | /event-register?event_id=* | Event registration | | /event-onboarding?* | Post-registration onboarding | | /community/* | Hub detail | | /notifications | Notification center |

BR-IOS-01: If app not installed: Universal Link falls back to web. BR-IOS-02: If app installed but user not logged in: save URL, redirect to login, navigate after login.

17.5 App Store Requirements Checklist

  • [ ] App name: "Team Move"
  • [ ] Subtitle: "Community Mobility & Carpooling" (max 30 chars)
  • [ ] Screenshots: iPhone 6.7" (required), 6.5", 5.5", iPad 12.9" (required if universal)
  • [ ] Privacy nutrition label: Contact info, User content, Location, Device IDs, Usage data
  • [ ] Age rating: 4+
  • [ ] Category: Social Networking (primary), Travel (secondary)

Apple IAP decision required before submission:

  • Option A: Remove all pricing/payment from iOS app. Link to web (https://teammoveapp.com) for subscription management. Avoids IAP requirement.
  • Option B: Implement Apple IAP wrapper with server-side receipt validation. Additional development required.
  • Option A is strongly recommended for initial submission.

17.6 iOS UX Requirements

Safe areas: All screens must respect notch, Dynamic Island, home indicator:

.safeAreaInset(edge: .bottom)  // for chat input, tab bars

Navigation: Use NavigationStack (iOS 16+). Large titles on: Home, Communities, Impact, Notifications. Standard titles on detail screens. Sheets for modals and filters.

Haptic feedback: | Event | Type | |-------|------| | Carpool joined | UINotificationFeedbackGenerator.success | | Level up | UINotificationFeedbackGenerator.success | | QR scanned | UINotificationFeedbackGenerator.success | | Message sent | UIImpactFeedbackGenerator.light | | Validation error | UINotificationFeedbackGenerator.error |

Dynamic Type: All text via semantic font styles (.headline, .body, .caption). Never hardcoded sizes. Test at xSmall and AX5 sizes. UI must not break.

Pull-to-refresh: refreshable modifier (SwiftUI) or UIRefreshControl (UIKit) on all list screens.

Keyboard avoidance: Chat input toolbar: safeAreaInset above keyboard. Forms: scroll view auto-scrolls to focused field.

17.7 QR Scanning — Implementation

// iOS 16+ VisionKit (preferred):
import VisionKit
let scanner = DataScannerViewController(
  recognizedDataTypes: [.barcode(symbologies: [.qr])],
  qualityLevel: .balanced
)
// Handle: extract barcode.payloadStringValue
// Validate: must start with "TEAMM:"
// Parse: participation_id = payload.dropFirst("TEAMM:".count)

BR-IOS-03: Green overlay animation on successful scan. Auto-dismiss scanner after 2s. BR-IOS-04: Camera permission denied: "Open Settings" button → UIApplication.openSettingsURLString.

17.8 Offline Behavior — iOS

Caching: URLCache (maxMemory: 50MB, maxDisk: 200MB). Cache keyed with auth token hash for per-user isolation.

Connectivity detection:

import Network
NWPathMonitor().pathUpdateHandler = { path in
  isOnline = path.status == .satisfied
}

Offline queue: Chat messages queued in CoreData. Processed on network reconnect.

| Feature | Online | Offline | |---------|--------|---------| | View event list | Live API | Cached + "Last updated X min ago" banner | | View hub chat | Live + subscribed | Cached; input disabled | | Send chat message | Immediate (optimistic) | Queued; sent on reconnect | | Join carpool | Live API | Blocked: "Internet required" | | Create event | Live API | Blocked | | View own QR code | Live | Available offline (store participation_id) | | Notifications | Live | APNs delivers when reconnected |


18. Android Requirements

18.1 SDK and Build Configuration

android {
  compileSdk 35
  defaultConfig {
    minSdk 26           // Android 8.0 Oreo
    targetSdk 35        // Android 15
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_17
    targetCompatibility JavaVersion.VERSION_17
  }
  buildFeatures { viewBinding true; buildConfig true }
}

18.2 Required Permissions

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.VIBRATE" />

Runtime permission request strategy:

| Permission | When to request | Rationale | |-----------|----------------|-----------| | POST_NOTIFICATIONS | First launch (API 33+) | "Get notified about carpools and events" | | CAMERA | First QR scan or camera open | "Required to scan codes and take photos" | | ACCESS_FINE_LOCATION | First address autocomplete use | "Used to suggest nearby addresses" | | READ_MEDIA_IMAGES | First gallery image select | "Required to upload event photos" | | RECORD_AUDIO | First voice message tap | "Required to record voice messages" |

BR-AND-01: Never request all permissions on launch. Request at moment of first use. BR-AND-02: Permanently denied: "Enable in Settings → App permissions → Team Move." with [Open Settings] button.

18.3 FCM Configuration

Add google-services.json (provided by Team Move engineering) to app/ directory.

class TeamMoveMessagingService : FirebaseMessagingService() {
  override fun onNewToken(token: String) {
    registerPushTokenWithBackend(token, "android")
  }
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
    val title = remoteMessage.notification?.title ?: remoteMessage.data["title"]
    val body = remoteMessage.notification?.body ?: remoteMessage.data["body"]
    val actionUrl = remoteMessage.data["action_url"]
    val channelId = remoteMessage.data["channel_id"] ?: "general"
    showLocalNotification(title, body, actionUrl, channelId)
  }
}

Create channels at app startup (before any notification can arrive):

fun createNotificationChannels(context: Context) {
  listOf(
    Triple("carpool", "Carpool Updates", NotificationManager.IMPORTANCE_HIGH),
    Triple("messages", "Messages", NotificationManager.IMPORTANCE_HIGH),
    Triple("reminders", "Event Reminders", NotificationManager.IMPORTANCE_DEFAULT),
    Triple("community", "Community Activity", NotificationManager.IMPORTANCE_DEFAULT),
    Triple("general", "General", NotificationManager.IMPORTANCE_LOW)
  ).forEach { (id, name, importance) ->
    NotificationChannel(id, name, importance).also {
      getSystemService(NotificationManager::class.java).createNotificationChannel(it)
    }
  }
}

18.4 Android App Links

Digital Asset Links (at https://teammoveapp.com/.well-known/assetlinks.json):

[{ "relation": ["delegate_permission/common.handle_all_urls"],
   "target": { "namespace": "android_app",
     "package_name": "com.teammove.app",
     "sha256_cert_fingerprints": ["<release SHA256>", "<debug SHA256>"] } }]

Intent filter in AndroidManifest.xml:

<activity android:name=".MainActivity">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="teammoveapp.com" />
  </intent-filter>
</activity>

18.5 Android UX Requirements

Material Design 3: Use MaterialTheme with custom scheme. Primary color: teal #0D9488. Dynamic Color (Android 12+): map to brand, not system-generated.

Edge-to-edge:

WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
  val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
  v.updatePadding(top = bars.top, bottom = bars.bottom)
  insets
}

Predictive back (Android 13+): Register OnBackPressedCallback. Enable android:enableOnBackInvokedCallback="true" in manifest.

Adaptive icon:

<adaptive-icon>
  <background android:drawable="@color/teal_600" />
  <foreground android:drawable="@drawable/ic_launcher_foreground" />
  <monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Splash screen (Android 12+): Use SplashScreen API with Team Move logo on teal background.

18.6 QR Scanning — Android Implementation

// ML Kit Barcode Scanning with CameraX:
val options = BarcodeScannerOptions.Builder()
  .setBarcodeFormats(Barcode.FORMAT_QR_CODE).build()
val scanner = BarcodeScanning.getClient(options)

imageAnalysis.setAnalyzer(executor) { imageProxy ->
  val input = InputImage.fromMediaImage(imageProxy.image!!, imageProxy.imageInfo.rotationDegrees)
  scanner.process(input)
    .addOnSuccessListener { barcodes ->
      barcodes.firstOrNull { it.rawValue?.startsWith("TEAMM:") == true }?.let { barcode ->
        val participationId = barcode.rawValue!!.removePrefix("TEAMM:")
        handleCheckIn(participationId)
      }
    }
    .addOnCompleteListener { imageProxy.close() }
}

18.7 Google Play Requirements Checklist

  • [ ] Data safety form: declare contact info, location, messages, device IDs.
  • [ ] Privacy policy URL live: https://teammoveapp.com/privacy.
  • [ ] 64-bit support: arm64-v8a ABI for all native libraries.
  • [ ] Target API 35+.
  • [ ] No SCHEDULE_EXACT_ALARM — use WorkManager.
  • [ ] READ_MEDIA_IMAGES on API 33+ (not READ_EXTERNAL_STORAGE).
  • [ ] Google Play App Signing enrolled.
  • [ ] Content rating: IARC questionnaire (expected: Everyone / PEGI 3).

18.8 Offline Behavior — Android

Room database for local cache:

  • Tables: cached_events, cached_hub_messages, cached_notifications, pending_send_queue.
  • TTL enforced by last_fetched timestamp column.

Network monitoring:

val cm = getSystemService(ConnectivityManager::class.java)
val cb = object : ConnectivityManager.NetworkCallback() {
  override fun onAvailable(network: Network) { processPendingSendQueue() }
  override fun onLost(network: Network) { showOfflineBanner() }
}
cm.registerDefaultNetworkCallback(cb)

WorkManager background sync (every 15 min when connected):

val request = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
  .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
  .build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("sync", KEEP, request)

19. Security

19.1 Authentication Security

| Threat | Mitigation | |--------|-----------| | Session token theft | Keychain/Keystore only. Never logged. Never in URLs or query params. | | Magic link replay | Single-use. Server-side invalidated on first use. | | Token expiry window | 15-minute OTP expiry limits attack window. | | Admin privilege escalation | Role verified server-side on every admin API call. Local role = display only. | | Root/jailbreak bypass | Optional detection (JailMonkey or equivalent). Log only — do not hard-block. | | Shoulder surfing | No tokens displayed in UI at any time. |

19.2 Network Security

Certificate pinning (recommended for production):

// iOS: pin to intermediate CA (not leaf) to avoid breakage on certificate renewal
URLSession configured with custom URLAuthenticationChallenge handler checking pinned public key.
// Android: OkHttp CertificatePinner
OkHttpClient.Builder()
  .certificatePinner(CertificatePinner.Builder()
    .add("api.base44.com", "sha256/<intermediate-ca-hash>")
    .build())

BR-SEC-01: Pin to intermediate CA, not leaf certificate.

Android network security config:

<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.base44.com</domain>
    <domain includeSubdomains="true">storage.base44.com</domain>
  </domain-config>
</network-security-config>

iOS: All domains must be HTTPS. NSAppTransportSecurity allows no exceptions.

19.3 Local Storage Security

  • iOS: Keychain items with kSecAttrAccessibleAfterFirstUnlock. Survives reboot, not accessible until first unlock.
  • Android: EncryptedSharedPreferences (AES-256) for tokens. Room database with SQLCipher for sensitive cached data (optional — evaluate risk).
  • BR-SEC-02: Chat message cache cleared on logout.
  • BR-SEC-03: Payments: never store card data locally. All via Stripe SDK (PCI DSS SAQ-A).

19.4 App Hardening

iOS:

  • Disable debug symbols in release builds.
  • Strip symbols from release IPA.
  • Enable bitcode: No (deprecated Xcode 14+).

Android:

  • R8 code shrinking and obfuscation in release.
  • android:debuggable="false" in release manifest.
  • android:allowBackup="false" to prevent backup of session tokens.
  • StrictMode in debug builds to detect thread violations.

19.5 Input Validation Reference

| Input | Client validation | Server validation | |-------|------------------|------------------| | Email | RFC 5321 format, max 254 chars | Duplicate check, domain validation | | Hub name | 3–100 chars, printable only | Same | | Pseudo | 2–30 chars, alphanumeric + space/hyphen/underscore | Same + hub uniqueness | | Invite code | 8 alphanumeric, auto-uppercase | Existence + validity | | Message text | Max 4000 chars, not empty | Same | | File upload | Type whitelist, size limit | MIME type validation server-side | | Date fields | Valid date, >= today where required | Same | | Price | > 0, max 9999.99 EUR | Same | | seats_available | 1–7 integer | Same | | price_per_km | > 0 (warning if > 0.32) | Logged for compliance |

19.6 Privacy — Data Minimisation

  • BR-PRIV-01: GPS not collected on every app open. Only when user provides a departure address.
  • BR-PRIV-02: Email addresses shown only to: the user, their event organizers, platform admins. Never in hub member lists (pseudo only).
  • BR-PRIV-03: Real names in user's own profile only. Hub contexts: pseudo only.
  • BR-PRIV-04: Push notification body for DMs: sender pseudo + hub name only. Not message content.
  • BR-PRIV-05: Production builds must not log email addresses, names, or location data.
  • BR-PRIV-06: Analytics: no email, name, or exact location in event properties. Use hashed user ID.

20. Performance & Scalability

20.1 Performance Targets

| Metric | P50 | P90 | P99 | |--------|-----|-----|-----| | App cold start | < 1.5s | < 2.5s | < 4.0s | | App warm start | < 0.4s | < 0.8s | < 1.5s | | Tab switch | < 100ms | < 200ms | < 400ms | | Full screen load (4G) | < 800ms | < 1500ms | < 3000ms | | Chat message send → visible | < 200ms | < 400ms | < 800ms | | Image upload (5MB, 4G) | < 3s | < 5s | < 10s | | Map load (50 pins) | < 500ms | < 1000ms | < 2000ms | | QR scan (good light) | < 500ms | < 1000ms | < 2000ms |

20.2 Rendering Performance

  • BR-PERF-01: All list screens at 60 FPS while scrolling. No dropped frames.
  • BR-PERF-02: Lists > 50 items: virtual/lazy rendering (LazyVStack/LazyColumn). Never load all into memory.
  • BR-PERF-03: Skeleton loading on all data-fetched views. No blank white screens.
  • BR-PERF-04: Images: progressive loading (blur placeholder → full). LRU cache (100MB disk, 30MB memory).
  • BR-PERF-05: No synchronous operations on main thread. All network, disk, and image processing on background queues.

20.3 Image Compression (Required)

Before any image upload:

  • Reduce to max 1920×1080 px (maintain aspect ratio).
  • Compress to WebP quality 80.
  • Fallback to JPEG quality 75 if WebP unsupported.
  • Target max upload size: 2 MB.

Voice messages: Opus codec (ogg container) at 32kbps mono. Typical 1-minute recording < 250 KB.

20.4 Caching Strategy

| Data | TTL | Storage | Invalidation | |------|-----|---------|-------------| | Event list | 5 min | Memory + disk | Pull-to-refresh, event mutation | | Event detail | 2 min | Memory | Navigation back, pull-to-refresh | | Hub chat | 30 sec | Memory | New message subscription | | Hub members | 10 min | Memory + disk | Member join/leave | | Community list | 5 min | Memory + disk | Hub created/joined | | Notifications | 60 sec | Memory | New notification received | | Impact points | 5 min | Memory | Point awarded | | NewsEvents | 30 min | Disk | Admin update | | Carpool list | 2 min | Memory | Carpool joined/created |

20.5 Pagination

All list endpoints support offset pagination (limit/skip). Default page size: 20.

  • BR-PERF-06: Cursor-based or offset pagination for all lists.
  • BR-PERF-07: Infinite scroll: load next page when within 5 items of end.
  • BR-PERF-08: "Loading more..." indicator during pagination.
  • BR-PERF-09: Subscription prepends new items to top of cached list.

20.6 Scalability Constraints

| Constraint | Current limit | Design implication | |-----------|-------------|-------------------| | Real-time subscriptions | ~500 concurrent per entity | Subscribe only to currently-visible entities | | Chat message volume | ~1000 messages/hub before degradation | Always paginate. Never load all. | | Notification count | 1000/user retained | Display max 100 in UI. "Load more" for older. | | API rate limit | 1000 reads/min | Cache aggressively. Deduplicate within 1s. | | Backend function timeout | 30 seconds | Do not call in user-blocking context if > 15s. |


21. Accessibility

21.1 Standards

WCAG 2.1 Level AA | Apple HIG Accessibility | Material Accessibility Guidelines.

21.2 Requirements

| Requirement | Target | |-------------|--------| | Touch target | Min 44×44 pt (iOS) / 48×48 dp (Android) | | Dynamic Type | All text scales with system font size | | Screen reader | VoiceOver (iOS) and TalkBack (Android) — all elements labeled | | Colour contrast | Min 4.5:1 (normal text), 3:1 (large text) | | Focus management | Modals trap focus. Back navigation restores focus. | | Error messaging | Text only — never colour alone | | Loading states | Skeleton screens with accessible labels | | Images | Alt text / contentDescription on all informational images | | Animations | Respect OS reduced-motion preference | | Error association | Error messages linked to input fields via accessibilityLabel |

Reduced motion implementation:

  • iOS: check UIAccessibility.isReduceMotionEnabled. If true: replace animations with crossfades.
  • Android: check Settings.Global.TRANSITION_ANIMATION_SCALE == 0. If so: skip animations.
  • Confetti (level-up): completely skipped if reduced motion active.

Screen reader specifics:

  • iOS: all interactive elements have meaningful accessibilityLabel. Custom components set accessibilityTraits.
  • Android: all interactive views have contentDescription. Custom views implement AccessibilityDelegate.
  • Decorative images: accessibilityHidden = true (iOS) / importantForAccessibility="no" (Android).
  • Action confirmations announced: e.g., "Carpool joined. 3 seats remaining."

Focus management:

  • Modal open: focus moves to first interactive element.
  • Modal dismiss: focus returns to triggering element.
  • Form validation error: focus moves to first error field.

22. Non-Functional Requirements

22.1 Availability

| Metric | Target | |--------|--------| | Platform uptime | 99.5% (Base44 SLA) | | App crash rate | < 0.1% of sessions | | ANR rate (Android) | < 0.1% | | Launch failure rate | < 0.5% |

Graceful degradation levels:

  1. Full connectivity: all features available.
  2. Poor connectivity (> 1000ms): skeleton screens, no timeout < 15s.
  3. Intermittent: optimistic UI for writes, queue failed operations.
  4. No connectivity: read-only from cache, writes queued, persistent "Offline mode" banner.
  5. Backend 5xx: "Service temporarily unavailable. Try again later." Exponential backoff retry.

22.2 Internationalisation (i18n)

  • Current UI: French (platform), English (admin/SRS).
  • All UI strings externalised: Localizable.strings (iOS) / strings.xml (Android). No hardcoded strings.
  • Date/time: DateFormatter (iOS) / DateTimeFormatter (Android) with locale.
  • Currency: NumberFormatter.currencyStyle with locale.
  • Numbers: locale-aware formatting (1 234,50 EUR for FR vs 1,234.50 EUR for EN).
  • Plurals: use platform plural rules.

Future: English and Spanish full localisation. String file structure must be in place from v1.0.

22.3 Analytics Events

| Event | Trigger | Properties | |-------|---------|-----------| | app_open | App launched | { source: "cold"/"warm", version } | | screen_view | Screen appears | { screen_name, context_id (optional) } | | carpool_created | Carpool entity created | { event_id, seats, trip_type } | | carpool_joined | Passenger joins | { carpool_id, event_id } | | trip_posted | CommunityTrip created | { type, transport_mode } | | trip_interest_expressed | Interest added | { trip_id, auto_accepted } | | event_registered | Participation created | { event_id, transport_mode, role } | | event_created | Event entity created | { event_type, access_type, visibility } | | hub_created | Community entity created | {} | | hub_joined | CommunityMember created | {} | | reward_redeemed | RewardRedemption created | { reward_id, cost_points } | | subscription_upgrade_intent | User taps upgrade | { from_plan, to_plan } | | subscription_upgraded | Status = actif | { plan_type, billing_cycle } | | push_received | Push notification received | { type } | | push_tapped | User taps push | { type } | | qr_scanned | Successful QR scan | { event_id } | | error_displayed | User-visible error | { error_code, screen } |

Provider: Firebase Analytics (or equivalent abstraction layer for swappability).

22.4 Crash Reporting

Firebase Crashlytics (recommended):

  • All unhandled exceptions auto-reported.
  • Log non-fatal errors for recoverable states.
  • Attach hashed user identifier (not plain email — GDPR).
  • Attach screen name and last user action to crash context.

22.5 Force Upgrade

GET /functions/getAppConfig
→ { min_version: "1.0.0", latest_version: "1.2.0", force_update: false }
  • force_update = true AND current < min_version: block app with modal "A required update is available." Non-dismissable.
  • latest > current AND force_update = false: soft prompt "A new version is available." Dismissable.

22.6 Error Handling Reference

| Status | Scenario | User message | Action | |--------|---------|-------------|--------| | Network timeout > 15s | Connection slow | "Connection timed out. Check your internet." | Retry button | | Network error | No connectivity | "No internet connection." | Persistent banner | | 400 | Validation error | Field-level error from response body | Highlight field | | 401 | Session expired | "Your session has expired. Please log in again." | Navigate to login | | 403 | Forbidden | "You don't have permission to do this." | Navigate back | | 404 | Resource deleted | "This content is no longer available." | Navigate back | | 409 | Conflict | Contextual (e.g., "Already registered") | Contextual message | | 429 | Rate limited | "Too many requests. Please wait a moment." | Retry after delay | | 500 | Server error | "Something went wrong on our end. Try again." | Retry button | | 503 | Maintenance | "Team Move is under maintenance. Check back soon." | Auto-retry 30s |

Retry strategy:

  1. Immediate → 2. After 1s → 3. After 2s → 4. After 4s. After 4 failures: persistent error with "Try again" and "Contact support" options.

Optimistic UI rollback: When optimistic update fails: revert local state immediately, show toast "[Action] failed. Changes reversed." Never leave UI in inconsistent state.

Empty states (all lists must handle):

| Screen | Message | CTA | |--------|---------|-----| | Event list | "No events found. Adjust filters or create one." | [Create event] | | Hub list | "No hubs yet. Create your first community hub!" | [Create hub] | | Trip board | "No trips posted yet. Be the first!" | [Post a trip] | | Notifications | "You're all caught up! No new notifications." | — | | Carpool list | "No carpools yet. Offer a ride!" | [Offer a ride] | | DM inbox | "No messages yet. Send a message to a member!" | — | | Impact history | "No impact recorded yet. Join an event to earn points!" | [Browse events] |


23. GDPR & Privacy

23.1 Data Inventory

| Category | Data | Storage | Retention | Legal basis | |----------|------|---------|----------|------------| | Identity | Email address, full name | User entity | Account lifetime | Contract | | Transport | Departure address, GPS coords | Participation entity | 3 years post-event | Consent | | Behavioural | App events, screen views | Analytics | 24 months | Legitimate interest | | Behavioural | Impact point history | ImpactPoint entity | Account lifetime | Legitimate interest | | Content | Chat messages | CommunityMessage | 2 years | Contract | | Content | DM messages | CommunityDirectMessage | 2 years | Contract | | Content | Trip posts | CommunityTrip | 2 years | Contract | | Content | Uploaded files | Base44 storage | Account lifetime | Contract | | Financial | Stripe customer ID (no card data on platform) | Subscription entity | 7 years (legal obligation) | Legal obligation | | Device | Push notification token | Backend server | Until unregistered | Consent | | Device | Crash reports | Crashlytics | 90 days | Legitimate interest |

23.2 User Rights Implementation

| Right | Mobile Implementation | |-------|----------------------| | Right of Access (Art. 15) | Profile → Privacy → "Export my data" → JSON download within 24h | | Right to Rectification (Art. 16) | Profile edit at any time. Participation transport mode updatable (free events). | | Right to Erasure (Art. 17) | Profile → Privacy → "Delete account" → confirmation → magic link verify → anonymization | | Right to Portability (Art. 20) | JSON export (structured, machine-readable) | | Right to Object (Art. 21) | Notification opt-out per category in Profile → Notifications |

23.3 Account Deletion Flow

  1. Profile → Privacy → "Delete my account."
  2. Warning: "This is irreversible. All your data will be deleted within 30 days."
  3. Enter email to confirm.
  4. Verify via magic link (prevents accidental deletion).
  5. On confirmation:
    • User record: email replaced with hash, name cleared.
    • Participation records: deleted.
    • ImpactPoint records: deleted.
    • Chat messages: content → "[Message deleted]", sender_email anonymised.
    • CommunityMember records: deleted.
    • CommunityTrip records: deleted.
    • Stripe: request data deletion via Stripe API.
    • Hub ownership: hub remains (admin-less); platform admin can manage.
  6. Retention exceptions: Subscription financial records 7 years (legal). AuditLog 5 years.

23.4 Consent Collection on Mobile

Push notifications:

  • Before system dialog: in-app explanation "Get notified about your carpools and events. You can change this in Settings."
  • iOS: requestAuthorization once. Never force-re-request after denial.
  • Android 13+: POST_NOTIFICATIONS once.
  • Re-request: at most once per 30 days if denied.

Location:

  • Request at first use of address autocomplete or carpool feature.
  • Explanation: "Used to suggest pickup points and calculate CO₂ savings."
  • iOS: requestWhenInUseAuthorization only. Never requestAlwaysAuthorization.

Analytics: Collected under legitimate interest. Opt-out in Profile → Privacy → "Opt out of analytics."

23.5 Data Breach Protocol

BR-GDPR-01: If security incident detected:

  1. Platform engineering notified immediately via criticalAlertNotifier.
  2. Incident assessment within 4 hours.
  3. If personal data compromised: CNIL notification within 72 hours.
  4. If high risk to user rights: affected users notified without undue delay.
  5. Incident logged in AuditLog.

24. Error Handling Reference

24.1 Module-Specific Error Catalogue

Authentication: | Code | Scenario | User message | |------|---------|-------------| | AUTH_TOKEN_EXPIRED | Session expired | "Session expired. Please log in again." | | AUTH_MAGIC_LINK_EXPIRED | > 15 min old | "This link has expired." + Resend button | | AUTH_MAGIC_LINK_USED | Already consumed | "This link has already been used." + Resend | | AUTH_USER_NOT_FOUND | Not registered | "No account found. Contact support." | | AUTH_TOO_MANY_REQUESTS | Rate limited | "Too many attempts. Wait X minutes." |

Hub: | Code | Scenario | User message | |------|---------|-------------| | HUB_NOT_FOUND | Invalid invite code | "Hub not found. Check the code and try again." | | HUB_ALREADY_MEMBER | Duplicate join | "You are already a member of this hub." | | HUB_PSEUDO_TAKEN | Pseudo not unique | "This name is already taken. Please choose another." | | HUB_NOT_MEMBER | Accessing without membership | "You are not a member of this hub." | | HUB_ADMIN_CANNOT_LEAVE | Admin tries to leave | "As admin, you cannot leave. Delete the hub or contact support." |

Event: | Code | Scenario | User message | |------|---------|-------------| | EVENT_NOT_FOUND | Deleted or bad ID | "This event is no longer available." | | EVENT_FULL | max_participants reached | "This event is full. Contact the organizer." | | EVENT_ALREADY_REGISTERED | Duplicate | "You are already registered." + link to participation | | EVENT_WRONG_CODE | Bad access code | "Incorrect access code. Please try again." | | EVENT_QUOTA_EXCEEDED | Plan limit | "You have reached your plan's event limit. Upgrade to create more." | | EVENT_PAYMENT_REQUIRED | Unpaid paid event | "Payment is required to register." | | EVENT_CLOSED | Registration closed | "Registration for this event is closed." |

Carpool: | Code | Scenario | User message | |------|---------|-------------| | CARPOOL_FULL | No seats | "This carpool is full." | | CARPOOL_ALREADY_JOINED | Already a passenger | "You are already in a carpool for this trip." | | CARPOOL_NOT_REGISTERED | Not event participant | "Register for the event before joining a carpool." |

Trip board: | Code | Scenario | User message | |------|---------|-------------| | TRIP_PAST_DATE | Date in past | "The trip date must be in the future." | | TRIP_SAME_CITY | Same cities | "Departure and destination must be different." | | TRIP_ALREADY_INTERESTED | Duplicate | "You have already expressed interest in this trip." | | TRIP_FULL | auto_accept + no seats | "Sorry, this trip is now full." |

Payment: | Code | Scenario | User message | |------|---------|-------------| | PAYMENT_FAILED | Card declined | "Payment failed: [Stripe error]." | | PAYMENT_INTENT_ERROR | Cannot initialize | "Could not initialize payment. Try again later." | | PAYMENT_ALREADY_ACTIVE | Duplicate sub | "You already have an active subscription." |


25. Development Backlog

25.1 Priority 1 — Core Mobile MVP (Required for Launch)

| # | Feature | Module | Acceptance criteria | |---|---------|--------|-------------------| | 1 | Magic link auth + session | Auth | Login, Keychain/Keystore storage, token refresh, logout | | 2 | First-time profile setup | Auth | Name (req), transport, avatar; skip allowed | | 3 | Home dashboard | Home | Event list, upcoming events, quick actions | | 4 | Community hub list | Hubs | My hubs, empty state, browse | | 5 | Hub creation | Hubs | All fields, image upload, invite code displayed | | 6 | Join hub (code + link) | Hubs | Lookup, preview, pseudo, join | | 7 | Hub detail — all 7 tabs | Hubs | Correct tab visibility per role | | 8 | Hub dashboard tab | Hubs | Stats, events, trips, quick actions | | 9 | Move Room chat | Hubs | Text, image, voice, poll; real-time; edit/delete; reporting | | 10 | DM inbox + conversation | Hubs | Thread list, unread badge, all message types | | 11 | Community trip board | Hubs | List, filter, empty state | | 12 | Post trip offer | Hubs | Full form, all validations, auto-accept | | 13 | Post trip request | Hubs | Form, validations | | 14 | Express trip interest | Hubs | Modal, pickup address, auto-accept flow | | 15 | Driver accept/decline | Hubs | Pending list, accept/decline actions | | 16 | Event listing | Events | Filter, search, pagination | | 17 | Event detail (participant) | Events | Full details, register button | | 18 | Event registration (free) | Events | Role, transport, departure, submit, success | | 19 | Event registration (code) | Events | Code validation before form | | 20 | Push notification setup | Notifications | APNs + FCM, token registration | | 21 | In-app notification center | Notifications | List, unread badge, mark read, deep link | | 22 | User profile view + edit | Profile | Name, avatar, transport, city | | 23 | Impact dashboard | Impact | Points, level, CO₂, history |

25.2 Priority 2 — Organizer Tools

| # | Feature | Module | Acceptance criteria | |---|---------|--------|-------------------| | 24 | Event creation multi-step | Events | 5 steps, draft/publish, quota check | | 25 | Participant management | Events | List, filter, export CSV, edit transport | | 26 | Carpool creation | Events | Full form, distance calculation | | 27 | Carpool listing + join | Events | Available seats, join flow, confirmation modal | | 28 | Carpool passenger management | Events | Driver view, accept/remove, J-2 confirmation | | 29 | Shuttle management | Events | Create, stops, assign passengers | | 30 | QR code check-in (organizer) | Events | Scanner, success/error, manual search | | 31 | Participant QR code | Events | Display offline from participation_id | | 32 | Return transport dashboard | Events | Confirmation tracking, override | | 33 | Bulk email to participants | Events | Compose, segment, send | | 34 | Event invitations | Events | Single email + CSV bulk | | 35 | Hub admin settings | Hubs | Edit, image, regenerate code, remove members, delete |

25.3 Priority 3 — Gamification & Discovery

| # | Feature | Module | Acceptance criteria | |---|---------|--------|-------------------| | 36 | Badge grid | Impact | Locked/unlocked, tap to see unlock criteria | | 37 | Leaderboard | Impact | Global, hub, monthly; current user rank always shown | | 38 | Challenges | Impact | List, progress bars, completed/expired states | | 39 | Rewards shop | Impact | Browse, level gate, stock, full redeem flow | | 40 | Level-up celebration | Impact | Animation + reduced-motion fallback | | 41 | Shareable impact badge | Impact | Canvas render, native share, camera roll save | | 42 | Move Events grid | Discovery | Category filter, region, date, pagination | | 43 | Move Events map | Discovery | Native maps SDK, clustering, pin tap bottom sheet | | 44 | Train schedule search | Discovery | SNCF + Luxembourg, pre-filled, results list |

25.4 Priority 4 — Billing & Advanced

| # | Feature | Module | Acceptance criteria | |---|---------|--------|-------------------| | 45 | Subscription plan screen | Billing | Plan cards, pricing (or web link per Apple IAP decision) | | 46 | Stripe payment sheet | Billing | Native Stripe SDK or web redirect | | 47 | Bank transfer flow | Billing | Instructions, proof upload, pending state | | 48 | My Subscription page | Billing | Status, expiry, upgrade, Customer Portal link | | 49 | Content report flow | Moderation | Long-press, reason, confirmation | | 50 | Member blocking | Hubs | Block, unblock, blocked list in profile | | 51 | Paid event registration | Events | Stripe PaymentIntent, payment sheet, confirmation | | 52 | GDPR data export | Privacy | Request, JSON download | | 53 | GDPR account deletion | Privacy | Confirmation, magic link verify, anonymization | | 54 | Notification preferences | Notifications | Per-category toggle (push + email) | | 55 | Hub community events | Hubs | Create, edit, delete CommunityEvent; join with transport mode | | 56 | RSE report view | Events | CO₂ chart, transport breakdown, download link to web |

25.5 Future Roadmap (v2.0+)

| Feature | Rationale | |---------|-----------| | Biometric unlock (Face ID / Touch ID / Fingerprint) | Faster access, security UX improvement | | GPS-based trip matching (radius, not city) | Accurate matching for suburban and rural areas | | Apple Sign-In / Google Sign-In | Reduced auth friction; Apple Sign-In required by App Store guidelines if competitor login offered | | English and Spanish full localisation | Geographic expansion | | Co-organizer support | Multiple organizers per event | | Hub co-admin | Second admin per hub | | Typing indicators in chat | Chat UX parity | | Message reactions (emoji) | Community engagement | | Live carpool GPS tracking | Driver shares location with passengers | | Apple Watch complication | Quick check-in shortcut | | Offline event registration | Queue-based offline capability for field use |

25.6 Out of Scope for Mobile v1.0

| Feature | Why excluded | Access | |---------|-------------|--------| | Super Admin full dashboard | Web-only; admin ops too complex for mobile v1 | Web app | | PDF report generation | jsPDF web-only | Trigger on web; deep link to download | | Admin bulk operations | Mass actions unsuitable for mobile UX | Web app | | Stripe webhook processing | Server-side only; no mobile involvement | Backend function | | Business plan editor | Admin-only workflow | Web app | | Custom form builder | Complex FormBuilder component | Build on web; render in mobile | | Event template management | Admin workflow | Web app |


End of Team Move Software Requirements Specification v3.0

Document control:
Version 3.0 — CTO-reviewed, production-ready. Approved for external mobile development.
Maintained by: Team Move Product Engineering
Contact: engineering@teammoveapp.com
Last updated: June 2026
Next review: Upon completion of mobile MVP (Q4 2026)

Team Move SRS v3.0 — Confidential — Production Ready — June 2026