Patient Picker Portal
What it is
The portal at http://localhost:9000/portal is the equivalent of Epic's LaunchPad — the UI where a clinician logs in, selects a patient from the HAPI FHIR server, and launches the SMART client app. It replaces Epic when developing or running the platform without Epic.
Flow
1. Clinician opens http://localhost:9000/portal
→ redirected to /login (not authenticated)
2. Clinician enters username + password
→ Spring Security form login
→ redirected to /portal on success
3. Portal fetches patients from HAPI FHIR
→ GET http://localhost:8080/fhir/Patient?_count=20
→ renders patient list
4. Clinician selects a patient, clicks "Launch App"
→ POST /portal/launch
patientId=ePatient-8675309
encounterId=eEncounter-001 (optional)
5. Server creates a launch token (32-byte random, 5-minute expiry)
→ stored in PostgreSQL
6. Browser redirects to SMART client:
GET http://localhost:8081/launch
?iss=http://localhost:8080/fhir
&launch={token}
7. SMART client completes OAuth2 handshake
→ SmartTokenCustomizer resolves the launch token
→ patient+encounter in token response
LaunchPortalController
@GetMapping
public String portal(Model model,
@RequestParam(required = false) String search) {
List<Map<String, String>> patients = fetchPatients(search);
model.addAttribute("patients", patients);
model.addAttribute("fhirBaseUrl", serverProperties.fhirBaseUrl());
return "portal";
}
@PostMapping("/launch")
public String launch(
@RequestParam String patientId,
@RequestParam(required = false) String encounterId,
@AuthenticationPrincipal UserDetails principal) {
String launchToken = launchContextService.createLaunchToken(
patientId, encounterId,
serverProperties.defaultNeedPatientBanner(),
DEFAULT_CLIENT_ID,
principal.getUsername()
);
String launchUrl = SMART_CLIENT_LAUNCH_URL
+ "?iss=" + serverProperties.fhirBaseUrl()
+ "&launch=" + launchToken;
return "redirect:" + launchUrl;
}
Patient search
The portal supports patient search by name:
GET /portal?search=Kumar
This passes search to fetchPatients() which calls:
client.search().forResource(Patient.class)
.where(Patient.NAME.matches().value(search))
.count(20)
.returnBundle(Bundle.class)
.execute();
Configuring the SMART client URL
The SMART client launch URL is configured via smart.server.smart-client-launch-url. It defaults to http://localhost:8081/launch for local development and must be set explicitly for any other deployment:
smart:
server:
smart-client-launch-url: ${SMART_CLIENT_LAUNCH_URL:http://localhost:8081/launch}
Or via environment variable:
export SMART_CLIENT_LAUNCH_URL=https://smart.yourhospital.org/launch
The portal reads this value from SmartServerProperties.smartClientLaunchUrl() — there is no hardcoded URL in the source.
Clinician credentials
The DataInitializer seeds one test clinician on first startup:
| Username | Password | Display name |
|---|---|---|
drsmith | password | Dr. Jane Smith |
Add more clinicians by inserting rows into the clinician table:
INSERT INTO clinician (id, username, password_hash, display_name, fhir_user_id)
VALUES (
gen_random_uuid(),
'drjones',
'$2a$12$...bcrypt-hash...',
'Dr. Bob Jones',
'eProvider-DEF456'
);
Generate a BCrypt hash: spring security encode --password yourpassword or use any online BCrypt generator.