Authentication
Authentication arrives with a SaaS starter pack. The free template has no login. Pick exactly one starter:
This page is the architecture. Credential setup stays on the pack pages.
Where auth lives
| Concern | Typical path after install |
|---|---|
| Auth middleware | apps/backend/src/middleware/auth/auth-middleware.ts |
| Authenticated app helper | apps/backend/src/utils/auth/auth-hono.ts (getAuthenticatedApp()) |
| Auth HTTP routes | apps/backend/src/routes/auth/ |
| Permissions middleware | apps/backend/src/middleware/permissions/has-access.ts |
| Ability builder | apps/backend/src/libs/permissions/permissions.ts |
| Private frontend layout | apps/frontend/src/routes/(private)/route.tsx |
| Onboarding layout | apps/frontend/src/routes/onboarding/route.tsx |
| Post-auth redirect helper | apps/frontend/src/features/auth/utils/post-auth-redirect.ts |
WorkOS keeps session state in a sealed cookie (wos-session). Better Auth mounts its handler under /api/auth/* and
reads the session through auth.api.getSession.
Sign-in flow (Better Auth)
Email or social sign-in
Browser submits credentials
The frontend auth forms call the Better Auth client or the pack's /auth routes.
Better Auth handler
Requests under /api/auth/* hit the betterAuth() config in apps/backend/src/libs/auth/auth.ts.
Session + user records
Better Auth creates or updates the user and session (organization plugin enabled in the starter).
Optional transactional email
Password reset and verification emails go out through Resend when RESEND_API_KEY is set.
See MailingRedirect into the app
Users without an organization land on /onboarding. Everyone else goes to /.
WorkOS follows the same product shape (login → session cookie → onboarding or app), but identity and memberships are resolved through the WorkOS API instead of local Better Auth tables. Google OAuth callback URLs differ per provider; set them on the pack setup page.
Protected API requests
Authenticated routers use getAuthenticatedApp(), which runs withAuthUser first. That middleware:
- Resolves the current user from the session
- Resolves organization membership and permissions for the active org
- Builds a CASL ability and stores
user,organizationId, andabilityon the Hono context
Routes then gate actions with:
hasAccess({ action: "read", subject: "project" });hasAccess requires a user, an organizationId, and ability.can(action, subject). Presets ship in the enums package
(admin gets manage:all; member gets a narrower set). See Organizations.
Frontend route protection
| Layout | Behavior |
|---|---|
(private) | Loads sdk.users.me. Unauthenticated users go to /login. Users with no organization go to /onboarding. |
onboarding | Reverse guard: if the user already has an organization, leave onboarding. |
Admin (private) | Separate admin app login; requires an admin role or SUPER_ADMIN_EMAIL. |
Side effects to remember
- Auth changes can trigger email (verification, password reset, invitations). See Mailing.
- Wrong
FRONTEND_URL,BETTER_AUTH_URL, or WorkOS redirect URLs break callbacks. See Troubleshooting. - The admin app uses the same identity providers with a separate allowlist.
Related setup pages
Every purchase and sponsorship funds my 8+ years of work on open source given freely to the community. It keeps the lights on, funds new packs, and keeps the ecosystem alive. Even a small tier means a lot. Thank you!
