6/8 - Public Beta (Discord)
|See Changelog
Skip to main content

User Management API

The User Management API provides comprehensive user profile management including personal information, fitness preferences, onboarding workflows, and profile image handling. This system manages both basic user data and detailed fitness-specific profile information.

⚠️ CRITICAL: THIN CLIENT IMPLEMENTATION ONLY

What Flutter SHOULD do:

  • ✅ Consume GraphQL endpoints for user data
  • ✅ Display profile information and forms
  • ✅ Handle UI state management for profile screens
  • ✅ Cache user profile data for offline display

What Flutter MUST NOT do:

  • ❌ Implement profile validation logic
  • ❌ Calculate user fitness metrics client-side
  • ❌ Duplicate onboarding business rules
  • ❌ Make decisions about profile completeness

🏗️ User Profile Architecture

Core Profile Structure

The user profile system uses a two-tier architecture:

  1. Basic User Data - Core account information (email, username, bio, etc.)
  2. Detailed Profile - Fitness-specific embedded data (goals, measurements, preferences)
Profile Architecture Overview
interface User {
// Basic account information
id: string;
email: string;
username: string;
firstName?: string;
lastName?: string;
bio?: string;
city?: string;
role: UserRole;
profileImageFileKey?: string;
profileImageUrl?: string;

// Fitness-specific profile (embedded)
profile: UserProfile;

// Onboarding status
onboardingStatus: OnboardingStatus;
}

interface UserProfile {
// Physical characteristics
age?: number;
gender?: Gender;
height?: number;
heightUnit?: HeightUnit;
weight?: number;
weightUnit?: WeightUnit;
dateOfBirth?: DateTime;

// Training preferences
trainingGoal?: TrainingGoal;
trainingLevel?: TrainingLevel;
trainingFrequency?: number;
preferredSplit?: WorkoutSplitPreference;
availableEquipment: Equipment[];

// App preferences
healthkitAuthorized?: boolean;
notificationsEnabled?: boolean;

// Onboarding tracking
onboardingCompletedAt?: DateTime;
onboardingVersion?: string;
}

🔍 GraphQL Schema

Core Types

User Object Type
type User {
id: ID!
email: String!
username: String!
firstName: String
lastName: String
bio: String
city: String
role: UserRole!
createdAt: DateTime!

# Profile image handling
profileImageFileKey: String
profileImageUrl: String

# Computed fields
fullName: String

# Embedded profile data
profile: UserProfile

# Onboarding status
onboardingStatus: OnboardingStatus!
}

🔧 GraphQL Operations

Queries

Get Current User Profile
query Me {
me {
id
email
username
firstName
lastName
bio
city
role
createdAt
fullName
profileImageFileKey
profileImageUrl
profile {
age
gender
height
heightUnit
weight
weightUnit
dateOfBirth
trainingGoal
trainingLevel
trainingFrequency
preferredSplit {
id
name
description
daysPerWeek
}
availableEquipment
healthkitAuthorized
notificationsEnabled
onboardingCompletedAt
onboardingVersion
createdAt
updatedAt
}
onboardingStatus {
isCompleted
completedAt
shouldShowOnboarding
currentStep
version
missingFields
}
}
}

Response Example:

{
"data": {
"me": {
"id": "60d5ecb54f3d4d2e8c8e4b5a",
"email": "user@example.com",
"username": "fitnessuser",
"firstName": "John",
"lastName": "Doe",
"bio": "Passionate about fitness and strength training",
"city": "New York",
"role": "USER",
"createdAt": "2023-01-01T10:00:00.000Z",
"fullName": "John Doe",
"profileImageFileKey": "profile_images/user123.jpg",
"profileImageUrl": "https://storage.example.com/profile_images/user123.jpg?expires=1640999999",
"profile": {
"age": 28,
"gender": "MALE",
"height": 180,
"heightUnit": "CM",
"weight": 75.5,
"weightUnit": "KG",
"dateOfBirth": "1995-03-15T00:00:00.000Z",
"trainingGoal": "MUSCLE_GAIN",
"trainingLevel": "INTERMEDIATE",
"trainingFrequency": 4,
"preferredSplit": {
"id": "push_pull_legs",
"name": "Push/Pull/Legs",
"description": "6-day split focusing on push/pull movements and legs",
"daysPerWeek": 6
},
"availableEquipment": ["BARBELL", "DUMBBELLS", "BENCH", "SQUAT_RACK"],
"healthkitAuthorized": true,
"notificationsEnabled": true,
"onboardingCompletedAt": "2023-01-02T14:30:00.000Z",
"onboardingVersion": "v2.1",
"createdAt": "2023-01-01T10:00:00.000Z",
"updatedAt": "2023-12-01T09:15:00.000Z"
},
"onboardingStatus": {
"isCompleted": true,
"completedAt": "2023-01-02T14:30:00.000Z",
"shouldShowOnboarding": false,
"currentStep": 5,
"version": "v2.1",
"missingFields": []
}
}
}
}

Authorization: JWT token required Use Cases:

  • App initialization and user context
  • Profile screen display
  • Navigation decisions based on onboarding status

Profile Update Mutations

Update Basic User Information
mutation UpdateMyProfile($data: UpdateMyProfileInput!) {
updateMyProfile(data: $data) {
id
firstName
lastName
bio
fullName
updatedAt
}
}

Input Type:

input UpdateMyProfileInput {
firstName: String
lastName: String
bio: String
}

Variables Example:

{
"data": {
"firstName": "John",
"lastName": "Smith",
"bio": "Fitness enthusiast and certified personal trainer. Specializing in strength training and functional movement."
}
}

Response:

{
"data": {
"updateMyProfile": {
"id": "60d5ecb54f3d4d2e8c8e4b5a",
"firstName": "John",
"lastName": "Smith",
"bio": "Fitness enthusiast and certified personal trainer. Specializing in strength training and functional movement.",
"fullName": "John Smith",
"updatedAt": "2023-12-01T10:30:00.000Z"
}
}
}

Business Rules:

  • All fields are optional (partial updates supported)
  • Bio has maximum length limit (enforced server-side)
  • fullName is automatically computed from firstName + lastName

Onboarding Mutations

Complete User Onboarding Process
mutation CompleteOnboarding($input: OnboardingDataInput!) {
completeOnboarding(input: $input) {
id
profile {
trainingGoal
trainingLevel
trainingFrequency
preferredSplit {
id
name
daysPerWeek
}
availableEquipment
age
gender
height
heightUnit
weight
weightUnit
dateOfBirth
healthkitAuthorized
notificationsEnabled
onboardingCompletedAt
onboardingVersion
}
onboardingStatus {
isCompleted
completedAt
shouldShowOnboarding
currentStep
version
missingFields
}
}
}

Input Type:

input OnboardingDataInput {
trainingGoal: TrainingGoal
trainingLevel: TrainingLevel
trainingFrequency: Int # 1-7 days per week
preferredSplit: WorkoutSplitInput
availableEquipment: [Equipment!]
dateOfBirth: DateTime
gender: Gender
weight: Float
weightUnit: WeightUnit
height: Float
heightUnit: HeightUnit
healthkitAuthorized: Boolean
notificationsEnabled: Boolean
}

input WorkoutSplitInput {
id: String!
name: String!
description: String
daysPerWeek: Int!
}

Variables Example:

{
"input": {
"trainingGoal": "MUSCLE_GAIN",
"trainingLevel": "BEGINNER",
"trainingFrequency": 3,
"preferredSplit": {
"id": "full_body",
"name": "Full Body",
"description": "Complete body workout 3x per week",
"daysPerWeek": 3
},
"availableEquipment": ["DUMBBELLS", "BODYWEIGHT_ONLY", "RESISTANCE_BANDS"],
"dateOfBirth": "1995-06-15T00:00:00.000Z",
"gender": "FEMALE",
"weight": 65,
"weightUnit": "KG",
"height": 165,
"heightUnit": "CM",
"healthkitAuthorized": true,
"notificationsEnabled": true
}
}

Response:

{
"data": {
"completeOnboarding": {
"id": "60d5ecb54f3d4d2e8c8e4b5a",
"profile": {
"trainingGoal": "MUSCLE_GAIN",
"trainingLevel": "BEGINNER",
"trainingFrequency": 3,
"preferredSplit": {
"id": "full_body",
"name": "Full Body",
"daysPerWeek": 3
},
"availableEquipment": ["DUMBBELLS", "BODYWEIGHT_ONLY", "RESISTANCE_BANDS"],
"age": 28,
"gender": "FEMALE",
"height": 165,
"heightUnit": "CM",
"weight": 65,
"weightUnit": "KG",
"dateOfBirth": "1995-06-15T00:00:00.000Z",
"healthkitAuthorized": true,
"notificationsEnabled": true,
"onboardingCompletedAt": "2023-12-01T10:45:00.000Z",
"onboardingVersion": "v2.1"
},
"onboardingStatus": {
"isCompleted": true,
"completedAt": "2023-12-01T10:45:00.000Z",
"shouldShowOnboarding": false,
"currentStep": 5,
"version": "v2.1",
"missingFields": []
}
}
}
}

Business Logic:

  • Marks onboarding as completed with timestamp
  • Calculates age from dateOfBirth
  • Validates all required fields are provided
  • Sets current onboarding version
  • Triggers program recommendation system (server-side)

📱 Flutter Implementation

Complete User Management Setup

user_models.dart
import 'package:json_annotation/json_annotation.dart';

part 'user_models.g.dart';

@JsonSerializable()
class User {
final String id;
final String email;
final String username;
final String? firstName;
final String? lastName;
final String? bio;
final String? city;
final UserRole role;
final DateTime createdAt;
final String? profileImageFileKey;
final String? profileImageUrl;
final String? fullName;
final UserProfile? profile;
final OnboardingStatus onboardingStatus;

User({
required this.id,
required this.email,
required this.username,
this.firstName,
this.lastName,
this.bio,
this.city,
required this.role,
required this.createdAt,
this.profileImageFileKey,
this.profileImageUrl,
this.fullName,
this.profile,
required this.onboardingStatus,
});

factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);

// Helper methods
bool get hasCompletedProfile =>
firstName != null && lastName != null && bio != null;

bool get needsOnboarding => !onboardingStatus.isCompleted;

String get displayName => fullName ?? username;
}

@JsonSerializable()
class UserProfile {
final int? age;
final Gender? gender;
final double? height;
final HeightUnit? heightUnit;
final double? weight;
final WeightUnit? weightUnit;
final DateTime? dateOfBirth;
final TrainingGoal? trainingGoal;
final TrainingLevel? trainingLevel;
final int? trainingFrequency;
final WorkoutSplitPreference? preferredSplit;
final List<Equipment> availableEquipment;
final bool? healthkitAuthorized;
final bool? notificationsEnabled;
final DateTime? onboardingCompletedAt;
final String? onboardingVersion;
final DateTime createdAt;
final DateTime updatedAt;

UserProfile({
this.age,
this.gender,
this.height,
this.heightUnit,
this.weight,
this.weightUnit,
this.dateOfBirth,
this.trainingGoal,
this.trainingLevel,
this.trainingFrequency,
this.preferredSplit,
required this.availableEquipment,
this.healthkitAuthorized,
this.notificationsEnabled,
this.onboardingCompletedAt,
this.onboardingVersion,
required this.createdAt,
required this.updatedAt,
});

factory UserProfile.fromJson(Map<String, dynamic> json) =>
_$UserProfileFromJson(json);
Map<String, dynamic> toJson() => _$UserProfileToJson(this);

// Helper methods
bool get hasPhysicalData => weight != null && height != null && age != null;
bool get hasTrainingPrefs => trainingGoal != null && trainingLevel != null;
String get equipmentSummary => availableEquipment.map((e) => e.name).join(', ');
}

@JsonSerializable()
class OnboardingStatus {
final bool isCompleted;
final DateTime? completedAt;
final bool shouldShowOnboarding;
final int currentStep;
final String version;
final List<String> missingFields;

OnboardingStatus({
required this.isCompleted,
this.completedAt,
required this.shouldShowOnboarding,
required this.currentStep,
required this.version,
required this.missingFields,
});

factory OnboardingStatus.fromJson(Map<String, dynamic> json) =>
_$OnboardingStatusFromJson(json);
Map<String, dynamic> toJson() => _$OnboardingStatusToJson(this);
}

@JsonSerializable()
class WorkoutSplitPreference {
final String id;
final String name;
final String? description;
final int daysPerWeek;

WorkoutSplitPreference({
required this.id,
required this.name,
this.description,
required this.daysPerWeek,
});

factory WorkoutSplitPreference.fromJson(Map<String, dynamic> json) =>
_$WorkoutSplitPreferenceFromJson(json);
Map<String, dynamic> toJson() => _$WorkoutSplitPreferenceToJson(this);
}

// Enums
enum UserRole { USER, ADMIN }
enum Gender { MALE, FEMALE, NON_BINARY, PREFER_NOT_TO_SAY }
enum HeightUnit { CM, INCHES }
enum WeightUnit { KG, LBS }
enum TrainingGoal { MUSCLE_GAIN, STRENGTH_GAIN, WEIGHT_LOSS, ENDURANCE, GENERAL_FITNESS }
enum TrainingLevel { BEGINNER, INTERMEDIATE, ADVANCED }
enum Equipment {
BARBELL, DUMBBELLS, KETTLEBELLS, RESISTANCE_BANDS, PULL_UP_BAR,
BENCH, SQUAT_RACK, CABLE_MACHINE, BODYWEIGHT_ONLY
}

🛡️ Business Logic & Validation

Profile Completeness Rules

The system uses server-side business logic to determine profile completeness and onboarding status:

Profile Completeness Logic
interface ProfileCompletionRules {
// Required for basic profile completion
basicRequired: ['firstName', 'lastName'];

// Required for fitness profile completion
fitnessRequired: [
'trainingGoal',
'trainingLevel',
'trainingFrequency',
'age',
'gender'
];

// Optional but recommended
recommended: [
'bio',
'weight',
'height',
'availableEquipment',
'preferredSplit'
];
}

Onboarding Flow Business Rules

  1. Step Validation: Each onboarding step has specific validation rules
  2. Progressive Disclosure: Advanced options shown based on previous selections
  3. Equipment-Based Recommendations: Program suggestions based on available equipment
  4. Goal-Based Customization: Training parameters adjusted based on selected goals

Data Privacy & Security

  • Profile Images: Signed URLs with expiration for secure access
  • Personal Data: Height, weight, age stored with user consent
  • Data Retention: User can delete profile data at any time
  • Privacy Controls: Users control visibility of profile information

📊 Error Handling

Common Error Scenarios

Validation Error Handling
class ProfileValidationException implements Exception {
final Map<String, String> fieldErrors;

ProfileValidationException(this.fieldErrors);

String? getFieldError(String field) => fieldErrors[field];

bool hasFieldError(String field) => fieldErrors.containsKey(field);
}

// Usage in forms
try {
await userService.updateProfile(data);
} on ProfileValidationException catch (e) {
setState(() {
_firstNameError = e.getFieldError('firstName');
_bioError = e.getFieldError('bio');
});
}

Common Validation Errors:

  • INVALID_AGE - Age must be between 13-120
  • INVALID_WEIGHT - Weight must be positive number
  • INVALID_HEIGHT - Height must be positive number
  • BIO_TOO_LONG - Bio exceeds 500 characters
  • INVALID_TRAINING_FREQUENCY - Must be 1-7 days per week

🔄 State Management

UserProvider Example

user_provider.dart
import 'package:flutter/material.dart';

class UserProvider extends ChangeNotifier {
User? _currentUser;
bool _isLoading = false;
String? _error;

User? get currentUser => _currentUser;
bool get isLoading => _isLoading;
String? get error => _error;
bool get isAuthenticated => _currentUser != null;
bool get needsOnboarding => _currentUser?.needsOnboarding ?? false;

Future<void> loadCurrentUser() async {
_setLoading(true);

try {
_currentUser = await _userService.getCurrentUser();
_error = null;
} catch (e) {
_error = e.toString();
_currentUser = null;
} finally {
_setLoading(false);
}
}

Future<void> updateBasicProfile(UpdateBasicProfileData data) async {
try {
final updatedUser = await _userService.updateBasicProfile(data);
_currentUser = updatedUser;
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
rethrow;
}
}

Future<void> completeOnboarding(OnboardingData data) async {
_setLoading(true);

try {
_currentUser = await _userService.completeOnboarding(data);
_error = null;
} catch (e) {
_error = e.toString();
rethrow;
} finally {
_setLoading(false);
}
}

void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}

void clearError() {
_error = null;
notifyListeners();
}
}

🚀 Testing

Unit Tests

user_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

void main() {
group('UserService', () {
late MockGraphQLClient mockClient;
late UserService userService;

setUp(() {
mockClient = MockGraphQLClient();
userService = UserService(mockClient);
});

test('should return current user on successful query', () async {
// Arrange
final userData = {
'me': {
'id': 'user123',
'email': 'test@example.com',
'username': 'testuser',
'firstName': 'Test',
'lastName': 'User',
'role': 'USER',
'createdAt': '2023-01-01T00:00:00Z',
'profile': null,
'onboardingStatus': {
'isCompleted': false,
'shouldShowOnboarding': true,
'currentStep': 0,
'version': 'v2.1',
'missingFields': ['trainingGoal'],
}
}
};

when(mockClient.query(any)).thenAnswer((_) async => QueryResult(
data: userData,
source: QueryResultSource.network,
options: QueryOptions(document: gql('')),
));

// Act
final user = await userService.getCurrentUser();

// Assert
expect(user.id, equals('user123'));
expect(user.needsOnboarding, isTrue);
expect(user.onboardingStatus.currentStep, equals(0));
});

test('should update basic profile successfully', () async {
// Test implementation
});

test('should complete onboarding with valid data', () async {
// Test implementation
});
});
}

📚 Migration Notes

  • All user IDs are MongoDB ObjectId strings
  • Profile data uses embedded MongoDB documents
  • Profile images use signed URLs with expiration
  • Onboarding system is versioned for future updates
  • Equipment preferences stored as arrays for multi-selection
  • Age calculated from dateOfBirth server-side
  • All enum values validated server-side

🆘 Support

For user management questions:

  1. Check onboarding status logic for navigation decisions
  2. Verify profile image URL expiration and refresh logic
  3. Ensure proper enum value handling across client/server
  4. Contact backend team for profile completeness rule changes

Ready to manage user profiles? Check out the Program Plans API for fitness program integration.