Enterprise features are a tax. We paid it once, in the open.
@alphabros/enterprise: SSO, SCIM, organizations, an audit trail and an admin portal for any better-auth app — MIT, on npm today.
Every B2B product hits the same wall. A prospect's IT team asks for SSO through their identity provider. Then for SCIM, so accounts appear and disappear with their HR system. Then for an audit log they can hand to a compliance officer, and a rule that says "everyone in our domain must log in through our IdP, no exceptions". None of it makes the product better for the people who use it every day. All of it is table stakes for the contract.
We run eight products. Seven of them authenticate with better-auth, each with its own copy of the same setup, its own database and its own Stripe account. Building that wall seven times was never going to happen. Buying it per connection from a US vendor didn't fit either. So we built it once, as a package every product embeds, and we published it.
@alphabros/enterprise is on npm, MIT-licensed, version 0.1.0.
Source: github.com/ShipFast-Syndicate/enterprise.
What you get
One package, four entry points, no framework-specific server code:
/server— a preset of better-auth plugins: organizations with teams and roles, SSO (SAML 2.0 and OIDC) with DNS domain verification and just-in-time provisioning, SCIM 2.0 provisioning for Users and Groups, two-factor, passkeys, API keys, and two of our own: a hash-chained audit log and an organization policy engine (require 2FA, enforce SSO with a break-glass owner, session lifetime, allowed sign-in methods, group-to-role mapping)./schema— the Drizzle tables, a plain SQL migration, and a CLI:ab-enterprise migratecopies the migration into your project,ab-enterprise verifychecks the live database has every table and column (migrations that never reached production are a real failure mode; we check after every deploy),ab-enterprise audit-verifyrecomputes an organization's audit chain./client— the matching client plugins, plusdiscoverHomeRealm(email): the "email first, then route to your IdP" step every enterprise login page needs./portal— seven Web Components (Lit) that make the whole thing self-serve for a customer's IT admin: members, an SSO wizard (paste IdP metadata, verify the domain by DNS TXT, do a mandatory test login, then enforce), SCIM tokens, security policy, API keys, and an audit viewer with CSV export and chain verification. They work unchanged in SvelteKit, Astro, Next and plain HTML, and they ship no colours, fonts or spacing of their own — you map your design tokens onto--ab-*variables.
The one thing the package does not decide is who pays for what. You pass a
resolveEntitlements(orgId) function that reads your own billing plan and returns the
features that organization has. Every portal endpoint answers 403 without the feature.
Entitlements stay in your product; the package only enforces them.
Five minutes to a working setup
// auth.ts
import { betterAuth } from "better-auth";
import { enterprisePreset } from "@alphabros/enterprise/server";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "sqlite" }),
plugins: [
...enterprisePreset({
product: "myapp",
secretsKey: process.env.ENTERPRISE_SECRETS_KEY!, // ≥ 32 chars, from your secret store
resolveEntitlements: async (orgId) => planFeatures(await getPlan(orgId)),
}),
],
});
npx ab-enterprise migrate --out drizzle # drops the SQL migration into your folder
npx ab-enterprise verify # against TURSO_DATABASE_URL after deploy
<ab-security-settings org-id="org_123"></ab-security-settings>
That's the whole integration. The SSO wizard, SCIM endpoints and audit log are live behind
your existing /api/auth handler.
Design choices, and why
Embedded, not central. We deliberately did not build an identity service. Each product
keeps its own users, its own database and its own IdP connections. A central identity
plane is a single point of failure and a user migration; we'll get there later, and the
data model already leaves room for it (studio_ref columns, per-organization SSO
config), but not as the first step.
Built on better-auth's own plugins where they exist. organization, sso, scim,
admin, twoFactor, passkey and apiKey are upstream. We added what upstream lacks:
SCIM Groups on the 1.6 line (mapped onto organization teams, with a role-mapping table),
the audit chain, the policy engine, the entitlement gate, the portal, and the secrets
wrapper that encrypts IdP client secrets at rest with your key (AES-256-GCM, bound to the
provider row).
It runs on Cloudflare Workers. Four of our products deploy there. SAML on workerd
was the open question; we proved samlify's signing and verification, encrypted assertions
included, under nodejs_compat before writing a line of the package.
Tamper-evident audit log. Each organization's events form a hash chain. verify
recomputes it; retention works by compaction with a chained anchor rather than deletion,
so a year-old log still verifies. It's honest about its limit: someone with write access
to your database can rewrite the chain, which is why the CLI exists and why we recommend
exporting checkpoints.
What we did about security before publishing
We ran two audits before the first publish, one on the repository and supply chain, one
on the code, and a whole-branch review after the fixes. They found real things: an
audit-log injection across tenants, a role recompute that could demote an organization
owner through a SCIM group change, a retention purge that broke the chain, an unused
encryption key. All of it is fixed, each with a regression test that reproduces the
original exploit. The production dependency tree is a single package (zod). Releases
are published from CI through npm Trusted Publishing with provenance; there is no
long-lived publish token. The one accepted advisory on the 1.6 line is documented in
docs/security.md, with the mitigation and the test that guards it.
What it is not, yet
- Pilot-grade. 0.1.0 has 397 tests and no production tenant yet. Two of our products go first; expect breaking changes before 1.0.
- Pinned to better-auth 1.6.33. The SSO and SCIM plugins for 1.7 are a breaking peer change; we stay on 1.6 until the fleet moves, and the package moves with it.
- Your billing, your rules. No Stripe integration, no pricing page. One function.
- No central login across products. Each app is its own tenant boundary.
Contributing
Issues are open on the repository; that's the intake for now, and every issue becomes a tracked task on our side. Security findings go through GitHub's private vulnerability reporting, not a public issue. Pull requests from outside the organization aren't accepted yet; we'll open that up once the pilots are through.
If your product has been putting off the enterprise tier because it's a tax with no upside for your users, this is the tax paid once. Use it.