---
title: "Authentication and organizations"
description: "better-auth on the API, the Vue client, route guards, and the current UI gaps."
canonical_url: "https://vinicunca.dev/taman/guides/authentication-and-organizations"
---
# Authentication and organizations

> better-auth on the API, the Vue client, route guards, and the current UI gaps.

## 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`) and `organization` (`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.google` with `prompt: '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 `activeOrganizationId` is missing on create, `resolveActiveOrganizationId` tries 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`:

```ts
export const authClient = createAuthClient({
  baseURL: apiUrl,
  plugins: [
    adminClient({ ac: adminAc, roles: adminRoles }),
  ],
});
```

> [!WARNING]
> 
> The client does **not** install `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:

1. The `authed` RPC middleware rejects a session without `activeOrganizationId`, unless the user is a platform admin (`role: admin`).
2. Each feature service still needs a membership. `TodoService` throws `ORG_REQUIRED` when `member` is `null`, because every query is scoped to `member.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.


## Sitemap

See the full [sitemap](https://vinicunca.dev/sitemap.md) for all pages.
