π Table of Contents
β¨ Features
All fields support partial matching (e.g., searching "John" finds "Johnson")
Click any search result to view a detailed profile page
OpenLayers map embed showing provider location using OpenStreetMap
Powered by Cloudflare Workers and D1
Modern, responsive design with customizable theme colors
Easy integration with Cloudflare Access
Works great on all devices
Easy to add/remove search, display, and profile fields
Mark fields as editable for future backend integration
π¨ Theme Colors
The application uses a cohesive blue color palette:
| Color | Hex | Usage |
|---|---|---|
| Primary Lightest | #afe3ff |
Backgrounds, hover states |
| Primary Light | #53abff |
Accents, badges |
| Primary | #0060ff |
Buttons, links, markers |
| Primary Dark | #0034c3 |
Headers, gradients |
| Primary Darkest | #0d0772 |
Deep backgrounds, gradients |
To customize colors, modify the CONFIG.THEME object in index.js.
π Deployment
Prerequisites
- Wrangler CLI installed
- Authenticated with your Cloudflare account:
wrangler login
Deploy the Worker
wrangler deploy
The worker will be deployed to https://npi-provider-search.<your-subdomain>.workers.dev
Optional: Custom Domain
Add a custom domain in the Cloudflare dashboard or update wrangler.toml:
routes = [
{ pattern = "npi-search.yourdomain.com", custom_domain = true }
]
πΊοΈ Map Integration
The provider profile page includes an embedded map showing the provider's location. The map uses:
- OpenLayers: For map rendering and interaction
- OpenStreetMap: As the tile source
- Nominatim: For geocoding addresses to coordinates
How Geocoding Works
The application uses Nominatim's structured query API for accurate address geocoding:
// Structured query parameters
params.append('street', addressData.street);
params.append('city', addressData.city);
params.append('state', addressData.state);
params.append('postalcode', zip); // First 5 characters only
params.append('country', 'USA');
π Setting Up Cloudflare Access SSO Protection
Cloudflare Access allows you to protect your application with SSO (Single Sign-On) using providers like Google, Microsoft, Okta, etc.
Step 1: Navigate to Cloudflare Zero Trust Dashboard
- Go to Cloudflare Dashboard
- Select your account
- Click Zero Trust in the left sidebar (or go to https://one.dash.cloudflare.com)
Step 2: Configure an Identity Provider
- Go to Settings β Authentication
- Click Add new under Login methods
- Choose your identity provider:
- Google: Easy setup with Google Workspace
- Microsoft Azure AD: For Microsoft 365 organizations
- Okta: Enterprise SSO
- GitHub: Developer-friendly option
- One-time PIN: Email-based authentication (no IdP needed)
- Follow the setup instructions for your chosen provider
Step 3: Create an Access Application
- Go to Access β Applications
- Click Add an application
- Select Self-hosted
- Configure the application:
| Field | Value |
|---|---|
| Application name | NPI Provider Search |
| Session duration | 24 hours (or your preference) |
| Application domain | npi-provider-search.<subdomain>.workers.devOr if using custom domain: npi-search.yourdomain.com |
- Click Next
Step 4: Create Access Policies
- Policy name:
Allow Organization Members - Action:
Allow - Configure rules:
For Email Domain:
Selector: Emails ending in
Value: @yourcompany.com
For Specific Users:
Selector: Emails
Value: user1@example.com, user2@example.com
For Groups (if using IdP groups):
Selector: Login Methods
Value: Your IdP
AND
Selector: Groups
Value: npi-access-group
- Click Next β Add application
Step 5: Test the Protection
- Open your worker URL in an incognito window
- You should be redirected to the Cloudflare Access login page
- Authenticate with your configured identity provider
- After successful auth, you'll be redirected to the NPI search application
Optional: Access JWT Headers in Worker
When protected by Access, your worker receives JWT tokens. You can access user info:
// In your worker, extract user email from Access JWT
async function getUserEmail(request) {
const jwt = request.headers.get('Cf-Access-Jwt-Assertion');
if (!jwt) return null;
// The JWT payload contains user info
// You can verify and decode it using the Access public keys
// https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/
// Simple extraction (for logging purposes):
const email = request.headers.get('Cf-Access-Authenticated-User-Email');
return email;
}
βοΈ Adding More Fields
The application is designed to be easily extensible. To add more fields:
1. Add to Search Fields
In index.js, find the CONFIG.SEARCH_FIELDS array and add your field:
SEARCH_FIELDS: [
'npi',
'provider_first_name',
'provider_last_name_legal_name',
// ... existing fields ...
// ADD NEW SEARCHABLE FIELDS HERE:
'provider_credential_text',
'healthcare_provider_taxonomy_code_1',
],
2. Add to Display Fields
Find CONFIG.DISPLAY_FIELDS and add fields you want shown in search results:
DISPLAY_FIELDS: [
'npi',
'entity_type_code',
// ... existing fields ...
// ADD NEW DISPLAY FIELDS HERE:
'provider_credential_text',
'provider_sex_code',
'healthcare_provider_taxonomy_code_1',
],
3. Add to Profile Sections
Find CONFIG.PROFILE_SECTIONS and add fields to the appropriate section for the profile page:
PROFILE_SECTIONS: {
profile: {
title: 'Profile Details',
fields: [
'npi',
'entity_type_code',
'provider_first_name',
// ... existing fields ...
// ADD NEW PROFILE FIELDS HERE:
'new_field_name',
]
},
address: {
title: 'Address Details',
fields: [
'provider_first_line_business_mailing_address',
// ... existing fields ...
]
},
taxonomy: {
title: 'Professional Details',
fields: [
'healthcare_provider_taxonomy_code_1',
// ... existing fields ...
]
}
},
4. Add Labels
Find CONFIG.FIELD_LABELS and add human-readable labels:
FIELD_LABELS: {
// ... existing labels ...
// ADD NEW LABELS HERE:
provider_credential_text: 'Credentials',
provider_sex_code: 'Gender',
healthcare_provider_taxonomy_code_1: 'Taxonomy Code',
provider_enumeration_date: 'Enumeration Date',
last_update_date: 'Last Updated',
},
5. Add Placeholders (Optional)
For better UX, add placeholder text for search inputs:
FIELD_PLACEHOLDERS: {
// ... existing placeholders ...
provider_credential_text: 'e.g., M.D., D.O.',
healthcare_provider_taxonomy_code_1: 'e.g., 207X00000X',
},
6. Mark Fields as Editable (Optional)
To mark fields that should be editable in the future (displayed with an edit icon):
EDITABLE_FIELDS: [
'provider_business_mailing_address_telephone_number',
'provider_business_mailing_address_fax_number',
// ADD MORE EDITABLE FIELDS HERE:
'provider_credential_text',
],
7. Redeploy
wrangler deploy
π Available Database Columns
Here are all columns available in the npidata table:
| Column | Description |
|---|---|
npi | National Provider Identifier |
entity_type_code | 1=Individual, 2=Organization |
provider_first_name | Provider's first name |
provider_last_name_legal_name | Provider's last name |
provider_middle_name | Provider's middle name |
provider_name_prefix_text | Name prefix (Dr., etc.) |
provider_name_suffix_text | Name suffix |
provider_credential_text | Credentials (M.D., etc.) |
provider_first_line_business_mailing_address | Address line 1 |
provider_second_line_business_mailing_address | Address line 2 |
provider_business_mailing_address_city_name | City |
provider_business_mailing_address_state_name | State |
provider_business_mailing_address_postal_code | ZIP code |
provider_business_mailing_address_telephone_number | Phone |
provider_business_mailing_address_fax_number | Fax |
provider_sex_code | M/F |
provider_enumeration_date | Date NPI was assigned |
last_update_date | Last record update |
healthcare_provider_taxonomy_code_1 | Primary taxonomy code |
provider_license_number_1 | License number |
provider_license_number_state_code_1 | License state |
π¨ Customization Tips
Change Entity Type Filter
Currently filters to entity_type_code='1' (individuals). To change:
In handleSearch() function, modify:
// For organizations only:
let conditions = ["entity_type_code = '2'"];
// For both:
let conditions = ["1=1"]; // No entity filter
Add Sorting Options
Modify the ORDER BY clause in handleSearch():
const dataQuery = `
SELECT ${selectFields}
FROM npidata
WHERE ${whereClause}
ORDER BY ${sortField} ${sortDirection}
LIMIT ? OFFSET ?
`;
Increase Results Per Page
RESULTS_PER_PAGE: 50, // Change from 25
Customize Map Appearance
Modify the marker style in the initMap() function:
style: new ol.style.Style({
image: new ol.style.Circle({
radius: 12, // Marker size
fill: new ol.style.Fill({ color: '#0060ff' }), // Marker color
stroke: new ol.style.Stroke({ color: '#ffffff', width: 3 }) // Border
})
})
Change Default Map Zoom
view: new ol.View({
center: ol.proj.fromLonLat([parseFloat(lon), parseFloat(lat)]),
zoom: 15 // Change zoom level (1-20)
})