PulseAuth 1.2.4
dotnet add package PulseAuth --version 1.2.4
NuGet\Install-Package PulseAuth -Version 1.2.4
<PackageReference Include="PulseAuth" Version="1.2.4" />
<PackageVersion Include="PulseAuth" Version="1.2.4" />
<PackageReference Include="PulseAuth" />
paket add PulseAuth --version 1.2.4
#r "nuget: PulseAuth, 1.2.4"
#:package PulseAuth@1.2.4
#addin nuget:?package=PulseAuth&version=1.2.4
#tool nuget:?package=PulseAuth&version=1.2.4

PulseAuth
Free, open-source OAuth2 / OpenID Connect authorization server for ASP.NET Core.
PulseAuth is a lightweight alternative to Duende IdentityServer designed to be accessible, free, and easy to set up. It implements the core OAuth2 and OIDC flows on top of ASP.NET Core minimal APIs and integrates natively with ASP.NET Core Identity.
Packages
| Package | Description |
|---|---|
PulseAuth |
Core OAuth2/OIDC server — endpoints, token service, in-memory stores |
PulseAuth.Identity |
Connects PulseAuth to ASP.NET Core Identity (UserManager, SignInManager) |
PulseAuth.EntityFramework |
EF Core persistent stores (clients, codes, refresh tokens) |
Each package targets net8.0, net9.0 and net10.0.
Supported flows
- Authorization Code + PKCE — secure for web apps, SPAs and mobile
- Client Credentials — service-to-service authentication
- Refresh Token — with optional rotation
- Resource Owner Password — direct username/password login (ideal for React/Angular SPAs that own their login UI)
- Google ID Token exchange — accept a Google-issued ID token from the frontend SDK and return PulseAuth tokens
- Facebook Access Token exchange — accept a Facebook access token and return PulseAuth tokens
OIDC endpoints
| Endpoint | URL |
|---|---|
| Discovery | /.well-known/openid-configuration |
| JWKS | /.well-known/jwks |
| Authorize | /connect/authorize |
| Token | /connect/token |
| UserInfo | /connect/userinfo |
| Revocation | /connect/revocation |
| End Session | /connect/endsession |
Quick start
1. Install packages
dotnet add package PulseAuth
dotnet add package PulseAuth.Identity
dotnet add package PulseAuth.EntityFramework
# Database provider — Pomelo for MariaDB/MySQL (recommended)
dotnet add package Pomelo.EntityFrameworkCore.MySql
# Or for SQL Server:
# dotnet add package Microsoft.EntityFrameworkCore.SqlServer
# EF Core CLI tools (if not already installed)
dotnet tool install --global dotnet-ef
MariaDB users: use Pomelo (
Pomelo.EntityFrameworkCore.MySql), not Oracle'sMySql.EntityFrameworkCore. Oracle's connector has known incompatibilities with MariaDB'sinformation_schemathat cause runtime errors. See Troubleshooting for details.
2. appsettings.json
{
"ConnectionStrings": {
"AuthDb": "Server=localhost;Port=3306;Database=myauth;User=root;Password=secret;"
},
"PulseAuth": {
"Issuer": "https://auth.myapp.com"
},
"Auth": {
"Google": { "ClientId": "" },
"Facebook": { "AppId": "", "AppSecret": "" }
}
}
3. Choose a context strategy
PulseAuth supports three patterns. Pick the one that fits your app:
Option A — Single migration for Identity + PulseAuth (recommended)
Inherit PulseAuthIdentityDbContext<TUser>. One migration creates both AspNet* (Identity) and PulseAuth_* tables in the same database. No double-context confusion.
// Infrastructure/Contexts/AuthDbContext.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using PulseAuth.EntityFramework.DbContexts;
public class AuthDbContext : PulseAuthIdentityDbContext<IdentityUser>
{
public AuthDbContext(DbContextOptions<AuthDbContext> options) : base(options) { }
// Add your own application DbSets here if needed
}
Then in Program.cs:
// Identity registers its stores via AuthDbContext
builder.Services
.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AuthDbContext>()
.AddDefaultTokenProviders();
// PulseAuth reads the same context that Identity already registered
builder.Services.AddPulseAuth(...)
.AddEntityFrameworkStoresWithIdentity<AuthDbContext>();
AddEntityFrameworkStoresWithIdentity<TContext>does not re-register the DbContext — it only resolvesTContextfrom DI (which Identity already registered) and wires up the PulseAuth stores.
Option B — PulseAuth tables only (inherit PulseAuthDbContext)
Inherit PulseAuthDbContext if you don't use ASP.NET Core Identity or you want Identity in a separate context.
public class AppDbContext : PulseAuthDbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Order> Orders { get; set; } = default!;
}
Option C — Fully standalone
Use PulseAuthDbContext directly. Identity and PulseAuth each use their own context (can be the same or different databases).
4. Program.cs — Option A (recommended)
Important: all
builder.Servicescalls must come beforebuilder.Build().
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using PulseAuth.Constants;
using PulseAuth.EntityFramework.Extensions;
using PulseAuth.Extensions;
using PulseAuth.Identity.Extensions;
using PulseAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var conn = builder.Configuration.GetConnectionString("AuthDb")!;
// ── ASP.NET Core Identity ─────────────────────────────────────────────────────
// Identity registers AuthDbContext in DI and configures its own stores.
builder.Services
.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AuthDbContext>()
.AddDefaultTokenProviders();
// Register AuthDbContext with EF Core (must come after AddIdentity, before Build)
builder.Services.AddDbContext<AuthDbContext>(opts =>
opts.UseMySql(conn, new MariaDbServerVersion(new Version(10, 11, 0))));
// ── PulseAuth ─────────────────────────────────────────────────────────────────
builder.Services
.AddPulseAuth(opts =>
{
opts.Issuer = builder.Configuration["PulseAuth:Issuer"]!;
opts.RotateRefreshTokens = true;
})
.AddDeveloperSigningCredential() // swap for persistent RSA key in prod
.AddIdentityUsers<IdentityUser>()
// Reuses the AuthDbContext already registered above by Identity
.AddEntityFrameworkStoresWithIdentity<AuthDbContext>()
.AddInMemoryClients(
[
new Client
{
ClientId = "my-spa",
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.RefreshToken,
GrantTypes.GoogleIdToken, GrantTypes.FacebookAccessToken],
AllowOfflineAccess = true,
AllowedScopes = ["openid", "profile", "email", "offline_access", "api"],
},
new Client
{
ClientId = "my-api",
ClientSecretHash = ClientSecretHelper.HashSecret("change-in-prod"),
AllowedGrantTypes = GrantTypes.ClientCredentialsOnly,
AllowedScopes = ["api"],
},
]);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapPulseAuth();
app.Run();
5. Migrations
PulseAuth.EntityFramework ships without an embedded database provider — migrations are generated from your startup project so the SQL matches your actual provider (MariaDB, SQL Server, PostgreSQL, etc.).
5.1 Create a design-time factory
Add this file to your startup project. It lets dotnet ef instantiate the context without booting your full app:
// Infrastructure/Factories/AuthDbContextFactory.cs (Option A — PulseAuthIdentityDbContext)
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
public class AuthDbContextFactory : IDesignTimeDbContextFactory<AuthDbContext>
{
public AuthDbContext CreateDbContext(string[] args)
{
// Use a hardcoded dev connection string — AutoDetect requires a live server.
var cs = Environment.GetEnvironmentVariable("DESIGN_CONNECTION")
?? "Server=localhost;Port=3306;Database=myauth_dev;" +
"User Id=root;Password=secret;";
var options = new DbContextOptionsBuilder<AuthDbContext>()
.UseMySql(cs, new MariaDbServerVersion(new Version(10, 11, 0)))
// For MySQL: new MySqlServerVersion(new Version(8, 0, 0))
// For SQL Server: .UseSqlServer(cs)
.Options;
return new AuthDbContext(options);
}
}
Why
new MariaDbServerVersion(...)instead ofServerVersion.AutoDetect?AutoDetectopens a real connection to detect the version. In design-time (CI, fresh machines) the server may not be available. A hardcoded version avoids this and is safe — it only affects how EF generates the SQL dialect, not the runtime behavior.
5.2 Run migrations
# From your startup project directory:
# Option A — PulseAuthIdentityDbContext (single migration for ALL tables)
dotnet ef migrations add Init --context AuthDbContext --output-dir Migrations
dotnet ef database update --context AuthDbContext
# Option B/C — separate contexts (one migration per context)
dotnet ef migrations add InitIdentity --context ApplicationDbContext --output-dir Migrations/Identity
dotnet ef migrations add InitPulseAuth --context PulseAuthDbContext --output-dir Migrations/PulseAuth
dotnet ef database update --context ApplicationDbContext
dotnet ef database update --context PulseAuthDbContext
Option A creates both
AspNet*andPulseAuth_*tables in a singledotnet ef database update. The table prefixes prevent naming collisions even in the same database.
Hashing client secrets
Never store plaintext secrets. Use the helper:
var (plain, hash) = ClientSecretHelper.GenerateAndHash();
Console.WriteLine($"Secret: {plain}"); // share with the client
Console.WriteLine($"Hash: {hash}"); // store in DB / config
// Or hash an existing value:
string hash = ClientSecretHelper.HashSecret("my-secret");
React / Angular SPA — pure API mode
Keep all UI in the frontend. PulseAuth exposes a REST token endpoint — no Razor pages required.
Email/password login
// Client config
new Client
{
ClientId = "my-spa",
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.RefreshToken],
AllowOfflineAccess = true,
AllowedScopes = ["openid", "profile", "email", "offline_access", "api"],
}
// React / TypeScript
const res = await fetch('https://auth.myapp.com/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'password',
client_id: 'my-spa',
username: email,
password: password,
scope: 'openid profile email offline_access api',
}),
});
const { access_token, refresh_token, id_token } = await res.json();
Google Sign-In (ID token exchange)
// Auth server
builder.Services.AddPulseAuth(...)
.AddGoogleTokenExchange(
builder.Configuration["Auth:Google:ClientId"]!);
// Client
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.GoogleIdToken, GrantTypes.RefreshToken],
// After Google SDK returns a credential (ID token)
const res = await fetch('https://auth.myapp.com/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:google_id_token',
client_id: 'my-spa',
token: googleCredential,
scope: 'openid profile email offline_access api',
}),
});
Facebook Login (access token exchange)
// Auth server
builder.Services.AddPulseAuth(...)
.AddFacebookTokenExchange(
builder.Configuration["Auth:Facebook:AppId"]!,
builder.Configuration["Auth:Facebook:AppSecret"]!);
// Client
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.FacebookAccessToken, GrantTypes.RefreshToken],
// After FB.login() returns authResponse.accessToken
const res = await fetch('https://auth.myapp.com/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:facebook_access_token',
client_id: 'my-spa',
token: fbAccessToken,
scope: 'openid profile email offline_access api',
}),
});
Refreshing tokens
const res = await fetch('https://auth.myapp.com/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'my-spa',
refresh_token: storedRefreshToken,
}),
});
Roles and custom claims in tokens
By default AddIdentityUsers<TUser>() maps standard profile fields (name, given_name, email, etc.) but does not include roles or custom claims. Enable them with the optional configureClaims delegate:
// Include everything: roles + all custom claims from AspNetUserClaims
.AddIdentityUsers<IdentityUser>(claims =>
{
claims.IncludeRoles = true;
claims.IncludeUserClaims = true;
})
// Only roles (no custom claims)
.AddIdentityUsers<IdentityUser>(claims =>
{
claims.IncludeRoles = true;
})
// Only specific claim types (e.g. department and tenant)
.AddIdentityUsers<IdentityUser>(claims =>
{
claims.IncludeUserClaims = true;
claims.ClaimTypeFilter = ["department", "tenant"];
})
// Roles + specific claim types
.AddIdentityUsers<IdentityUser>(claims =>
{
claims.IncludeRoles = true;
claims.IncludeUserClaims = true;
claims.ClaimTypeFilter = ["department", "tenant", "subscription_plan"];
})
The resulting JWT for a user with role Admin and a custom claim department=engineering:
{
"sub": "abc-123",
"email": "user@example.com",
"role": "Admin",
"department": "engineering",
"exp": 1719000000
}
| Option | Type | Default | Description |
|---|---|---|---|
IncludeRoles |
bool |
false |
Adds roles from AspNetUserRoles as role claims |
IncludeUserClaims |
bool |
true |
Adds custom claims from AspNetUserClaims |
ClaimTypeFilter |
ICollection<string> |
[] (all) |
When non-empty, restricts which custom claim types are included |
Profile fields (
name,given_name,family_name,picture) are always mapped regardless of these settings.
Managing roles and claims with Identity
// Add a role to a user
await userManager.AddToRoleAsync(user, "Admin");
// Add a custom claim to a user
await userManager.AddClaimAsync(user, new Claim("department", "engineering"));
await userManager.AddClaimAsync(user, new Claim("tenant", "acme"));
// Remove
await userManager.RemoveFromRoleAsync(user, "Admin");
await userManager.RemoveClaimAsync(user, new Claim("department", "engineering"));
Role and claim changes take effect on the next login (next token issued). Existing tokens remain valid until they expire. Use short token lifetimes (
AccessTokenLifetime) if you need changes to propagate faster.
Using roles in microservices
// Attribute-based
app.MapGet("/reports", [Authorize(Roles = "Admin,Manager")] () => ...);
// Policy-based
builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
opts.AddPolicy("PremiumUsers", p => p.RequireClaim("subscription_plan", "premium"));
});
app.MapGet("/dashboard", () => ...).RequireAuthorization("AdminOnly");
Validating tokens in other microservices
Any ASP.NET Core microservice can validate PulseAuth tokens using standard JWT Bearer — no PulseAuth package required:
// In any microservice Program.cs
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.Authority = "https://auth.myapp.com"; // PulseAuth discovery endpoint
opts.Audience = "my-spa";
opts.RequireHttpsMetadata = false; // dev only
});
builder.Services.AddAuthorization();
// ...
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/orders", (ClaimsPrincipal user) => ...)
.RequireAuthorization();
The microservice fetches public keys from /.well-known/jwks automatically and caches them. No shared secrets, no extra dependencies beyond Microsoft.AspNetCore.Authentication.JwtBearer.
Custom user store
Implement IUserAuthenticationService to use any user database (without ASP.NET Core Identity):
public class MyUserService : IUserAuthenticationService
{
public Task<UserInfo?> ValidateCredentialsAsync(string user, string pass, CancellationToken ct) { ... }
public Task<UserInfo?> GetUserByIdAsync(string subjectId, CancellationToken ct) { ... }
public Task<UserInfo?> FindByExternalProviderAsync(string provider, string externalId, CancellationToken ct) { ... }
public Task<UserInfo> AutoProvisionUserAsync(string provider, string externalId,
IEnumerable<Claim> claims, CancellationToken ct) { ... }
}
// Register instead of .AddIdentityUsers<T>():
builder.Services.AddPulseAuth(...)
.AddUserAuthentication<MyUserService>();
Login page (Authorization Code flow)
PulseAuth redirects to LoginPath (default /Account/Login) when the user is not authenticated. Your login page signs the user in via ASP.NET Core Identity and redirects back:
public class LoginModel : PageModel
{
public async Task<IActionResult> OnPostAsync(string returnUrl = "/")
{
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, false, false);
if (result.Succeeded)
return LocalRedirect(returnUrl);
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
Troubleshooting
Object cannot be cast from DBNull when running database update
Cause: Oracle's MySql.EntityFrameworkCore package is not fully compatible with MariaDB. Its internal character-set loader expects columns that MariaDB's information_schema returns as NULL.
Fix: replace Oracle's package with Pomelo, which is built for both MySQL and MariaDB:
dotnet remove package MySql.EntityFrameworkCore
dotnet add package Pomelo.EntityFrameworkCore.MySql
Then replace UseMySQL (capital SQL) with UseMySql everywhere:
opts.UseMySql(conn, ServerVersion.AutoDetect(conn))
// or with a pinned version (recommended for design-time):
opts.UseMySql(conn, new MariaDbServerVersion(new Version(10, 11, 0)))
'Requested value 'None' was not found.' during migrations
Cause: Oracle's MySql.EntityFrameworkCore cannot parse SslMode=None — that value does not exist in its enum. The design-time factory's connection string contains an invalid SSL mode, or the connection string passed to UseMySQL is empty.
Fix (if still using Oracle's package): use SslMode=Disabled instead of SslMode=None, and ensure the connection string is never empty:
var cs = "Server=localhost;Port=3306;Database=myauth;User Id=root;Password=secret;SslMode=Disabled;";
Recommended fix: switch to Pomelo (see above) — this error does not occur with Pomelo.
Unable to create a 'DbContext' — factory not found
EF tools print this when no IDesignTimeDbContextFactory<T> is found in your startup project and the application host startup also fails.
Ensure your factory: (1) is public, (2) is in your startup project (not in the library), (3) implements IDesignTimeDbContextFactory<YourContext> where YourContext matches the --context argument.
Services registered after builder.Build()
All builder.Services.Add* calls must come before var app = builder.Build(). Services added after Build() are silently ignored.
// ✅ Correct
builder.Services.AddDbContext<AppDbContext>(...);
builder.Services.AddPulseAuth(...);
var app = builder.Build();
// ❌ Wrong — these services are never registered
var app = builder.Build();
builder.Services.AddDbContext<AppDbContext>(...);
AuthDbContext cannot be resolved — inheriting PulseAuthDbContext
When your context inherits PulseAuthDbContext, its constructor must accept DbContextOptions<YourContext> (not DbContextOptions<PulseAuthDbContext>):
// ❌ Wrong — DI registers DbContextOptions<AppDbContext>, not <PulseAuthDbContext>
public AppDbContext(DbContextOptions<PulseAuthDbContext> options) : base(options) { }
// ✅ Correct — PulseAuthDbContext has a protected constructor that accepts DbContextOptions
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
And in Program.cs use the generic overload so DI wires up AppDbContext as PulseAuthDbContext:
.AddEntityFrameworkStores<AppDbContext>(opts => opts.UseMySql(conn, version))
Cannot create a DbSet for 'IdentityUser' at runtime
Cause: Your context inherits PulseAuthDbContext (which does NOT include Identity tables) but ASP.NET Core Identity expects Identity's DbSets to be present.
Fix: switch to PulseAuthIdentityDbContext<TUser> (Option A) so a single context includes both Identity and PulseAuth tables:
// ❌ Wrong — no Identity tables
public class AuthDbContext : PulseAuthDbContext { ... }
// ✅ Correct — includes both AspNet* and PulseAuth_* tables
public class AuthDbContext : PulseAuthIdentityDbContext<IdentityUser> { ... }
CS1929 / ambiguous call on AddEntityFrameworkStores with Identity
Cause: Both PulseAuth.EntityFramework and Microsoft.AspNetCore.Identity.EntityFrameworkCore define AddEntityFrameworkStores<TContext> extension methods on IdentityBuilder. The compiler cannot resolve which one to call.
Fix: split the calls so there is no ambiguity — call Identity's version via the IdentityBuilder chain, then call PulseAuth's version separately on the PulseAuthBuilder:
// ✅ No ambiguity
builder.Services
.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AuthDbContext>() // Identity's extension
.AddDefaultTokenProviders();
builder.Services.AddDbContext<AuthDbContext>(opts => opts.UseMySql(conn, version));
builder.Services.AddPulseAuth(...)
.AddEntityFrameworkStoresWithIdentity<AuthDbContext>(); // PulseAuth's extension
💖 Support
This project is developed and maintained by Andrés Mariño. If you find this library useful, consider supporting its continued development:
📝 License
MIT — see LICENSE for details.
Made with ❤️ for the .NET community
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Authentication.Facebook (>= 10.0.0 && < 11.0.0)
- Microsoft.AspNetCore.Authentication.Google (>= 10.0.0 && < 11.0.0)
- Microsoft.AspNetCore.Authentication.MicrosoftAccount (>= 10.0.0 && < 11.0.0)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.0.0)
- Microsoft.IdentityModel.Tokens (>= 8.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.0.0)
-
net8.0
- Microsoft.AspNetCore.Authentication.Facebook (>= 8.0.0 && < 9.0.0)
- Microsoft.AspNetCore.Authentication.Google (>= 8.0.0 && < 9.0.0)
- Microsoft.AspNetCore.Authentication.MicrosoftAccount (>= 8.0.0 && < 9.0.0)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.0.0)
- Microsoft.IdentityModel.Tokens (>= 8.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.0.0)
-
net9.0
- Microsoft.AspNetCore.Authentication.Facebook (>= 9.0.0 && < 10.0.0)
- Microsoft.AspNetCore.Authentication.Google (>= 9.0.0 && < 10.0.0)
- Microsoft.AspNetCore.Authentication.MicrosoftAccount (>= 9.0.0 && < 10.0.0)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.0.0)
- Microsoft.IdentityModel.Tokens (>= 8.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on PulseAuth:
| Package | Downloads |
|---|---|
|
PulseAuth.Identity
ASP.NET Core Identity integration for PulseAuth — connects PulseAuth's user authentication to Identity's UserManager and SignInManager. |
|
|
PulseAuth.EntityFramework
Entity Framework Core persistent stores for PulseAuth — clients, authorization codes, refresh tokens and grants backed by any EF Core provider. |
GitHub repositories
This package is not used by any popular GitHub repositories.