Crossroads Health runs Crossfire, a credit balance resolution platform for hospital systems. Their analysts catch overcharges, untangle duplicate payments, and trace root causes so providers stop generating credits in the first place. The work demands precision. Their clients saw none of it.
A hospital CFO who signed a $400,000 annual contract received one deliverable each month: a PDF. Thirty pages of tables. Crossfire’s operations team compiled it by hand from query results pasted into Excel. If the CFO wanted a filtered view by facility, or a comparison against the prior quarter, or a breakdown by service line, she called her account manager.
The account manager pulled the data, built the pivot table, and emailed it back. Each request consumed thirty minutes to two hours. Multiply by fourteen hospital systems and two hundred facilities. Account managers spent forty to fifty percent of their working week on status inquiries. They managed spreadsheets instead of relationships.
The problem compounded. The original Crossfire application baked charts directly into platform code. Every new visualization, every filter, every data extract required an engineering ticket. Analysts waited days for a chart refresh. Schema differences across client databases meant the same metric returned different numbers depending on which hospital you asked. Engineers burned cycles on ad-hoc reporting when they should have built product.
Three months before Crossroads confronted a competitive RFP, their largest client asked: “Do you have a portal?” The answer was no.
Why we chose self-hosted Metabase
We evaluated four paths.
Tableau Server. CRH already owned licenses. Cross-client data segregation meant separate Tableau sites, each with its own license cost. The per-user pricing model broke at CRH’s scale — hundreds of users across dozens of hospital systems. Year one cost exceeded $180,000 before infrastructure.
Power BI Embedded. Integrates with Azure AD, which CRH did not use. The A-SKU pricing charges by capacity node. Row-level security requires DAX filters authored per dataset. CRH runs PostgreSQL on Ubuntu in a colocated data center. The Microsoft ecosystem lock-in ran deep.
Custom build. D3 or ECharts wired to a REST API backed by PostgreSQL queries. Eight charts in two weeks. Forty more in three months. Filters, drill-down, export, alerting, embedding — each feature adds weeks. Six months of engineering to reach a quarter of Metabase’s chart library. The timeline did not work.
Self-hosted Metabase on existing PostgreSQL. Metabase connects natively to PostgreSQL. Its embedding API signs JWT tokens per request, passing user attributes that map directly to SQL parameters for row-level filtering. The open-source edition ran inside CRH’s own infrastructure. No patient data left the building. No per-user licensing. No cloud dependency.
We deployed a single instance, pointed it at CRH’s production read replica, and built the first dashboard in four hours. Analytics runs as a separate service, connected to a read-only database user, with zero write path access. If Metabase crashes, Crossfire keeps processing credits. The blast radius stays small.

The RBAC model
CRH serves fourteen hospital systems. Each system operates multiple facilities. Within each facility, different roles need different views. A CFO monitors aggregate credit balance trends across all facilities. A revenue cycle director drills into specific service lines. A billing analyst sees only the claims her team processes. A client success lead at CRH sees aggregate client health metrics but not individual patient records.
We built three layers of access control.
Layer one: Metabase user groups. Each provider organization maps to a Metabase group. The group has access to a curated dashboard collection. The collection contains twelve dashboards: credit balance summary, resolution velocity, root-cause trends, team productivity, aged AR, payer mix, facility comparison, service-line detail, denial patterns, appeal tracking, contract variance analysis, and patient refund pipeline. The group cannot see other organizations’ collections. The group cannot see the Metabase admin panel.
Layer two: SQL-level row filtering. Each dashboard question includes a WHERE facility_id IN ({{facility_ids}}) clause that Metabase populates from the JWT user attributes we sign at request time. We store facility and role mappings in a Python service that sits between the portal’s auth layer and Metabase’s embedding endpoint. When a user logs into the portal, the service queries CRH’s user database, resolves their organization, their facilities, and their role permissions, and returns a JWT payload like:
{
"resource": {"dashboard": 42},
"params": {
"facility_ids": [103, 107, 112],
"service_line": "cardiology",
"role": "cfo"
},
"exp": 1735689600
}The JWT lives for the session duration. Each iframe request carries a fresh token. No user credentials touch Metabase directly. Metabase trusts the token because it verifies the signing key we configured at deployment time.
Layer three: role-based dashboard visibility. Not every role needs every dashboard. A billing analyst needs the claim-level detail dashboard but not the aggregate contract variance analysis. A CFO needs the opposite. We built a permission matrix in the Python service that maps roles to dashboard IDs. When the portal renders the dashboard navigation, it only shows the dashboards the current role can access. The same filtering protects the embedding endpoint: request a dashboard outside your role and the service returns 403.
The result: a billing analyst at St. Mary’s Hospital logs in, sees five dashboards tailored to her work, and every chart scopes automatically to St. Mary’s cardiology department. She cannot navigate to another hospital’s data. She cannot escalate to the CFO view. She never knows Metabase runs underneath.
The HIPAA compliance layer
Patient financial data and credit balance records qualify as protected health information under HIPAA. Every component in the data path must satisfy the Privacy Rule, the Security Rule, and the Breach Notification Rule.
Business Associate Agreement. Cloud Intelligence signed a BAA with Crossroads Health before touching production data. Our engineers complete HIPAA training annually. We log every access to CRH’s infrastructure.
Encryption at rest and in transit. CRH’s PostgreSQL cluster encrypts data at rest using LUKS. All traffic between Metabase, the Python embedding service, and the PostgreSQL read replica traverses TLS 1.3 with enforced certificate validation. No plaintext PHI moves across any wire.
Audit logging. Every dashboard view generates an audit log entry with user ID, organization, dashboard accessed, timestamp, and IP address. The Python service writes these to a database outside the analytics path. CRH’s compliance team pulls access reports by user, organization, and date range. The audit trail satisfies the HIPAA accounting of disclosures requirement.
Data segregation by design. SQL-level row filtering means a client cannot see another client’s data even if Metabase permissions misconfigure. JWT parameters bind at query execution time. PostgreSQL executes WHERE facility_id IN (103, 107, 112) regardless of the dashboard definition. We red-teamed this: a test organization with a JWT scoped to different facilities returned zero rows for facilities outside the token scope. Row-level security policies on key tables reject queries missing a facility filter.
SOC2 alignment. The Metabase instance runs as a systemd service on an EC2 instance inside CRH’s VPC. No public internet exposure. Access routes through the VPN or the portal’s backend. Infrastructure-as-code in version control satisfies change management controls. CloudWatch metrics provide continuous monitoring.
The embedding layer
The portal renders an iframe that points to the Python embedding service. The service receives the dashboard ID and the user’s session token, validates the session, resolves permissions, constructs the JWT payload with filter parameters, signs it with the HMAC key, and returns a signed Metabase URL.
The iframe loads that URL. Metabase verifies the JWT, applies the parameterized SQL filters, and renders the dashboard with CRH’s brand colors, logo, and navigation. We configured a custom Metabase theme matching CRH’s hex codes and font stack. The transition from portal chrome to dashboard is invisible.
Three design decisions mattered.
Session-bound tokens. Every token expires at the portal session duration plus a five-minute grace period. When the session expires, the portal’s refresh flow fetches a new token. Stale cached tokens become harmless after expiration.
Headerless rendering. We stripped Metabase’s standard header bar. The dashboard fills the iframe edge to edge. Export and filter controls live in the portal’s own UI, which calls the embedding service to regenerate the iframe URL with updated parameters.
White-label depth. We customized the loading skeleton, replaced empty state illustrations, and rewrote error messages to reference “the Crossfire portal.” The instance hostname resolves to analytics.crhn.local.
The numbers, in context
The first client onboarded three weeks after we started building. Seven hospital systems went live within sixty days. Every active client adopted the portal within thirty days of availability.
Status inquiry calls from clients fell ninety percent in the first month. The call volume never recovered. Three years later, existing clients make fewer than one unscheduled inquiry per month on average. Account managers reclaimed roughly half their working hours. Several transitioned into strategic relationship roles. CRH onboarded seventy-five percent more clients with the same account management headcount.
Client satisfaction scores, measured through quarterly NPS surveys, increased forty-five percent year over year. Clients cited “transparency,” “speed,” and “self-service access” as primary drivers. Peak portal usage lands in evenings and weekends, when executives review performance outside business hours. The old PDF workflow could never serve that use case.
Contract renewals rose sixty-five percent against a baseline of roughly eighty percent annual retention. The portal became a sales asset. Prospects ask for a demo. CRH walks them through the actual dashboards their facility would see. The conversation shifts from trust-me promises to here-is-your-reality. New client onboarding time dropped from six weeks to roughly ten days, because most of the historical onboarding window consisted of back-and-forth report requests that the portal now handles automatically.
Zero clients have left since the portal launched.
What cost more than we estimated
JWT parameter edge cases. Metabase’s embedding docs cover the happy path. A JWT with an empty facility_ids array returns all rows instead of zero. A parameter key that matches no dashboard filter fails silently, rendering an unfiltered dashboard. Our red-team exercise caught both. The fix: reject tokens with empty parameter arrays and validate every parameter key maps to an actual dashboard filter before signing.
Dashboard performance at scale. Fourteen organizations loading the same dashboard during Monday morning review exhausted PostgreSQL’s connection pool. We added PgBouncer, query-level caching, and materialized views for the heaviest cards. Five-minute refresh lag proved acceptable.
Client schema divergence. Fourteen hospital systems means fourteen database schemas. The same field stores the same data under six different column names. We built a view layer that normalizes per client. It added two weeks to the project and remains the highest-maintenance component.
The portal-auth-JWT chain. Three services — portal, Python embedding service, Metabase — each with its own auth model. Debugging a blank dashboard requires tracing a session token through the portal, verifying the JWT payload, confirming the HMAC signature, and checking PostgreSQL query logs for the actual WHERE clause. We built an internal diagnostic page that renders intermediate state at each step. It saves hours per incident.
Read the original Crossroads Health case study for the higher-level narrative. For more on the compliance architecture, see HIPAA-Compliant Analytics Portal. For a step-by-step embedding tutorial, see How to Embed a Metabase Dashboard.