Project Overview
PulsePass is the shared sign-in and account system that sits behind every product in the Pulse ecosystem. One account, many apps. A single user has one credential, optional passkeys, optional TOTP, an avatar, and a set of organizations they belong to — and every Pulse application asks PulsePass who that person is rather than storing identity data of its own.
PulsePass is not one application. It is three cooperating Blazor apps that share a design system, a database, and a domain model:
PulsePass.Id— the identity provider. The only app that owns the password check, the OIDC endpoints, and the consent prompt. Deliberately small and boring.PulsePass.My— the admin and self-service surface. Profile, security, passkeys, connected apps, organizations, invitations, brand colors, websites, and the admin tooling that runs the platform.PulsePass.Web— the public marketing site atpulsepass.com.
All three sit on top of a shared SQL model (PulsePass.Data), shared domain contracts (PulsePass.Contracts), shared service implementations (PulsePass.Infrastructure), and a single design system (PulsePass.Design) that ships its compiled CSS, SVG sprite, and self-hosted Geist webfont as static web assets.
The Challenge
PulsePass had to be the foundation of a multi-product company, not a side project. The requirements pulled hard in every direction:
- Real SSO across the ecosystem. Sign-in for ThePulseNet, ThePulseCom, PulsePass.My, and any future Pulse product had to round-trip through one identity provider, with each relying party holding only its own session cookie afterwards.
- Organizations as a first-class entity. Most Pulse products are used by teams, not individuals. PulsePass had to model organizations, memberships, owners, named roles, invitations, addresses, branded websites, and a default workspace per user — and expose all of it to relying parties through OIDC claims rather than a side-channel API.
- Modern auth, end-to-end. Passwords, recovery codes, TOTP, WebAuthn passkeys, persistent consent, activity logging, avatar pipelines — none of which any consuming app should have to re-implement.
- Brand consistency everywhere. Each organization picks brand colors once, in both light and dark, and every Pulse product that shows that organization's name renders it the same way.
- Three independent environments. Development, Staging, and Production each run their own SQL database, their own Key Vault, their own App Service, and their own OIDC client registrations — without any chance of a Staging deployment silently linking into Production.
- Solo, with full reversals allowed. Built by a single engineer over twelve months, with the freedom — and the obligation — to throw away earlier choices that turned out wrong.
Two architectural pivots that defined the project
Twice during the build, the right answer was to throw away a substantial amount of working code. Each time, the alternative was to ship a product that quietly couldn't grow.
Pivot 1 — Duende IdentityServer to OpenIddict
The first version of PulsePass.Id ran on Duende IdentityServer. It worked. It also came with a per-environment commercial license cost that scaled with the company, and an integration model that pushed identity logic out into IdentityServer-shaped extensibility points rather than into the EF Core domain model.
Switching to OpenIddict gave us:
- An MIT-licensed authorization server with no production-usage restrictions (ADR 0001).
- Native EF Core stores for applications, authorizations, scopes, and tokens — wired directly into the same
ApplicationDbContextthat already owned users, organizations, memberships, and websites. - A clear distinction between system clients (the
pulsepass_myadmin portal, code-seeded bySeedDataWorkeron every PulsePass.Id boot) and admin-registered clients (every other relying party, registered through the/admin/identity/client-sitesUI in PulsePass.My, hashed intoOpenIddictApplicationsby the save handler). - A small, predictable consent model — the user approves a set of scopes once, an
Authorizationrow is persisted, and returning sign-ins skip the consent screen until the scope set changes.
The migration touched the authorize endpoint, the token endpoint, the consent prompt, the avatar streaming endpoint, the seed worker, and every relying party's OIDC scheme. It shipped without breaking sign-in for any product.
Pivot 2 — ASP.NET Core (mixed) to Blazor everywhere
The second pivot was bigger. PulsePass had grown a mix of ASP.NET Core MVC controllers, Razor Pages, and an early Blazor admin area, each with its own conventions and its own set of styles. Adding a feature meant deciding which surface to add it to and then hand-translating tokens, components, and layout primitives across three idioms.
Going Blazor end-to-end meant:
- Every PulsePass surface —
PulsePass.Id,PulsePass.My,PulsePass.Web— became a Blazor Server app with the same component vocabulary. - Every relying party in the ecosystem —
ThePulseNet.UI,ThePulseCom.WebClient— was already (or moved to) Blazor, so the OIDC integration could be shared as a single recipe rather than three slightly different ones. - Scoped Razor CSS was forbidden by an MSBuild target (
ForbidScopedRazorCss) in the projects that enforce it, so styles could not silently fragment back into per-component stylesheets. Every visual decision lives in the SCSS token tree. - One design system —
PulsePass.Design— became the source of truth for tokens, components, the SVG sprite, and the Geist webfont.
The pivot took weeks of focused work and replaced thousands of lines of MVC and Razor Pages code. The payoff is that every page in the PulsePass family now reads the same way, and a fix in the design system propagates to every app at once.
Architecture
PulsePass is structured as three layers, with a clear contract between each:
| Layer | What it owns | Where it lives |
|---|---|---|
| 1. Identity Provider | Password check, TOTP, passkeys, OIDC endpoints, consent, token issuance, avatar streaming | PulsePass.Id |
| 2. Organization data | Organizations, memberships, roles, invitations, websites, brand colors, client-site registrations | Entities in PulsePass.Contracts; persisted by PulsePass.Data; admin UI in PulsePass.My |
| 3. Consumer applications | Their own domain data — never identity, never membership | PulsePass.My, ThePulseNet.UI, ThePulseCom.WebAPI + ThePulseCom.WebClient, future Pulse products |
The sign-in flow. A user clicks "Sign in" on any Pulse app. The relying party redirects them to PulsePass.Id /connect/authorize. PulsePass.Id authenticates them — password, passkey, or TOTP if enabled — and shows the consent screen if the requested scopes haven't been approved yet. It redirects back with an authorization code. The relying party exchanges the code for tokens at /connect/token over a secure back channel and stores its own session cookie. From that point on, ongoing requests never round-trip through PulsePass — the relying party can scale, restart, and even fail without involving the identity provider.
Where it runs. Each PulsePass app is a Linux Azure App Service (ADR 0003) backed by Azure SQL via EF Core (ADR 0002). Three environments — Development, Staging, Production — each run a complete copy with its own database, its own Key Vault, its own client-secret material, and its own warning-colored staging indicator pinned to the top of every non-production page.
Identity features
The user-facing security surface is built on top of ASP.NET Core Identity and extended where the modern auth story demanded it:
- Passwords with hashing, lockout, and recovery codes — the boring baseline, done correctly.
- TOTP two-factor authentication — authenticator-app codes with recovery code generation and a self-service enable / disable flow.
- Passkeys (WebAuthn) — phishing-resistant credentials registered against a device or hardware key, with the infrastructure already wired for fully passwordless sign-in.
- Connected apps with persistent consent — each authorized scope set is stored as a PulsePass
Authorizationrecord, so a returning user does not re-approve every sign-in, and a revoke fromPulsePass.Myimmediately invalidates future token exchanges. - Activity logging — sign-ins, security changes, and other user-meaningful events are captured in a structured audit trail surfaced both to the user (in their account page) and to admins.
- Avatars — uploaded once to
PulsePass.Id, processed into 128 px and 512 px WebP renditions stored in private Azure Blob containers, streamed back through anonymous HTTPS endpoints, and surfaced to relying parties via thepictureclaim and thepulsepass_org_profileclaim per membership.
Organizations as a first-class entity
Most Pulse products are used by teams. PulsePass models that explicitly:
Organization— name, slug, addresses, billing preferences, brand colors.OrganizationMembership— the join between user and organization, with the owner flag.OrganizationRole+OrganizationRoleAssignment— named roles per organization, assigned per member, so other Pulse apps can honor "Owner", "Admin", and "Billing" without re-defining them.OrganizationInvitation— pending invites for users who have not yet joined.OrganizationWebsite— websites registered under an organization, with optional verification, manifest, brand overrides, and anAllowAuditProfileSyncopt-in that lets ThePulseNet's Pulse Check write AI-extracted brand data back into the profile after each successful audit.- Default workspace. Every account is auto-provisioned a single organization at registration that the user always retains. A unique filtered index on
Organization.OwnerUserIdguarantees exactly one per account; application logic blocks deleting it. Every PulsePass account therefore always has somewhere to keep its work.
Brand colors that inherit, not duplicate
An organization picks two brand colors — a primary accent and a background — and can optionally pick dark-mode versions of each. Any of its websites can override any of those four values independently. Every Pulse application that renders the organization's chrome resolves each slot through a deterministic null-coalescing chain:
effective_theme_color = site.ThemeColor ?? org.ThemeColor ?? token_default
effective_background_color = site.BackgroundColor ?? org.BackgroundColor ?? token_default
effective_theme_color_dark = site.ThemeColorDark ?? site.ThemeColor
?? org.ThemeColorDark ?? org.ThemeColor
?? token_default_dark
effective_background_color_dark = site.BackgroundColorDark
?? site.BackgroundColor
?? org.BackgroundColorDark
?? org.BackgroundColor
?? token_default_dark
The four color fields are nvarchar(16) NULL; null means "inherit from the next level down". On the wire, they are emitted on the pulpasspass_org_profile and pulsepass_org_websites OIDC claims with JsonIgnoreCondition.WhenWritingNull, so absent keys mean inherit and present keys mean override. The same BrandColorEditor.razor Blazor component is reused by both the organization preferences page and the per-website edit dialog, so a brand author sees identical controls everywhere they edit color.
Shared design system — PulsePass.Design
Every PulsePass surface is styled out of one Razor Class Library:
- Token layer in
Styles/pulsepass/_tokens.scss(admin and auth) andStyles/pulsepassweb/_tokens-web.scss(marketing). Every color, spacing value, and typography choice is a--pp-*or--pw-*custom property; pages may not hard-code hex or px values. - Compiled CSS at
wwwroot/css/pulsepass.cssandwwwroot/css/pulsepassweb.css, served as static web assets via_content/PulsePass.Design/. - SVG sprite at
_content/PulsePass.Design/assets/sprite.svg, one symbol per line so adding an icon is a one-line copy from the catalogue file. - Self-hosted Geist webfont so PulsePass renders identically with no third-party font hop.
The result is that PulsePass.Id, PulsePass.My, and PulsePass.Web all reach for auth-input, auth-button, auth-button--primary, and the --pp-* token tree rather than re-styling the same controls three different ways. Bypassing the system is intentionally hard — the engineering doc explicitly forbids raw Bootstrap form classes inside any of the three apps.
Cross-environment correctness
Three environments, three databases, three Key Vaults, three sets of OIDC client registrations — and a hard rule that no link from any Pulse app may ever silently resolve to a different environment than the one rendering it.
IPulseEnvironmentUrlsinPulsePass.Infrastructureresolves environment-correct base URLs for every Pulse-ecosystem app (MyPulsePassUrl,IdPulsePassUrl,PulsePassWebUrl,ThePulseNetUrl,ThePulseComUrl). Pages and services inject it instead of reading a hard-coded host.- Staging indicator.
Pulse.Blazor/StagingIndicator.razorrenders a thin warning-colored bar at the top of every non-production page so testers always know which environment they are looking at. - Migrations are admin-applied in Staging and Production. Each owning app exposes a protected
GET /api/admin/db/migrations+POST /api/admin/db/migratepair. Dev auto-applies on F5; Staging and Prod do not. EveryIHostedServicepre-flights pending migrations and wraps its body in a guard so a missing column becomes a logged warning, never a fatalSqlExceptionduring host start.
Secrets discipline — Key Vault as the source of truth
Every PulsePass-adjacent web app — PulsePass.Id, PulsePass.My, PulsePass.Web, and ThePulseNet.UI — binds to its environment's Azure Key Vault as a configuration source via a single builder.Configuration.AddPulsePassKeyVault("<AppPrefix>", builder.Environment) call. The extension translates -- in secret names to : in config keys, throws in non-Development if KeyVault:Name is missing (so Staging and Production fail fast at startup instead of booting on stale defaults), and warns in Development to keep offline F5 working.
Secrets are namespaced into two tiers:
- Shared (every app) —
Section--SubKey, e.g.ConnectionStrings--DefaultConnection. - App-scoped (one app only) —
<AppPrefix>--Section--SubKey, e.g.PulsePassMy--Oidc--ClientSecret. Known prefixes are filtered out of other apps so PulsePass.My never accidentally reads PulsePass.Id-only secrets.
The build also bakes in a hard-won distinction: system clients (the pulsepass_my admin portal) are reseeded by code on every PulsePass.Id boot and need their plaintext stored in two vaults so the hash on the Id side and the secret on the My side stay byte-for-byte identical. Admin-registered clients (everyone else) are typed into the /admin/identity/client-sites form, hashed into OpenIddictApplications at save time, and read from the consuming app's vault under its own prefix. Confusing the two paths produces an OpenIddict invalid_client error at the token endpoint after a DB reseed; documenting which path each client takes was non-negotiable.
What's next — thePulse.dev
PulsePass's next consumer is thePulse.dev, an in-development developer portal. It will plug straight into the same identity, organization, and brand-color claims as every other Pulse product — no new sign-in code, no new tenant model, no new design system. The portal itself is still being built and not yet public, but the fact that adding it is a sign-in scheme and a few claim consumers, not a new identity stack, is the point of everything described above.
Key Outcomes
| One sign-in, every Pulse product | PulsePass.My, ThePulseNet, ThePulseCom, and the upcoming thePulse.dev all delegate to the same identity provider |
Zero passwords outside PulsePass.Id | No relying party ever sees or stores credentials; each holds only its own session cookie |
| Modern auth available to every account | TOTP and WebAuthn passkeys, persistent consent, recovery codes, activity log |
| Brand colors authored once | Light + dark variants per organization and per website, inherited via a deterministic null-coalescing chain on the OIDC claim |
| Three environments, no leaks | IPulseEnvironmentUrls + per-environment Key Vault + admin-applied migrations means a Staging deployment never silently links into Production |
| One design system, three apps | PulsePass.Design ships compiled CSS, an SVG sprite, and the Geist webfont consumed identically by Id / My / Web |
| Two pivots, zero downtime | Duende → OpenIddict and ASP.NET Core → Blazor both shipped without breaking sign-in for any consuming app |
Technology Stack
| Framework | ASP.NET Core / Blazor Server (.NET 10), every PulsePass surface |
| Identity | ASP.NET Core Identity + custom PulsePassSignInManager |
| Authorization server | OpenIddict (OIDC + OAuth 2.0, Auth Code + PKCE, persistent consent) |
| Strong auth | WebAuthn passkeys, TOTP authenticator apps, recovery codes |
| Persistence | Azure SQL via Entity Framework Core 10, three independent databases |
| Hosting | Azure App Service (Linux), per-environment slots |
| Secrets | Azure Key Vault with DefaultAzureCredential (dev) and managed identity (Staging / Prod) |
| Storage | Azure Blob Storage for avatar renditions (private containers + anonymous streaming endpoint) |
| Design system | PulsePass.Design RCL — SCSS tokens, compiled CSS, SVG sprite, self-hosted Geist webfont |
| Cross-env URLs | IPulseEnvironmentUrls + StagingIndicator.razor |
| Logging | Serilog with structured per-request correlation |
| Documentation | In-app admin docs viewer (Pulse.Docs RCL) rendering Markdown next to the code |
What Made This Project Interesting
Two pivots, one product. Most projects survive at most one architectural reversal. PulsePass survived two — a license-driven move from Duende to OpenIddict, and a coherence-driven move from mixed ASP.NET Core to Blazor everywhere — and shipped both without breaking sign-in for a single consuming app. The lesson the second time around was the same as the first: the cost of replacing a layer that is already wrong is always less than the cost of paying for the wrong layer for another year.
Brands inherit, they don't duplicate. The four-column light + dark color model with a null-coalescing resolution chain ended up being the template for how the whole identity surface is shaped. Claims omit absent values rather than serializing nulls. Organizations inherit defaults from the app, websites inherit from organizations, and consumers inherit from claims they actually receive. "Absent means inherit" is more honest than "null means inherit", and it kept the OIDC tokens small.
Identity has to be the boring layer. PulsePass.Id is intentionally the smallest and most uneventful service in the ecosystem. It signs people in, issues tokens, streams avatars, and nothing else. Everything interesting — organizations, memberships, websites, brand colors, audit history, billing — lives one layer up in PulsePass.My or in the consuming applications. That separation is what lets a product like thePulse.dev show up next quarter as a sign-in scheme and a claim reader, not an identity rebuild.