> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aveid.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Register an Ave app, run the OAuth authorization code flow with PKCE, exchange the code, and create your app session.

<Note>
  Need auth before you register an app? Start with [Quick Ave](/guides/quick-ave). Come back here when you need refresh tokens, app branding, configured redirect URIs, or a confidential client.
</Note>

## Start with the embed flow

For most web apps, open Ave from your sign-in button and let the embed choose the best browser surface.

```ts theme={null}
import { startAveAuth } from "@ave-id/embed";

startAveAuth({
  clientId: "YOUR_CLIENT_ID",
  redirectUri: "https://yourapp.com/callback",
  onSuccess: ({ redirectUrl }) => {
    window.location.assign(redirectUrl);
  },
});
```

`startAveAuth()` opens the Ave sheet first, escalates to a popup when a top-level browser context is required, and falls back to a redirect when popups are blocked. Omit `clientId` to use Quick Ave.

## Prerequisites

<Steps>
  <Step title="Create an app in the developer portal">
    Go to [devs.aveid.net](https://devs.aveid.net) and create an OAuth app. You'll receive:

    * **Client ID** — used in all requests; safe to include in browser code
    * **Client secret** — server-side only; never put this in browser or mobile code

    <Tip>
      Building a SPA or any app that runs code in the browser? Use PKCE instead of a client secret. PKCE doesn't require a secret.
    </Tip>
  </Step>

  <Step title="Register your redirect URI">
    Add the exact callback URL where Ave should redirect after login (e.g. `https://yourapp.com/callback`). The URI must match exactly — no wildcards, no trailing-slash differences.

    For local development, enable development mode on the app to allow localhost, loopback, and Expo Go callback URLs without registering every port. Keep production callbacks registered explicitly.
  </Step>
</Steps>

## Login flow

<Tip>
  Using `@ave-id/sdk`? `startPkceLogin` and `finishPkceLogin` handle all of these steps automatically — PKCE generation, storage, state verification, code exchange, and token validation. The steps below show what's happening under the hood.
</Tip>

<Steps>
  <Step title="Generate PKCE parameters and redirect the user">
    Before redirecting, generate PKCE parameters. They prove that the same browser session that started login is the one completing it.

    ```ts theme={null}
    import { generateCodeVerifier, generateCodeChallenge, generateNonce } from "@ave-id/sdk";

    const codeVerifier = generateCodeVerifier();
    const codeChallenge = await generateCodeChallenge(codeVerifier);
    const state = crypto.randomUUID(); // CSRF protection
    const nonce = generateNonce();     // Replay protection for id_token

    // Store these — you need them when the user returns
    sessionStorage.setItem("ave_code_verifier", codeVerifier);
    sessionStorage.setItem("ave_state", state);
    sessionStorage.setItem("ave_nonce", nonce);

    const url = new URL("https://aveid.net/signin");
    url.searchParams.set("client_id", "YOUR_CLIENT_ID");
    url.searchParams.set("redirect_uri", "https://yourapp.com/callback");
    url.searchParams.set("scope", "openid profile email");
    url.searchParams.set("state", state);
    url.searchParams.set("nonce", nonce);
    url.searchParams.set("code_challenge", codeChallenge);
    url.searchParams.set("code_challenge_method", "S256");

    window.location.href = url.toString();
    ```

    <Note>
      `state` prevents CSRF attacks — without it, an attacker can trick your app into completing a login they started. `nonce` prevents replay attacks against the ID token. Both are random strings you generate and store temporarily.
    </Note>
  </Step>

  <Step title="Handle the callback">
    Ave redirects to your `redirect_uri` with `?code=AUTH_CODE&state=YOUR_STATE`. Validate `state` before doing anything else.

    ```ts theme={null}
    const params = new URLSearchParams(window.location.search);
    const code = params.get("code");
    const returnedState = params.get("state");

    const savedState = sessionStorage.getItem("ave_state");
    const codeVerifier = sessionStorage.getItem("ave_code_verifier");

    if (!returnedState || returnedState !== savedState) {
      throw new Error("State mismatch — possible CSRF attack");
    }

    // Clear immediately — these are single-use
    sessionStorage.removeItem("ave_state");
    sessionStorage.removeItem("ave_code_verifier");
    ```

    <Warning>
      The authorization code expires in seconds. Don't queue the exchange — call the token endpoint immediately after validating `state`.
    </Warning>
  </Step>

  <Step title="Exchange the code for tokens">
    <Tabs>
      <Tab title="PKCE (public client)">
        ```ts theme={null}
        import { exchangeCode } from "@ave-id/sdk";

        const tokens = await exchangeCode(
          { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
          { code, codeVerifier }
        );
        ```
      </Tab>

      <Tab title="Client secret (confidential client)">
        ```ts theme={null}
        // Server-side only — never expose clientSecret in browser code
        import { exchangeCodeServer } from "@ave-id/sdk/server";

        const tokens = await exchangeCodeServer(
          {
            clientId: "YOUR_CLIENT_ID",
            clientSecret: process.env.AVE_CLIENT_SECRET!,
            redirectUri: "https://yourapp.com/callback",
          },
          { code }
        );
        ```
      </Tab>

      <Tab title="Raw HTTP">
        ```bash theme={null}
        POST https://api.aveid.net/api/oauth/token
        Content-Type: application/json

        {
          "grant_type": "authorization_code",
          "code": "AUTH_CODE",
          "redirect_uri": "https://yourapp.com/callback",
          "client_id": "YOUR_CLIENT_ID",
          "code_verifier": "PKCE_VERIFIER"
        }
        ```

        <Note>
          Ave accepts both OAuth-standard `snake_case` fields and the legacy `camelCase` variants for backward compatibility. Prefer `grant_type`, `client_id`, `redirect_uri`, and `code_verifier` in new integrations.
        </Note>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Understand the token response">
    A successful exchange returns:

    ```json theme={null}
    {
      "access_token": "ave_at_...",
      "access_token_jwt": "eyJ...",
      "id_token": "eyJ...",
      "refresh_token": "rt_...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "scope": "openid profile email",
      "user": {
        "id": "identity-uuid",
        "handle": "alice",
        "displayName": "Alice Smith",
        "email": "alice@example.com",
        "avatarUrl": "https://..."
      }
    }
    ```

    Ave returns tokens with different purposes:

    | Token              | Format          | `aud` claim         | Use for                                         |
    | ------------------ | --------------- | ------------------- | ----------------------------------------------- |
    | `access_token`     | Opaque string   | —                   | Calling Ave's `/api/oauth/userinfo`             |
    | `access_token_jwt` | Signed JWT      | `https://aveid.net` | Ave APIs that accept JWTs, Connector delegation |
    | `id_token`         | Signed OIDC JWT | **Your client ID**  | User session, Convex auth                       |
    | `refresh_token`    | Opaque string   | —                   | Getting new tokens without re-login             |

    <Note>
      `id_token` and `access_token_jwt` are both signed JWTs with different audiences. The `id_token` audience is your `clientId`. The `access_token_jwt` audience is `https://aveid.net`. This matters for Convex and any library that validates JWT audience.

      `id_token` is only returned when you request the `openid` scope. `refresh_token` is only returned with `offline_access`.
    </Note>
  </Step>

  <Step title="Create your app session">
    Use `id_token` or the `user` object to create a session in your app:

    ```ts theme={null}
    import { verifyJwt } from "@ave-id/sdk";

    const { user, id_token, access_token, refresh_token } = tokens;
    const savedNonce = sessionStorage.getItem("ave_nonce");
    sessionStorage.removeItem("ave_nonce");

    // Option A: use the user object directly (simplest)
    // user.id is the identity UUID — use as your stable user key

    // Option B: validate id_token for higher assurance
    const claims = await verifyJwt(id_token, {
      audience: "YOUR_CLIENT_ID",
      nonce: savedNonce,
    });
    if (!claims) throw new Error("Invalid id_token");
    // claims.sub, claims.email, claims.name, etc.
    ```

    Store `refresh_token` securely, such as in an HTTP-only cookie or server-side session, if you requested `offline_access`. Never put it in `localStorage`.
  </Step>
</Steps>

## Refresh tokens

When the access token expires, exchange the refresh token for a new set without re-authenticating the user.

```ts theme={null}
import { refreshToken } from "@ave-id/sdk";

const newTokens = await refreshToken(
  { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
  { refreshToken: storedRefreshToken }
);
```

<Warning>
  Refresh tokens are **rotated on every use** — each successful refresh returns a new refresh token and invalidates the previous one. Store the new token immediately after every refresh. Reusing an old refresh token triggers `invalid_grant` and may revoke related tokens as a security measure.
</Warning>

## Error reference

<AccordionGroup>
  <Accordion title="invalid_client">
    Client ID or client secret doesn't match any registered app.
  </Accordion>

  <Accordion title="invalid_grant">
    The code or refresh token is bad, expired, or was already used. For refresh tokens, this may also mean reuse detection triggered.
  </Accordion>

  <Accordion title="invalid_request">
    A required field is missing. Common cause: forgetting `codeVerifier` when PKCE is required.
  </Accordion>

  <Accordion title="invalid_scope">
    A requested scope isn't allowed for this app. Check your app's scope allowlist in the developer portal.
  </Accordion>

  <Accordion title="invalid_target">
    The Connector `requestedResource` key doesn't exist or isn't active.
  </Accordion>

  <Accordion title="access_denied">
    The Connector grant is missing or was revoked. Re-run the Connector authorization flow.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Token details" href="/guides/oauth-authorization-code-flow">
    JWT payload reference for `id_token` and `access_token_jwt`, validation steps, and refresh token rotation.
  </Card>

  <Card title="PKCE deep dive" href="/guides/pkce-for-public-clients">
    PKCE security model, browser storage guidance, and anti-patterns to avoid.
  </Card>

  <Card title="Convex auth" href="/guides/convex-custom-auth">
    Exactly how to wire Ave tokens into Convex, including which token to use and why.
  </Card>

  <Card title="Scopes and claims" href="/guides/scopes-and-claims">
    Complete scope catalog with the exact JWT claims each scope adds.
  </Card>
</CardGroup>
