πŸ₯ NPI Provider Search Application

Beautiful, fast search UI for National Provider Identifier data

✨ Features

πŸ” Fuzzy Search

All fields support partial matching (e.g., searching "John" finds "Johnson")

πŸ‘€ Provider Profiles

Click any search result to view a detailed profile page

πŸ—ΊοΈ Map Integration

OpenLayers map embed showing provider location using OpenStreetMap

⚑ Fast

Powered by Cloudflare Workers and D1

🎨 Beautiful UI

Modern, responsive design with customizable theme colors

πŸ”’ SSO Ready

Easy integration with Cloudflare Access

πŸ“± Mobile Friendly

Works great on all devices

βš™οΈ Configurable

Easy to add/remove search, display, and profile fields

✏️ Editable 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

  1. Wrangler CLI installed
  2. 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:

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');
πŸ“Œ Note: Postal codes longer than 5 characters (e.g., "12345-6789") are automatically truncated to the first 5 digits for better geocoding accuracy.

πŸ”’ 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

  1. Go to Cloudflare Dashboard
  2. Select your account
  3. Click Zero Trust in the left sidebar (or go to https://one.dash.cloudflare.com)

Step 2: Configure an Identity Provider

  1. Go to Settings β†’ Authentication
  2. Click Add new under Login methods
  3. 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)
  4. Follow the setup instructions for your chosen provider

Step 3: Create an Access Application

  1. Go to Access β†’ Applications
  2. Click Add an application
  3. Select Self-hosted
  4. Configure the application:
Field Value
Application name NPI Provider Search
Session duration 24 hours (or your preference)
Application domain npi-provider-search.<subdomain>.workers.dev
Or if using custom domain: npi-search.yourdomain.com
  1. Click Next

Step 4: Create Access Policies

  1. Policy name: Allow Organization Members
  2. Action: Allow
  3. 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
  1. Click Next β†’ Add application

Step 5: Test the Protection

  1. Open your worker URL in an incognito window
  2. You should be redirected to the Cloudflare Access login page
  3. Authenticate with your configured identity provider
  4. 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
npiNational Provider Identifier
entity_type_code1=Individual, 2=Organization
provider_first_nameProvider's first name
provider_last_name_legal_nameProvider's last name
provider_middle_nameProvider's middle name
provider_name_prefix_textName prefix (Dr., etc.)
provider_name_suffix_textName suffix
provider_credential_textCredentials (M.D., etc.)
provider_first_line_business_mailing_addressAddress line 1
provider_second_line_business_mailing_addressAddress line 2
provider_business_mailing_address_city_nameCity
provider_business_mailing_address_state_nameState
provider_business_mailing_address_postal_codeZIP code
provider_business_mailing_address_telephone_numberPhone
provider_business_mailing_address_fax_numberFax
provider_sex_codeM/F
provider_enumeration_dateDate NPI was assigned
last_update_dateLast record update
healthcare_provider_taxonomy_code_1Primary taxonomy code
provider_license_number_1License number
provider_license_number_state_code_1License 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)
})