The Problem
Every new SaaS idea needs the same unglamorous plumbing — orgs, invites, SSO, billing, an audit trail, and some form of support tooling — before it can do anything specific to the product. Rebuilding that each time is wasted effort, so the project documents an explicit fork-and-rebrand workflow (rename, strip unused features, re-key secrets) as the intended way to start a new product from this base.
My Role
Solo, self-directed — built end to end as a personal foundation, not client work.
What I Built
- An organization/member/invitation model on Better Auth, with invite-only signup enforced at a single choke point so no auth flow — password, OAuth, magic link, or SSO — can bypass it
- SSO/SAML via Better Auth's SSO plugin plus an optional BoxyHQ Jackson sidecar, with org-scoped SAML metadata and post-login provisioning
- A Payload CMS backend of 20+ collections spanning auth, support, onboarding, M365 integration, and permissions
- Stripe billing with webhook-driven subscription sync, seat-based entitlements, and idempotent event processing
- An append-only audit log where create and update are hard-denied at the collection level — the only write path is a server-side helper, so audit rows can't be forged or edited through the API or admin UI
- Stratos Support: an OpenRouter-routed conversational agent for Microsoft 365 IT issues, with a scripted phase machine (gathering → findings → checklist → escalation) split across a tenant-facing plane and a cross-tenant operator queue
Architecture
- 1
Next.js
App Router + Payload admin
- 2
Better Auth + SSO/SAML
Orgs, invitations, BoxyHQ Jackson
- 3
Payload CMS
20+ collections: support, billing, audit
- 4
PostgreSQL + Drizzle
Shared schema with Payload
- 5
Stripe + OpenRouter
Billing and the Stratos Support agent
Next.js App Router, split into a frontend route group and a Payload-generated admin/API route group that's treated as generated code and never hand-edited, so upgrades stay clean. A custom middleware layer resolves org membership from the URL slug and redirects non-members. Better Auth is wired directly onto Payload's own Postgres/Drizzle schema rather than run as a separate service.
Engineering Challenge
The support product needed real tenant isolation for customer-facing conversations, but also a cross-tenant queue for the team actually operating support — without standing up a second auth system or hand-writing per-customer proxy exceptions. Invite-only signup also had to hold across every auth method, not just the primary one, or it isn't actually invite-only.
Solution
The support product is split into two explicit planes — a tenant plane gated by ordinary org membership, and an operator plane gated by membership in one designated internal org — rather than a role flag or a per-route exception list. Invite-only enforcement lives in a single Better Auth lifecycle hook that every signup path runs through, instead of being duplicated per auth method.