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

# Account sharing

> Add ShieldLabs identification to a login flow and allow one active device per demo account.

## What you will build

This tutorial starts with a working login form. You will identify each sign-in, read the result on the server and compare its **Device ID** with the account's active session. If a different device signs in, the demo asks whether to sign out the first one. The one-device rule belongs to this example application; ShieldLabs does not impose it on your accounts.

You need Node.js 22 or later. The `starter` app runs locally without keys. To try `final` with real identifications, have a hostname registered under **Integration > Domains**, its Public Key and Private API Key from **Integration > API keys**, and HTTPS routing that hostname to your Node server. A customer's key is not automatically accepted from `localhost`.

## Run the starting application

Clone the [public tutorial repository](https://github.com/ShieldLabs-ai/use-case-tutorials) and install only `account-sharing`:

```sh theme={null}
git clone https://github.com/ShieldLabs-ai/use-case-tutorials.git
cd use-case-tutorials
git switch starter
cd account-sharing
npm ci --omit=dev
cp .env.example .env
npm run dev
```

Open `http://127.0.0.1:3000` and sign in with `demo@example.com` / `demo-password`. The starter has a login form and SQLite sessions, but it does not identify devices. Sign in from another browser: there is no Device ID check. The app stores only disposable teaching data.

## Add the integration

Stop the starter server. Follow the changes in order below; the `final` branch contains their complete implementation. To see every line changed, run `git diff starter origin/final -- account-sharing` from the repository root. `origin/final` exists immediately after cloning, before you switch branches.

<Steps>
  <Step title="1. Configure the example">
    The `.env` copied from `starter` contains no ShieldLabs keys. Add these two lines to your private `account-sharing/.env` with the values for your registered hostname:

    ```dotenv theme={null}
    SHIELDLABS_PUBLIC_KEY=your-public-key
    SHIELDLABS_API_KEY=sec_your_private_api_key
    ```

    Keep the Private API Key on the server. In `final`, [`server/server.js`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/server/server.js) serves `/config.js` with only the Public Key and serves the installed JS SDK at `/vendor/shieldlabs.js`. [`public/index.html`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/public/index.html) loads those scripts before `index.js`.
  </Step>

  <Step title="2. Identify each login attempt in the browser">
    [`public/shieldlabs.js`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/public/shieldlabs.js) loads `@shieldlabs-ai/js`, starts a check when the form first receives focus and queues checks so they do not overlap. The submit handler in [`public/index.js`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/public/index.js) takes the resulting request ID:

    ```js theme={null}
    const identification = identifyOnFirstFocus(loginForm);
    const requestId = await identification.take();
    const data = await postJson('/api/login', {
      email, password, requestId, signOutOtherDevice
    });
    ```

    The browser sends the request ID, not a Risk Score or a Private API Key. The request ID is a reference to the result the server will retrieve.
  </Step>

  <Step title="3. Verify the result on the server">
    The login route passes `requestId` to [`server/accounts.js`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/server/accounts.js). That file calls `verifyIdentification(requestId)` from [`server/shieldlabs.js`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/server/shieldlabs.js):

    ```js theme={null}
    const check = await verifyIdentification(requestId);
    if (!check.ok) {
      return { success: false, message: `Sign-in refused: ${check.message}` };
    }
    const deviceId = check.identification.device_id;
    ```

    The server uses `@shieldlabs-ai/node` and the Private API Key to read History by that exact request ID. It refuses a missing, stale or reused identification, automation, unusable device data and Dangerous traffic before creating a session. A request ID returned to the browser does not by itself prove that the identification reached History.
  </Step>

  <Step title="4. Compare the active device">
    Once the password is correct, `server/accounts.js` reads the active session from SQLite. Its decision is based on the verified Device ID:

    ```js theme={null}
    if (active && active.device_id !== deviceId) {
      if (!signOutOtherDevice) {
        return {
          success: false,
          otherDevice: true,
          message: 'This account is signed in on another device. Sign out there first, or sign that device out.',
        };
      }
      endActiveSessions(account.email, 'signed_out_elsewhere');
    }
    ```

    [`server/db.js`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/server/db.js) stores the Device ID with each session and records used request IDs. The confirmation button in `public/index.js` sends `signOutOtherDevice: true`. A page that is already signed in checks `/api/session` and shows when another device ended its session. The signed-in account is sent to later checks as a hashed User HID; the raw email is not passed to the browser SDK.
  </Step>

  <Step title="5. Run the completed app">
    Switch to the completed version and reinstall that folder's dependencies:

    ```sh theme={null}
    cd ..
    git switch final
    cd account-sharing
    npm ci --omit=dev
    npm start
    ```

    Serve the Node process through HTTPS on the hostname registered for your keys. The process listens on `127.0.0.1:3000` behind your reverse proxy. Keep the `.env` private and check that `/config.js` exposes only the Public Key.
  </Step>
</Steps>

## Follow the integration code

The complete, runnable source is in the [account-sharing application](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/final/account-sharing). Compare the `starter` and `final` versions of these files:

1. [Browser helper](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/public/shieldlabs.js): loads the installed SDK, serializes checks and returns a request ID.
2. [Server verification](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/server/shieldlabs.js): reads real History, checks the matching ID, rereads delayed results and rejects unusable or replayed checks.
3. [Action routes](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-sharing/server/server.js): passes the action to this example's business modules. These modules store the decision in this app's own database.

The example's session table and policy are for learning. Adapt the action and account rules to your own product before using them for customer sign-ins.

## Try the completed application

1. On your registered HTTPS hostname, sign in with `demo@example.com` / `demo-password` on one device. Expect **Signed in as [demo@example.com](mailto:demo@example.com)**.
2. On a different device, use the same demo account. Check that its actual Device ID differs from the first device in History; clearing cookies alone is not evidence of a different device. The app should show **This account is signed in on another device** and offer **Sign out the other device and sign in here**.
3. Confirm. The second device becomes signed in. The first device should show that it was signed out after the next `/api/session` check.
4. If you want to repeat the demonstration, use **Reset demo DB** when `DEMO_ALLOW_RESET=1`. This resets this example's teaching state, not ShieldLabs History.

If a check is rate limited, stop and follow [the troubleshooting steps](/tutorials/overview#if-a-check-does-not-finish). Do not run parallel checks or change IPs to get around a limit.

## Check your result

Check one of your test attempts in History using its request ID. Confirm that the Device ID used for the first sign-in is the one stored for that session; for the second device, the ID must differ. Without a History row, the server should refuse the action. A reused request ID should not authorize another sign-in.

Run `npm run check` and `npm test` in `account-sharing` to check the source and the example's local decision rules. These commands use local test data and do not send another identification.

Allow more than a minute between real checks. A private window may still be identified as the same device; use the values you actually observe.

## Adapt it to your product

This example is a starting point, not a production-ready authorization system. Bind each action to an authenticated session where appropriate, authorize administration screens, persist state and atomically consume request IDs across all server instances. Review legitimate shared-device behavior before applying a device-based restriction. See [Acting on results](/guides/acting-on-risk-score).


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