<!-- AI agents: this is the Markdown version of an AppNorth documentation page. Index: https://www.appnorth.app/llms.txt -->

# Verified identity

Use verified identity for identified community actions, account-based chat, and private report replies. Your authenticated backend decides which user to sign; the iOS app receives only a short-lived credential.

## Create the signing secret {#secret}

An administrator creates the project's identity secret in SDK setup. Store that secret and the internal **Project ID** in your backend environment. The public **App ID** passed to `AppNorthConfiguration` is a different value.

Never ship the secret or signing code in your app. A public App ID, an allowed `Origin`, and the anonymous voter token do not prove who a person is.

## Issue a credential {#sign}

Authenticate the request using your existing backend session. Obtain the email from that session, not a caller-supplied email. Sign the exact UTF-8 payload below with HMAC-SHA256 and return a hexadecimal signature. `expiresAt` is integer Unix seconds, with a maximum lifetime of 24 hours; this example uses 15 minutes.

```javascript title="Authenticated backend identity handler"
import { createHmac } from "node:crypto";

export function appNorthIdentity(authenticatedUser) {
  const projectId = process.env.APPNORTH_PROJECT_ID;
  const secret = process.env.APPNORTH_IDENTITY_SECRET;
  if (!projectId || !secret) throw new Error("Missing AppNorth identity configuration");
  const email = authenticatedUser.email.trim().toLowerCase();
  const expiresAt = Math.floor(Date.now() / 1000) + 15 * 60;
  const payload = `v1\n${projectId}\n${expiresAt}\n${email}`;
  const signature = createHmac("sha256", secret).update(payload).digest("hex");
  return { email, expiresAt, signature };
}
```

Return the credential over HTTPS from an authenticated endpoint with `Cache-Control: no-store`. Do not log the credential: it acts as a bearer credential until it expires.

## Install it in the client {#identify}

Decode your backend response and install it after sign-in and whenever your backend renews it:

```swift title="AppNorthIdentity.swift"
import AppNorth

struct IdentityResponse: Decodable {
    let email: String
    let expiresAt: Int64
    let signature: String
}

func applyIdentity(_ response: IdentityResponse, to client: AppNorthClient) throws {
    let credential = try AppNorthIdentityCredential(
        email: response.email,
        expiresAt: response.expiresAt,
        signature: response.signature
    )
    client.setIdentityCredential(credential)
}
```

The SDK keeps credentials in memory and sends them in `X-AppNorth-Identity-*` headers, never in the query or JSON body. `hasValidIdentity(at:)` and `isValid(at:)` check local expiry only; they do not verify HMAC signatures. An expired installed credential blocks requests locally with `INVALID_IDENTITY`. Renew through your backend rather than silently downgrading the action to anonymous.

## Add signed properties {#properties}

For properties, sign `v2\n<projectId>\n<expiresAt>\n<normalizedEmail>\n<canonicalProperties>`. Canonical properties are a JSON object serialized with keys sorted alphabetically. Return the same values to the app and pass them using `AppNorthIdentityProperty.string`, `.number`, or `.boolean` in the credential initializer.

There may be at most 20 keys. Keys start with a letter and contain only letters, digits, `_`, `.`, or `-`, up to 64 characters; `constructor` is forbidden. Values are strings of at most 200 characters, finite numbers, or booleans. Objects, arrays, and null are rejected, and the serialized object must fit in 2 KiB. Omitting properties uses v1; an empty properties object still uses v2. Changing properties on the client invalidates the signature.

## End or change the session {#logout}

Before switching accounts, dismiss SDK screens, stop custom polling, stop and discard any [RevenueCat helper](/docs/paywall-cards#account-changes), and cancel host-owned requests. Then clear the credential:

```swift title="ClearIdentity.swift"
import AppNorth

func clearAppNorthIdentity(client: AppNorthClient) {
    client.clearIdentityCredential()
}
```

`clearIdentityCredential()` clears only the credential. Your app must discard account-specific view models and clear or separate its own [read cache and offline storage](/docs/offline-and-cache#account-changes). Install the next identity before recreating screens. Do not persist credentials as long-lived sessions.

Secret rotation invalidates existing signatures and hosted-roadmap sessions. There is no per-person revocation list in this contract; stop issuing credentials to that account and use short expiry, or rotate the project secret when immediate project-wide revocation is needed.

## Verify {#verify}

1. Submit an identified action with a real backend-issued credential.
2. Change one signed field, then test an expired credential; both must fail.
3. Sign out and verify that another account cannot see the previous account's private report or chat.
4. Rotate the secret in a test project and confirm old credentials no longer work.
