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

# Quickstart

> A working <b>Sign in with Alive</b>. Your backend signs the request, the browser renders it, your server verifies the response offline.

<Note>
  **Maturity:** Limited preview. This flow works end to end today. Preview
  environments verify against development-ceremony keys, which carry no
  cryptographic soundness guarantee.
</Note>

Three pieces of code, in the order the data moves. Assumes you have finished
[Get access](/build/get-access).

<Accordion title="Diagram: the whole sign-in, end to end">
  ```mermaid theme={null}
  sequenceDiagram
      autonumber
      participant P as Your page
      participant RP as Your backend
      participant APP as Alive app
      participant DIR as Client directory

      P->>RP: POST /api/sign-in
      Note over RP: issue nonce and state,<br/>createSignInRequest,<br/>sign with the presenter key
      RP-->>P: request (compact JWS)
      Note over P: buildDeepLink, then render<br/>one static QR or a tap target

      APP->>DIR: resolve your client_id
      DIR-->>APP: display name, response endpoints,<br/>presenter keys
      Note over APP: verify the request signature,<br/>show the consent sheet,<br/>mint the login token

      alt Cross-device: QR scanned by another phone
          APP->>RP: POST your response endpoint
      else Same-device: deep link on this phone
          APP->>P: redirect, token in the URL fragment
          P->>RP: handOffToBackend
      end

      Note over RP: verifySelfIssuedToken<br/>runs locally. Array is not called.
      RP-->>P: verdict
  ```
</Accordion>

The two response paths are the part worth reading twice. A scanned QR means the
phone has no idea your page exists, so it posts to your registered endpoint
directly. A deep link on the same phone can come back through the page, which
is the only reason `receiveFragmentResponse` exists.

## 1. Backend: issue a nonce and sign the request

The request is a compact JSON Web Signature (JWS) signed with your registered
presenter key. The browser never holds that key, and the nonce must be issued
where its single-use burn happens.

```ts theme={null}
import { createSignInRequest } from "@vibechecklabs/array-identity-rp";
import {
  createPresentationRequestSigner,
  createNonceStore,
  createStateStore,
  DynamoDbNonceBackend,
} from "@vibechecklabs/array-identity-server";

const backend = new DynamoDbNonceBackend({ client: docClient, tableName: "rp-auth-nonce" });
const nonces = createNonceStore(backend);
const states = createStateStore(backend);
const signer = createPresentationRequestSigner({ privateJwk, kid: "rp-key-1" });

// POST /api/sign-in
const nonce = await nonces.issue();
const state = await states.issue(sessionId);

const claims = JSON.parse(createSignInRequest({
  clientId: "my-rp",
  nonce,
  responseEndpoint: "https://api.my-rp.com/auth/siop-response",
  scope: "arrayid",      // omit for pairwise
  state,
  idFacetProof: true,    // ask for a membership proof
}));

return { state, request: await signer.sign(claims) };
```

<Warning>
  Unsigned JSON is refused on the device (`unsigned_request`). A pairwise-only
  integration still needs a presenter key.
</Warning>

## 2. Browser: render it

Both delivery paths carry the same thing, a deep link holding the signed
request. Cross-device renders that link as a single static QR code. Same-device
navigates to it.

```ts theme={null}
import { buildDeepLink } from "@vibechecklabs/array-identity-rp";
import QRCode from "qrcode";

const { request } = await (await fetch("/api/sign-in", { method: "POST" })).json();
const link = buildDeepLink(request, "vibecheck");

// Cross-device: one static QR of the link, for the phone's camera.
await QRCode.toCanvas(canvas, link, { scale: 4, margin: 4, errorCorrectionLevel: "L" });

// Same-device: the same link as a tap target.
openInAppButton.href = link;
```

The QR encodes the **link**, not the bare payload. That is what lets the
phone's native camera open the app directly, with no in-app scanner and no
second app to install. A bare payload is a meaningless string to a camera.

The scheme is the installed build's: `vibecheck` for production,
`vibecheck-testnet`, or `vibecheck-dev`. It is a required argument, since any
default would be the wrong app for someone. The route is `array-siop` for both
sign-in and claim requests, and the app dispatches on the request's `typ`.

The scheme still reads `vibecheck` even though the app is called Alive. It is a
registered URL scheme rather than a product name, so it is what the installed
build actually answers to. Do not substitute `alive`.

<Tip>
  Three QR settings decide whether a code scans first time, and none is about
  capacity. Fix an **integer** `scale` rather than a target width, because a
  width makes the renderer derive a fractional scale and resample modules into a
  ragged 2px/3px mix. Use error-correction level **`L`**, since a code on a clean
  display is not defending against physical damage and the redundancy costs
  modules. Keep the full 4-module quiet zone inside the canvas rather than
  relying on the surrounding CSS padding.
</Tip>

On a same-device sign-in the app returns the user to your page with the token
in the URL fragment. Handle that on whichever page you registered as the
return target:

```ts theme={null}
import { receiveFragmentResponse, handOffToBackend }
  from "@vibechecklabs/array-identity-rp";

const response = receiveFragmentResponse();   // scrubs the fragment on receipt
if (response) await handOffToBackend("/auth/siop-response", response);
```

Cross-device sign-ins skip the browser. The phone posts to your response
endpoint directly, so there is nothing for the page to do.

## 3. Backend: verify

Both delivery paths arrive at the same endpoint, one as JSON and one as the
form post `handOffToBackend` sends.

### Where roots come from

A membership proof says the user is in the identity tree. On its own that
proves nothing, because anyone can build a tree and prove membership of it. So
the proof is checked against that tree's Merkle root, and the root has to be
one you accept.

Our Indexer signs each root with an operator key and publishes it on a feed.
You do not trust the feed. The provider polls it in the background and
re-verifies every entry against the operator key **you** pinned, so a tampering
relay can stall you but cannot forge for you. Start it once at boot, not per
request.

```ts theme={null}
import { DefaultRootsProvider, pinnedBootstrapKey }
  from "@vibechecklabs/array-identity-server";

const roots = new DefaultRootsProvider({
  feedUrl: process.env.ROOTS_FEED_URL,
  apiKey: process.env.ROOTS_FEED_API_KEY,        // needs the roots:read scope
  fetchImpl: fetch,
  pinnedOperatorKeys: [pinnedBootstrapKey(process.env.ROOTS_OPERATOR_BOOTSTRAP_PUBKEY)],
  expectedChainId: process.env.ROOTS_EXPECTED_CHAIN_ID,
  expectedContractAddress: process.env.ROOTS_EXPECTED_CONTRACT_ADDRESS,
});
await roots.start();
```

`roots.warm` is true once at least one entry has been fetched and verified.
Until then there is no root to judge a proof against. A fetch that succeeds but
returns nothing leaves the provider cold, which is the ordinary state on a
fresh feed deploy rather than an edge case.

If you only want authentication, drop `idFacetProof` from the request in step 1
and from the verify call below. You then need no feed, no pins and no provider,
and your ceiling is `self_certified`.

The constructor above is the minimum. Polling clamps, how freshness is decided,
and what the provider does with the keys the feed serves are in the
[RP server reference](/build/sdks/rp-server#the-roots-provider).

### Verify the token

```ts theme={null}
import { verifySelfIssuedToken, tierAtLeast } from "@vibechecklabs/array-identity-server";

// POST /auth/siop-response → { id_token, state? }
if (!roots.warm) {
  return reply.code(503).send({ code: "membership_verification_unavailable" });
}

const verdict = await verifySelfIssuedToken(idToken, {
  audience: "my-rp",                           // must equal the token aud exactly
  expectedMode: "disclosed",                   // matches scope: "arrayid" in step 1
  consumeNonce: (n) => nonces.consume(n),
  idFacetProof: {
    roots: { isAcceptableRoot: (root, ref) => roots.isSignedAndFresh(root, ref) },
    dedup: myTagDedupStore,
  },
});
// → { tier, sub, unique, mode }

if (!tierAtLeast(verdict.tier, "membership_proven")) throw new Error("too weak");
await states.consume(state, sessionId);
```

<Warning>
  Gate on `roots.warm` **before** calling the verifier. A cold roots provider
  throws rather than returning false, and the right answer is 503, not 401. A
  cold provider means you could not tell, whereas a 401 tells the device its
  credential is bad and spends the one retry it has. The Fastify
  `selfIssuedPreHandler` does not do this check for you, and by the time it runs
  the nonce is already consumed.
</Warning>

## Verdict tiers

| Tier | What the holder proved | `unique` |
| - | - | - |
| `key_possession` | They hold the private key the token is signed with. | no |
| `self_certified` | The subject derives from that key, plus a key-binding signature. | no |
| `membership_proven` | A verified ZK proof of membership in the identity tree, under a root you accept. | **yes** |

`membership_proven` is the only tier carrying a sybil guarantee. If your gate
cares that one human gets one of something, check for it.

## Subject modes

Set by the `scope` field in step 1.

| | `arrayid` (disclosed) | *(omitted)* (pairwise) |
| - | - | - |
| `sub` | the user's global ArrayID | a per-application pseudonym |
| Tier without a proof | `self_certified` | `key_possession` |
| Tier with a proof | `membership_proven` | `membership_proven` |
| What you learn | who they are | a real, unique member you cannot identify |

A pairwise subject derives from your `client_id`, so the same person signing
into two applications presents two unrelated subjects. Choose the mode on the
server, not in the page, and pass the same mode to `verifySelfIssuedToken` as
`expectedMode`. A token of the other mode is refused, which stops a pairwise
token from passing as a disclosed one.

## Next

The full exported surface is in the [RP](/build/sdks/rp) and
[RP server](/build/sdks/rp-server) references.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.