# Identify users

Source: https://docs.mirafive.io/guides/identify-users

> Tie events to a browser, a session and a signed-in person, reset them on logout, and link browser and server events.

Identifying users ties events to the same browser, the same visit and your own user id, so MIRA FIVE can count people instead of pageviews. It needs [full mode](https://docs.mirafive.io/guides/consent#full-mode) and the visitor's consent. In the default consentless mode there are no ids at all, and none of this applies.

## The three ids

| Id | What it is | Who sets it | Where it lives |
| --- | --- | --- | --- |
| Anonymous id | A UUID for one browser | The browser SDK, on the first event after consent | `localStorage`, 365 days since last seen |
| Session id | A UUID for one visit | The browser SDK | `localStorage`, ends after 30 minutes without an event |
| User id | Your own id for the signed-in person | You, with `identify()` | Memory of the page; sent on every later event |

Sessions live in `localStorage`, not `sessionStorage`, so a link opened in a new tab stays in the same session.

The user id is pseudonymous: pass your internal id (`u_42`, a UUID, a database key), never an email address. It has 1 to 256 characters. In development, the browser SDK warns when an id contains `@`.

## Turn on full mode

Full mode needs the identity code in the page and a consent answer:

**Script tag**

```html
<script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
<script defer src="https://cdn.mirafive.io/mira.js" data-key="mf_…" data-mode="full"></script>
```

The identity chunk downloads on the first consent grant.

**Browser**

```ts
import { createMira } from '@mirafive/sdk-browser'
import { identity } from '@mirafive/sdk-browser/identity'
import { pageviews } from '@mirafive/sdk-browser/pageviews'

export const mira = createMira({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  mode: 'full',
  plugins: [pageviews(), identity()],
})
```

`mode: 'full'` without `identity()` throws when the client is created.

**React**

```tsx title="src/main.tsx"
import { createMira } from '@mirafive/sdk-browser'
import { identity } from '@mirafive/sdk-browser/identity'
import { pageviews } from '@mirafive/sdk-browser/pageviews'
import { MiraProvider } from '@mirafive/sdk-react'
import { createRoot } from 'react-dom/client'

import { App } from './App'

const mira = createMira({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  mode: 'full',
  plugins: [pageviews(), identity()],
})

createRoot(document.getElementById('root')!).render(
  <MiraProvider client={mira}>
    <App />
  </MiraProvider>,
)
```

Before a consent answer, full mode stores nothing and sends nothing. Pass the answer from your consent manager with `consent()`, as described in [Consent](https://docs.mirafive.io/guides/consent#consent-answers). Events are sent once statistics consent is granted.

## Identify after login

Call `identify(userId, traits)` once the person is signed in. It sends `$identify` with the traits as properties, and every later event on the page carries the user id:

**Script tag**

```html
<script>
  mirafive('identify', 'u_42', { plan: 'pro' })
</script>
```

**Browser**

```ts
mira.identify(user.id, { plan: user.plan })
```

**React**

```tsx
import { useMira } from '@mirafive/sdk-react'
import { useEffect } from 'react'

export function Session({ user }: { user: { id: string; plan: string } }) {
  const mira = useMira()

  useEffect(() => {
    mira.identify(user.id, { plan: user.plan })
  }, [mira, user.id, user.plan])

  return null
}
```

Rules:

- The user id is kept in memory only. Call `identify()` on every page load while the person is signed in, not only right after the login form.
- `$identify` needs statistics consent. Called before the grant, the user id is kept and stamped on later events, but the `$identify` event itself is dropped. Call `identify()` again after the grant to send the traits.
- Traits follow the [property limits](https://docs.mirafive.io/guides/track-events#properties) and hold no personal data you do not need: a plan or a role, not a name or an email address.
- When a different user signs in on the same browser, the SDK starts fresh anonymous and session ids first, so two people never share one.

## Reset on logout

Call `reset()` when the person signs out. It forgets the user, the anonymous id and the session, so the next event starts new ones:

**Script tag**

```html
<script>
  document.querySelector('#logout').addEventListener('click', () => mirafive('reset'))
</script>
```

**Browser**

```ts
await signOut()
mira.reset()
```

A consent decline (`consent(false)`) does the same and also clears the queue.

## Link browser and server events

A server does not know the browser's anonymous id unless the page sends it. Read it with `anonymousId()` and pass it with your own request, under any header or field name you choose. It is `undefined` without statistics consent.

**Script tag**

```html
<script>
  mirafive('anonymousId', (anonymousId) => {
    document.querySelector('input[name=mirafive_anonymous_id]').value = anonymousId ?? ''
  })
</script>
```

**Browser**

```ts
await fetch('/api/checkout', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Anonymous-Id': mira.anonymousId() ?? '' },
  body: JSON.stringify(cart),
})
```

On the server, pass it with the event. `identify()` with an anonymous id links that browser to the user:

**Node.js**

```ts
const anonymousId = request.headers.get('X-Anonymous-Id') || undefined

mira.identify(user.id, { plan: user.plan }, { anonymousId })
mira.track('order completed', { userId: user.id, anonymousId, properties: { revenue: 49.9, currency: 'EUR' } })
```

**PHP**

```php
$anonymousId = $_SERVER['HTTP_X_ANONYMOUS_ID'] ?? null;

$mira->identify((string) $user->id, ['plan' => $user->plan], anonymousId: $anonymousId);
$mira->track('order completed', userId: (string) $user->id, anonymousId: $anonymousId, properties: ['revenue' => 49.9, 'currency' => 'EUR']);
```

**Laravel**

```php
use MiraFive\Laravel\Facades\Mira;

$anonymousId = $request->header('X-Anonymous-Id');

Mira::forUser($request->user())->identify(['plan' => $request->user()->plan], anonymousId: $anonymousId);
Mira::forUser($request->user())->track('order completed', ['revenue' => 49.9, 'currency' => 'EUR'], anonymousId: $anonymousId);
```

Server events are sent in full mode by default, and you hold the consent for the ids you pass. Only pass an anonymous id the page gave you, which exists only after statistics consent. Server flag reads take the same id to keep a visitor's variants: see [Feature flags](https://docs.mirafive.io/guides/feature-flags#read-flags-on-a-server).

## What is stored where

| Where | What | Lifetime |
| --- | --- | --- |
| `localStorage` `mirafive:{ns}:aid` | `{anonymousId}.{lastSeenMs}` | 365 days since last seen |
| `localStorage` `mirafive:{ns}:sid` | `{sessionId}.{lastSeenMs}` | 30 minutes idle |
| `localStorage` `mirafive:{ns}:uid` | A hash of the user id, only to notice a different user signing in | 365 days |
| Page memory | The user id and traits from `identify()` | Until the page unloads or `reset()` |

`{ns}` is the key's namespace: the website key without its last `_…` part, for example `mf_ab12cd34`, so the key's tail is never stored. The SDKs set no cookies, in any mode. Nothing is stored before a consent scope is granted, and `consent(false)` or `reset()` removes all three entries.

Server SDKs store nothing on the visitor's device.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| Events have no user id | `identify()` did not run on this page load. Call it on every load while signed in. |
| No `$identify` event | It ran before statistics consent. Call `identify()` again after the grant. |
| `anonymousId()` is `undefined` | No statistics consent yet, the page is in consentless mode, or Do Not Track or Global Privacy Control is on. |
| `identify()` does nothing | The client has no `identity()` plugin or runs in consentless mode. A development warning says `identify() needs its plugin`. |
| Server throws on `userId` | The server client is in consentless mode. Use full mode where you hold consent. |
