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.