Standalone Launch
Standalone launch is used when your app launches independently (not embedded in an EHR) and needs to authenticate a user to access their health data.
When to Use Standalone Launch
Use standalone launch for:
- Patient-facing mobile apps - Apps patients download and use independently
- Personal health record apps - Apps that manage personal health information
- Consumer health apps - Fitness, wellness, or health tracking applications
- Research apps - Apps for clinical studies or data collection
How It Works
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Your App │ │ FHIR Server │ │ User │
└──────┬──────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
│ 1. Get server capabilities │ │
├─────────────────────────────────>│ │
│ │ │
│ 2. Redirect to auth URL │ │
├──────────────────────────────────┼──────────────────────────────────>│
│ │ │
│ │ 3. User logs in & authorizes │
│ │<──────────────────────────────────┤
│ │ │
│ 4. Redirect back with auth code │ │
│<─────────────────────────────────┼───────────────────────────────────┤
│ │ │
│ 5. Exchange code for tokens │ │
├─────────────────────────────────>│ │
│ │ │
│ 6. Access token & refresh token │ │
│<─────────────────────────────────┤ │
│ │ │
│ 7. Make FHIR requests │ │
├─────────────────────────────────>│ │
│ │ │
Basic Implementation
Step 1: Create the Client
import 'package:fhir_r4_auth/fhir_r4_auth.dart';
final client = SmartFhirClient(
config: SmartConfig(
// Your registered client ID
clientId: 'your-app-client-id',
// FHIR server base URL
fhirBaseUrl: 'https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4'.toFhirUri,
// Your app's redirect URI (must be registered)
redirectUri: Uri.parse('com.yourcompany.yourapp://callback'),
// Scopes you're requesting
scopes: [
'patient/*.read', // Read all patient resources
'launch/patient', // Patient context
'offline_access', // Refresh token
],
// Launch type (standalone is default)
launchType: LaunchType.standalone,
// Enable PKCE (recommended, enabled by default)
enablePkce: true,
// Request OpenID Connect (recommended)
enableOpenId: true,
),
);
Step 2: Authenticate
try {
await client.authenticate();
print('Successfully authenticated!');
// Client is now ready to make FHIR requests
} on AuthenticationException catch (e) {
print('Authentication failed: ${e.message}');
// Handle error
} on NetworkException catch (e) {
print('Network error: ${e.message}');
// Handle network issues
}
Step 3: Make FHIR Requests
Once authenticated, use the client to make FHIR requests:
// Read a resource
final patient = await client.read(
resourceType: 'Patient',
id: 'patient-id',
);
// Search for resources
final observations = await client.search(
resourceType: 'Observation',
parameters: {
'patient': 'patient-id',
'category': 'vital-signs',
'_count': '10',
},
);
// Create a resource
final newObservation = Observation(/* ... */);
final created = await client.create(
resource: newObservation,
);
// Update a resource
patient.name?.first?.text = FhirString('Updated Name');
final updated = await client.update(
resource: patient,
);
Complete Example
Here's a complete Flutter app with standalone launch:
import 'package:flutter/material.dart';
import 'package:fhir_r4_auth/fhir_r4_auth.dart';
import 'package:fhir_r4/fhir_r4.dart';
void main() {
runApp(MyHealthApp());
}
class MyHealthApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Health App',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
SmartFhirClient? _client;
Patient? _patient;
bool _isLoading = false;
String? _error;
void initState() {
super.initState();
_initializeClient();
}
void _initializeClient() {
_client = SmartFhirClient(
config: SmartConfig(
clientId: 'your-client-id',
fhirBaseUrl: 'https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4'.toFhirUri,
redirectUri: Uri.parse('com.yourcompany.yourapp://callback'),
scopes: ['patient/*.read', 'launch/patient', 'offline_access'],
),
);
}
Future<void> _login() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
await _client!.authenticate();
// Get patient ID from token response
final patientId = _client!.patientId;
if (patientId != null) {
// Fetch patient data
final patient = await _client!.read(
resourceType: 'Patient',
id: patientId,
) as Patient;
setState(() {
_patient = patient;
_isLoading = false;
});
}
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _logout() async {
await _client?.logout();
setState(() {
_patient = null;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Health App'),
actions: _patient != null
? [
IconButton(
icon: Icon(Icons.logout),
onPressed: _logout,
),
]
: null,
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('Error: $_error'),
SizedBox(height: 16),
ElevatedButton(
onPressed: _login,
child: Text('Try Again'),
),
],
),
);
}
if (_patient == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.medical_services, size: 64),
SizedBox(height: 16),
Text('Welcome to My Health App'),
SizedBox(height: 8),
Text('Connect to your health records'),
SizedBox(height: 24),
ElevatedButton(
onPressed: _login,
child: Text('Connect Health Records'),
),
],
),
);
}
return ListView(
padding: EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: Icon(Icons.person),
title: Text(_patient!.name?.first?.text?.value ?? 'Unknown'),
subtitle: Text('Patient ID: ${_patient!.id}'),
),
),
// Add more patient information here
],
);
}
void dispose() {
_client?.close();
super.dispose();
}
}
Configuration Options
Scopes
Scopes control what data your app can access:
scopes: [
// Patient resources
'patient/Patient.read', // Read patient demographics
'patient/Observation.read', // Read observations
'patient/Condition.read', // Read conditions
'patient/MedicationRequest.read', // Read medications
'patient/*.read', // Read all patient resources
// Launch context
'launch/patient', // Get patient context
// Special scopes
'offline_access', // Request refresh token
'openid', // OpenID Connect
'fhirUser', // Get user identity
],
Custom Parameters
Add custom parameters to the authorization request:
config: SmartConfig(
// ... other config
customParameters: {
'aud': 'https://fhir.epic.com',
'custom_param': 'value',
},
),
Session Management
Add session management to handle timeouts:
final client = SmartFhirClient(
config: config,
sessionManager: SessionManager(
idleTimeout: Duration(minutes: 15),
absoluteTimeout: Duration(hours: 8),
),
);
// Listen for timeout warnings
client.onSessionTimeoutWarning?.listen((remaining) {
print('Session expires in ${remaining.inMinutes} minutes');
// Show warning to user
});
// Listen for session expiry
client.onSessionTimeout?.listen((reason) {
print('Session ended: $reason');
// Redirect to login
});
// Record activity to prevent idle timeout
await client.recordActivity();
Error Handling
Handle common error scenarios:
try {
await client.authenticate();
} on AuthenticationException catch (e) {
// User cancelled or auth failed
if (e.message.contains('cancelled')) {
print('User cancelled authentication');
} else {
print('Authentication error: ${e.message}');
}
} on ConfigurationException catch (e) {
// Configuration problem
print('Configuration error: ${e.message}');
print('Check field: ${e.configurationField}');
} on NetworkException catch (e) {
// Network problem
print('Network error: ${e.statusCode}: ${e.message}');
} catch (e) {
// Other errors
print('Unexpected error: $e');
}
Testing
Test standalone launch in your app:
- Use a sandbox - Start with SMART Health IT Sandbox
- Register your app - Get a client ID and register your redirect URI
- Test authentication flow - Verify the full OAuth flow works
- Test token refresh - Ensure tokens refresh automatically
- Test logout - Verify tokens are revoked on logout
Next Steps
- Installation guide - Platform-specific setup
- Auth overview - Full API reference and feature overview