Authentication and organizations
API
apps/api/server/auth/better-auth.instance.ts builds a singleton betterAuth({...}):
- Adapter: drizzle (
pg) with tables from@taman/db-pg(user, session, account, verification, organization, team, teamMember, member, invitation). - Plugins:
admin(adminAc/adminRoles) andorganization(organizationAc/organizationRoles, teams enabled, no default team on org create). - Email/password: enabled,
requireEmailVerification: true. - Email verification:
autoSignInAfterVerification: true; sends via the email dispatcher. - Google:
socialProviders.googlewithprompt: 'select_account'. Empty client id/secret means Google login will fail until you set them. - Cookies: prefix
taman. IDs are UUIDv7. - Extra user field:
phoneNumber(optional string). - Account linking: enabled.
- Session hook: if
activeOrganizationIdis missing on create,resolveActiveOrganizationIdtries to pick one.
HTTP surface: apps/api/server/routes/api/auth/[...all].ts. RPC procedures do not re-implement login; they read the session through resolveContext.
Non-admin RPC callers without activeOrganizationId get ORPCError('FORBIDDEN') with data.code = ORG_REQUIRED (@taman/api-contract).
Web client
apps/web/src/auth/auth.client.ts:
export const authClient = createAuthClient({
baseURL: apiUrl,
plugins: [
adminClient({ ac: adminAc, roles: adminRoles }),
],
});
organizationClient. Organization/team helpers (authClient.organization.*) are not typed or available in the SPA. The API still runs the organization plugin (invitations, members, teams).Session is resolved in the router guard via ensureSession() (TanStack Query, cookies). registerAccessDirective reads roles from useSessionStore().roles through a lazy getter so Vue Query exists before the first useQuery.
Route meta
meta.auth (from @vinicunca/taman-core/typings):
| Value | Who may enter |
|---|---|
false | No auth check; excluded from generateAccess |
{ only: 'guest' } | Unauthenticated only (login). Authenticated users go to defaultHomePath |
{ only: 'user' } / omitted | Authenticated. Default. |
Root (path: '/') declares auth: false but AUTH_LAYOUT_ROUTE_NAMES skips it so that value does not leak to children. Leaf meta is resolved by walking matched from the leaf up (resolveAuthMetaFromMatched).
Login route: /auth/login (LOGIN_PATH). There is no onboarding route or route mode yet. A signed-in user without an organization reaches protected pages, and API calls fail with ORG_REQUIRED.
Current UI gaps
These are missing in apps/web/src/views today:
- No profile page
- No onboarding page or route mode
- No organization create / switch / invite / member views
Generated apps therefore cannot complete an org-required flow in the UI.
Organization checks happen in two places:
- The
authedRPC middleware rejects a session withoutactiveOrganizationId, unless the user is a platform admin (role: admin). - Each feature service still needs a membership.
TodoServicethrowsORG_REQUIREDwhenmemberisnull, because every query is scoped tomember.organizationId. The admin role does not bypass this.
nx run api:seed therefore creates admin@taman.local and a demo organization with the admin as owner. When a session starts, the session.create hook (resolveActiveOrganizationId) makes the user's first membership the active organization. If you were already signed in before seeding, sign out and back in.
Google OAuth
Set NITRO_GOOGLE_CLIENT_ID and NITRO_GOOGLE_CLIENT_SECRET. Authorized redirect URIs must include {NITRO_BASE_URL}/api/auth/callback/google. NITRO_TRUSTED_ORIGINS must include the web origin.