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
- Introduction & Product Vision
- Functional Architecture
- User Roles & Permissions
- Authentication & Onboarding
- Community Hubs Module
- Events Module
- Mobility Coordination Module
- Journey Mutualization (Community Trip Board)
- Environmental Impact Module
- Discovery Module
- Notifications System
- Subscription & Billing
- Administration & Super Admin
- Rewards & Gamification
- API Specifications
- Database & Data Model
- iOS Requirements
- Android Requirements
- Security
- Performance & Scalability
- Accessibility
- Non-Functional Requirements
- GDPR & Privacy
- Error Handling Reference
- 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:
- Establish subscription on screen focus.
- Unsubscribe on screen blur or app background.
- Re-subscribe on app foreground resume.
- Handle subscription errors with exponential backoff (1s, 2s, 4s, 8s, max 30s).
- 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
- User opens app → check Keychain/Keystore for session token.
- If no valid token: navigate to Login screen.
- User enters email (RFC 5321 format, max 254 chars).
- App calls:
POST /auth/magic-link { email } - Platform sends OTP link valid for 15 minutes, single-use.
- User taps link in email → deep link opens app.
- App extracts token:
/auth/verify?token=<token> - App calls:
POST /auth/verify { token } - Response:
{ session_token, user: { id, email, full_name, role } } - Store session_token in Keychain (iOS) / Keystore (Android).
- Call
GET /auth/meto fetch full user profile. - If full_name is null/empty: navigate to Profile Setup (4.4).
- 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:
kSecClassGenericPasswordKeychain withkSecAttrAccessibleAfterFirstUnlock - 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.
- "Welcome to Team Move" screen.
- Required: Display name (full_name). Validation: 2–100 chars, at least one printable character.
- Optional: Preferred transport mode.
- Optional: City of residence (max 100 chars).
- Optional: Avatar — AI-generated (calls generateAvatar; 5–10s wait) or emoji set.
- PATCH /auth/me with { full_name, transport_mode, city, avatar_url }.
- Avatar generation failure: silently fall back to default avatar. Do not block onboarding.
- 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>
- App calls:
POST /functions/validateEventInvitation { token } - If expired: "This invitation has expired. Contact the organizer." END.
- If already accepted: "You already accepted this invitation." with link to event. END.
- Display: event name, date, location, organizer name.
- User enters name (required) + transport mode.
- Tap "Accept":
POST /functions/acceptEventInvitation { token, name, transport_mode } - Backend creates/links Participation record, marks invitation "accepted."
- Prompt: "Create a Team Move account to access your event details anytime."
- 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
- User taps "Create a Hub."
- Enters name (required), description (optional), cover image (optional, jpg/png/webp, max 10 MB).
- Taps "Create Hub."
POST /entities/Community { name, description, owner_email, owner_name }- Server generates invite_code (globally unique), creates CommunityMember for creator (role="admin"), sets members_count = 1.
- Navigate to Hub Detail with created hub.
- 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):
- Auto-uppercase input, 8-char alphanumeric filter.
POST /functions/getCommunityByCode { code }- Display hub preview: name, cover image, member count.
- Enter pseudo (2–30 chars, alphanumeric + space/hyphen/underscore, no pure whitespace).
POST /entities/CommunityMember { community_id, user_email, pseudo, role: "member" }- Server increments members_count, dispatches community_new_member notification.
- ImpactPoint: community_join (+5 pts).
- 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:
- Fetch Community by ID.
- Fetch current user's CommunityMember record for this hub.
- If no record: show non-member state.
- 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:
- Hub cover image + name + member count.
- RSE stats: total CO₂ saved by hub members (sum ImpactPoint.co2_saved_kg for community_id), total trips coordinated.
- Quick actions: [Post a trip] [Create an event] [Invite a member].
- Upcoming events: next 3 CommunityEvent records, future dates.
- 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
- Member taps "Leave this hub" (in hub settings or profile).
- Confirmation: "Leave [Hub name]? You will lose access to all hub content."
- DELETE /entities/CommunityMember/<id>. Server decrements members_count.
- 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
- Navigate to event (from listing, invite link, or direct URL).
- If access_type = "private_code": validate code before form (client-side; server re-validates on submit).
- If access_type = "paid": initiate Stripe payment before form (section 6.5).
- Registration form: role, departure city, departure address (Places autocomplete), transport mode outbound, transport mode return.
- Submit:
POST /entities/Participation { event_id, user_email, user_name, role, transport_mode, transport_mode_return, departure_city, departure_address, status: "à_compléter" } - If transport involves distance (carpool_driver, car_solo): call calculateRouteDistance → store distance_km, carbon_saved_kg.
- Call notifyOrganizerOnRegistration.
- ImpactPoint: event_participation (+5). If bike/walk/train/public_transport/shuttle: low_carbon_transport (+15).
- 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.
- Call:
POST /functions/createEventPaymentIntent { event_id, user_email } - Response:
{ client_secret, amount, currency } - Present Stripe PaymentSheet (native SDK: STPPaymentSheet iOS / PaymentSheet Android).
- On PaymentSheetResult.completed: create Participation record.
- On PaymentSheetResult.failed: display Stripe error message.
- On PaymentSheetResult.canceled: return to event detail.
- 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:
- Open Check-in tab. Tap "Scan QR code."
- Decode QR: extract participation_id.
- PATCH
/entities/Participation/<participation_id>{ checked_in: true, checked_in_at: now }. - Show: participant name, role, transport mode.
- Haptic success + optional audio beep (respects silent mode on iOS).
- 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
- Tap "Join this carpool."
- Confirm modal: driver name, departure address, time, cost per person.
- PATCH /entities/Carpool/<id>: add to passengers[], decrement available_seats.
- PATCH /entities/Participation/<id>: set transport_mode = "carpool_passenger", carpool_id.
- 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":
- Direction selector (if aller_retour), pickup address (required for car modes), notes.
- PATCH /entities/CommunityTrip/<id>: append to interested[].
- If auto_accept: status = "accepted," decrement seats_available.
- If not auto_accept: status = "pending," notify author.
- ImpactPoint: community_trip_interest (+5 pts).
For requests — "I can drive on this route":
- Opens TripOffer creation form pre-filled with destination, date.
- Context banner: "Responding to [pseudo]'s request for [route]."
- 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:
- Browse shop (active = true items only). Locked items shown if min_level not met.
- Tap reward → detail sheet.
- If eligible: "Redeem for X points." Confirmation: "You have Y points. Remaining: Y - X."
- Server: verify points >= cost_points AND (stock > 0 OR stock = null). Atomic: create RewardRedemption + ImpactPoint(-(cost_points)).
- If stock: decrement Reward.stock by 1.
- 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):
- Long-press message → "Report."
- Reason: Harassment / Spam / Inappropriate / Hate speech / Violence / Other.
- Optional details (max 500 chars).
- POST /entities/ContentReport { reporter_email, reported_user_email, reported_user_pseudo, message_id, message_content (snapshot), community_id, context, reason, details, status: "pending" }.
- 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:
- Dimmed overlay.
- Level badge zooms in from center.
- Confetti burst.
- Text: "Level up! You reached [Level Name]."
- Animated points counter.
- "X more points to [next level]."
- [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
- User taps "Share my impact" on Impact Dashboard.
- 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."
- Save to camera roll (with write permission) + open native share sheet.
- 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:
- Full connectivity: all features available.
- Poor connectivity (> 1000ms): skeleton screens, no timeout < 15s.
- Intermittent: optimistic UI for writes, queue failed operations.
- No connectivity: read-only from cache, writes queued, persistent "Offline mode" banner.
- 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:
- 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
- Profile → Privacy → "Delete my account."
- Warning: "This is irreversible. All your data will be deleted within 30 days."
- Enter email to confirm.
- Verify via magic link (prevents accidental deletion).
- 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.
- 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:
- Platform engineering notified immediately via criticalAlertNotifier.
- Incident assessment within 4 hours.
- If personal data compromised: CNIL notification within 72 hours.
- If high risk to user rights: affected users notified without undue delay.
- 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)