Skip to Content
Autional logo

Turn your app into a multi-user product

Paste one prompt into your AI coding agent. It fetches the onboarding skill — a guided procedure for adding Autional login, accounts, and sessions to your existing project. Falls back to an own-CDN mirror if the site is unreachable.

Step 1 — Copy this prompt

Fetch https://www.autional.com/ai/skill.md and follow it to add Autional login to this project. If unreachable, use:
https://cdn.autional.com/ai/latest/SKILL.md
Works in Claude Code, Cursor, OpenCode, or any agent with web access.
Prefer Chinese? Use this prompt instead
请读取 https://www.autional.com/ai/skill.zh.md,并据此为当前项目接入 Autional 登录。
若打不开,改用国内镜像:
https://cdn.autional.com/ai/latest/SKILL.zh.md

Step 2

Paste it into your agent

Run it in the project you want to upgrade. The agent reads the skill and works through it phase by phase, confirming with you at each gate.

Step 3

Follow the confirmation gates

The agent inspects your app first, then proposes the plan — you approve before anything is changed. Everything lands on a separate git branch.

What the agent will do

0

Safety net first

Git branch, toolkit check, risk disclosure — every change is reversible.

1

Inspect your app

Framework, existing auth, conflicts, and what the integration will change.

2

Design the policy

Tenant naming, compliance-driven password policy, login methods.

3

Provision identity

Tenant + application created in the developer portal; credentials handled once.

4

Install the SDK

Framework-appropriate packages and environment variables.

5

Wire it in

Provider, login UI, external-id mapping, user center — real code, then verify the build.

6-7

Test & verify

Unit tests plus an 8-step end-to-end flow: register → login → profile → refresh → logout → revoked.

8

Hand over

Change inventory, test accounts, and what to check in the portals.

What you need

  • •An existing project — React, Vue, Next.js, Node, or similar
  • •An AI coding agent with web access
  • •A git repository (recommended — enables safe rollback)
  • •An Autional account for the developer portal (created during onboarding)

What "done" looks like

  • ✓Sign-up, sign-in, and sign-out work end-to-end
  • ✓Session survives page reload; tokens refresh silently
  • ✓Protected routes require login before rendering
  • ✓Existing app data is still there after accounts are added
  • ✓Build passes and unit tests are green
  • ✓End-to-end flow verified: discovery → register → login → profile → refresh → logout → revoked token rejected
  • ✓AUTIONAL_CHANGES.md lists every file that changed
  • ✓Test accounts created for multi-user verification

If your agent can't browse the web

Option A — Paste the full skill text

Expand, copy everything, and paste it into your agent together with the instruction "follow this and add Autional login to my project." The text below is byte-identical to/ai/skill.md(the own-CDN mirror's same-named file is byte-identical as well).

Show full skill text (42 KB)
---
name: autional-com-onboarding
description: "Add Autional multi-user identity to an existing app — guided onboarding that turns a single-player app into a multi-user product: OIDC login, accounts, sessions, token management, test accounts and verification. Supports React, Vue, Next.js and Node."
region: com
lang: en
canonical: https://www.autional.com/ai/skill.md
generated: core@952f0b50f0bc
---

<!-- Generated by skills/_gen/build-skills.mjs from skills/_core — do not edit directly. -->
# Autional Onboarding — add multi-user identity to your app

> **Region:** `.com` · **Language:** English
> **Issuer:** https://api.autional.com
> **API:** https://api.autional.com/bff
> **Portals:** https://admin.autional.com · https://user.autional.com · https://security.autional.com · https://developer.autional.com · https://status.autional.com
> **Tenant links:** Admin/User/Security portals are tenant-scoped — append `/{TENANT_SLUG}/` after the host (see §8.2). Developer/Status are global sites.
> **Reference files (read them, absolute URLs):**
> - https://www.autional.com/ai/references/compliance-matrix.md
> - https://www.autional.com/ai/references/config-template.md
> **Canonical source of this file:** https://www.autional.com/ai/skill.md
> **Integrity:** https://www.autional.com/ai/skill.md.sha256

## About this skill

Autional gives applications an identity layer. This skill turns a single-player app (or an app with no accounts) into a multi-user product: login, registration, profiles, sessions, token lifecycle.

You are an AI agent executing this procedure inside the user's project. Work in the user's conversation language. Follow every phase in order; each phase ends with a user confirmation gate.

**What the user gets**

- Email + password login (and optional OAuth / SMS / magic-link) powered by Autional OIDC
- Registration, password reset, profile, device/session management
- Automatic token refresh with multi-tab sync
- Audit trail, CAPTCHA, breach checks, compliance-driven password policy
- Multiple users on one backend — the multiplayer layer for an AI-generated app

**Boundaries**

- This file contains **zero secrets** — every endpoint in it is a public URL.
- Only fetch related files from this file's own canonical domain (`.com`). Never follow links to other regions or third-party mirrors except the fallback mirror listed below.

> **Fallback mirror:** if `www.autional.com` is unreachable, fetch the same file from Autional CDN:
> https://cdn.autional.com/ai/latest/SKILL.md

## Hard rules — do not skip

**Phase order is mandatory.** Skipping a phase leaves the user with a broken integration.

1. Execute Phases 0 → 8 in order. Before each phase, announce "Phase N starting". After each phase, show results and wait for the user's confirmation before continuing.
2. On a code error: `git checkout -- <file>` to restore that file, fix, retry. If a whole phase fails: restore every file you changed in it, restart the phase.
3. All changes happen on an isolated branch (`feature/autional`). Detect the repo's default branch first — `git symbolic-ref --short refs/remotes/origin/HEAD`, fallback `main`, then `master` — and branch from it. No git? Fall back to per-file backups (`cp file file.autional-backup`).
4. Never run `git reset --hard`. Restore files individually with `git checkout -- <file>`.
5. Dirty working tree? **Stop and ask the user.** Never stash or commit their work silently. Options: [A] the user commits or stashes it (recommended); [B] you run `git stash --include-untracked` only after explicit consent, and restore it with `git stash pop` at the end (hand conflicts to the user); [C] the user handles it another way (another clone/worktree, …) and tells you when to proceed.
6. Leave a checkpoint commit at every phase boundary (`git add -A && git commit -m "checkpoint: Phase N"`), so every finished phase can be returned to.
7. **Never merge into the default branch and never `git push` without the user's explicit choice at the Phase 8 gate.** Until then the default branch stays exactly as it was before the integration.
8. Never read the existing contents of the user's `.env` — only append your own keys. Never read or dump the user's user-table data.
9. The admin password is displayed **once**. Never write it into a code or config file.
10. All injected code is MIT-compatible; do not introduce GPL dependencies. Do not remove or alter the user's existing license.
11. After changes, `npm run build` (or the project's build command) must pass; on failure, restore the changed files immediately.
12. Write `AUTIONAL_CHANGES.md` recording every file/package you changed.
13. Retry without a hard limit on tenant-name collisions and test failures — except network unreachability and invalid credentials, which must surface to the user instead of silently retrying.
14. Degrade transparently: if your environment cannot run a command, print the exact command for the user and continue with what you can; if the user asks for plan-only, output the plan and commands and execute nothing.

## Phase 0 · Preparation (automatic)

### 0.1 Toolkit check

Require: `node` ≥ 18, a package manager (`npm`/`pnpm`/`yarn` — detect from lockfiles), `git`, `curl`.

### 0.2 Git detection + branch isolation

```
1. No git? → fall back to file-backup mode (cp file file.autional-backup before each change).
2. Not a repo? → git init && echo "node_modules/" > .gitignore
3. Missing identity? → git config user.name "Autional Onboarding" / user.email "onboarding@localhost"
4. Default branch: git symbolic-ref --short refs/remotes/origin/HEAD
   (no remote or no ref → try main, then master)
5. Working tree clean? → go to step 6.
   Dirty? → STOP and ask the user. Never stash silently. Options:
     [A] user commits or stashes their work now (recommended)
     [B] you run git stash --include-untracked — only after explicit consent;
         you must restore it with git stash pop at the end
     [C] user handles it their own way (another clone/worktree, …) — wait for their go-ahead
6. Isolate:  git checkout -b feature/autional <default>  (branch exists → git checkout feature/autional)
7. Checkpoint at every phase boundary: git add -A && git commit -m "checkpoint: Phase N"
8. Recovery paths:
   one file  → git checkout -- <file>
   one phase → git checkout -- <all files changed in this phase>
   abandon   → restore every file changed in the current phase (git checkout -- <files>),
               git checkout <default>,
               keep or delete feature/autional (user's call),
               then git stash pop if and only if step 5 [B] stashed user work
```

### 0.3 Risk disclosure (mandatory)

```
This procedure will modify project files (list shown in Phase 5.0).
All changes happen on an isolated branch; the default branch stays untouched
until you explicitly approve a merge at the end.
Your existing work is handled first (0.2 step 5) — nothing is stashed or committed without your consent.
You can say "abandon" at any time and the project is restored.
Continue? [Continue] [Cancel]
```

## Phase 1 · System analysis (read-only)

Scan the project. **Do not modify anything in this phase.**

### 1.1 Framework detection

Read `package.json`:

| Dependency | Framework |
|---|---|
| `react` / `react-dom` | React |
| `vue` | Vue 3 |
| `next` | Next.js (React) |
| `@angular/core` | Angular |
| none of the above | ask the user |

Show the result and let the user confirm ("Detected React 18 + Vite. Correct? [Yes] [No, it's Vue]").

### 1.2 Existing user-system detection

| Signature | Meaning |
|---|---|
| `users` table/model with `email`/`password` | Own user system |
| `Login.tsx` / `Login.vue` / signin page | Own login UI |
| `bcrypt` / `argon2` / `crypto` imports | Own password hashing |
| `jwt` / `token` / `session` code | Own auth |
| `middleware` / `auth` / `guard` route protection | Own guards |

Report what exists. Autional takes over: password verification, token issuance/refresh/revocation, session management. The user keeps: their own user table's business fields (name, role, avatar) and business logic.

### 1.3 Conflict detection

| Conflict | Example | Recommendation |
|---|---|---|
| Route clash | your `/api/login` vs Autional endpoints | SDK replaces page logic; your API routes stay |
| Field clash | your `password_hash` column vs Autional-managed passwords | keep the table, drop the auth columns after migration |
| Middleware clash | your auth middleware vs the SDK guard | SDK guard wins (gets auto-refresh + tab sync) |
| Cookie clash | your `auth_token` vs SDK cookie | use the SDK's cookie |

Flag each conflict with options: [A] replace (recommended) [B] coexist [C] postpone.

### 1.4 Gains and impact

```
Replacing hand-rolled auth with Autional:
  ~500 lines of auth logic → one provider import
  plaintext password handling → hashed transmission (SHA-256 / symmetric)
  manual token refresh → automatic refresh with concurrent de-dup
  no CAPTCHA → progressive PoW
  no audit trail → full event tracking
  single user → multi-user + RBAC

Impact:
  New users are created in Autional. Existing users need a one-time link (Phase 5.3).
  The existing login page is replaced (option-based, Phase 5.2).
  The app must be able to reach the Autional endpoints above.
```

### 1.5 Bundle-size impact

```
core + framework package adds ≈ 15 KB (≈ 5 KB gzipped).
First load: +≈ 50 ms (OIDC discovery, cached afterwards). Token refresh only on 401.
Identical weight to the auth code it replaces — plus security updates you no longer maintain.
```

### 1.6 App-data isolation check (read-only)

Login is only half of "multi-user". Check where the app keeps its **application data** (notes, todos, documents…) and whether that data is isolated per account:

```
1. Locate the data layer: localStorage / IndexedDB / SQLite / a backend API.
2. Are storage keys scoped per user?  "notes" (shared) vs "notes:<userId>" (isolated).
3. Are backend rows scoped per user?  a user_id / owner_id column, or none?
4. Once login works (Phase 7): log out, log in as another account — does the data change?
   Then repeat without a logout: end the session another way (let the token expire or
   revoke it, clear the stored session), log in as another account — does the previous
   account's data still show?
```

Report honestly:

```
Data isolation: ISOLATED / NOT ISOLATED / NOT APPLICABLE (no app data yet)
Where: <storage keys or tables>
What another login sees after a logout: <a fresh account's empty state | the same shared data>
What another login sees after a no-logout session end: <previous account's residual data | clean state>
```

If NOT isolated, present options — **do not touch the data layer on your own; any change happens only after the user explicitly approves it**:

```
[A] Namespace the existing storage per user id (small; local-first apps)
[B] Move app data behind a backend with per-user scoping (larger; enables multi-device)
[C] Keep as-is for now (e.g. single-machine demo) — record the finding instead
```

**Account-switch guard (local-first apps).** A session does not always end with a logout — token expiry, a platform-side kill, or a cleared cookie ends it too. If local data is only cleared at logout, the previous account's residue survives into the next account: it merges into that account's view, and the next sync pushes it into their cloud state. Guard the switch: persist the last signed-in user id; when a session starts, compare it with the current user id — mismatch (including "no marker yet") → clear the local user data and full-reload BEFORE any pull / merge / push; same id → leave data untouched. Reuse the logout's clear list so the two paths cannot drift apart.

Record the finding and the user's decision in `AUTIONAL_CHANGES.md`. An approved change is extra scope: list it in the Phase 5.0 preview and keep it on the same branch.

## Phase 2 · Strategy recommendation (options + confirmation)

### 2.1 Tenant naming

Auto-derive from the project name (`my-blog-app` → `my-blog`). Rules: 3–64 chars, letters/digits/hyphens, not all digits. Check availability; on collision propose variants (`my-blog-2024`, `my-blog-app`, `myblog-hq`) and retry without limit.

### 2.2 Security policy (compliance-driven)

Match the user's scenario against the full matrix in `https://www.autional.com/ai/references/compliance-matrix.md` (NIST SP 800-63B AAL1–3, PCI DSS v4.0, GDPR, HIPAA). Present options with a recommendation:

```
[A] NIST baseline (recommended for most SaaS) — hash transmission, min 8, upper+lower+digit
[B] Basic (internal tools) — min 6
[C] High security (+MFA mandatory, breached-password check)
[D] Custom
```

### 2.3 Login methods

Recommend from the user's audience: password (default), magic link, OAuth, SMS code. Enterprise → password + SSO; consumer → password + OAuth + SMS.

After the auth policy is written, re-open the login page and confirm the intended inputs render. Zero input fields = the tenant's `login_methods` is the dead end (a declared method with no matching provider); fix it via the tenant auth policy (`PUT /admin/tenants/:id/auth-policy` — `login_methods` is a JSON string), then read the login page again to verify. Do not trust a 200 from identity `PUT /admin/security/auth-config` — it can succeed silently without changing anything.

## Phase 3 · Tenant + app credentials

### 3.1 Choose the path

Ask the user:

```
[A] Trial tenant — fastest, no invitation code (recommended to start)
[B] Invitation code — permanent tenant
[C] I already have a tenant — skip registration, reuse existing appId/issuer (see "Existing tenant" edge case)
```

For [A]/[B]: if the installed SDK client exposes `createTrialTenant(...)` / an invitation-based registration call, drive it programmatically (it creates the tenant and the admin user). If the installed version does not expose it, open the developer portal (https://developer.autional.com) and walk the user through the same flow in the browser. Do not invent API calls that are not exposed by the installed SDK or documented by the portal.

### 3.1a Post-registration self-check (mandatory)

Right after trial registration returns, log in once as the admin you just registered:

```
createTrialTenant(...) → 201 { tenantId, slug, status }
→ login({ email: adminEmail, password: adminPassword, tenantId })
```

- **Login succeeds** → continue to 3.2. Note: the auth-config `portal_oauth_client_id` (`GET /identity/api/v1/public/auth-config/{tenantId}`; same value as the legacy `oauth_client_id`) is the tenant **portal** client (`kind=portal`) — it backs the portal SPA sign-in and is **NOT** your app's `appId`. Your app's `appId` is the `client_id` issued in 5.2b when you register the app.
- **Registration returns 5xx `trial provisioning failed`** → the tenant was **compensated (soft-deleted)** because admin/RBAC provisioning failed; nothing is half-created. Retry once with the same name; if it fails again, stop and report `request_id` + the failing step.
- **Login fails 401 `invalid credentials`** (older servers, or a manual portal flow) → the tenant exists but its admin was NOT provisioned. Do **not** re-submit the registration (it burns the per-IP trial quota and returns 409). Tell the user, give them the tenantId/slug, and use a fallback path: invitation-code tenant, or the developer portal. Report `request_id` and the tenantId.

### 3.2 Credential handling

```
- Admin password: shown ONCE. Tell the user to save it in their password manager now. Never write it to code or config files.
- tenantId / issuer / api URL / tenant slug: public configuration — write to .env (public block) and src/autional.ts. The slug drives tenant-scoped portal links (see 8.2).
- `appId` = the `client_id` of **your app's own client**, issued in 5.2b. Never the auth-config `portal_oauth_client_id` (legacy alias: `oauth_client_id`) — that one is the tenant portal client.
```

### 3.3 Test accounts

Offer to create 3 test accounts (`player1`…`player3` by default, named to the user's domain) with auto-generated passwords satisfying the Phase 2 policy:
- [A] create 3 (recommended) — via the SDK register call
- [B] skip — later via the admin portal

Record the test accounts in `AUTIONAL_SETUP.md` (passwords shown once).

## Phase 4 · Dependencies

### 4.1 Install (match the framework)

```bash
# React
npm install @autional/core @autional/react @autional/api-identity

# Vue
npm install @autional/core @autional/vue @autional/api-identity

# Next.js
npm install @autional/core @autional/react @autional/next @autional/api-identity

# Node backend
npm install @autional/core @autional/node @autional/api-identity
```

> `@autional/core` must be **>= 0.3.8**. Versions 0.3.0–0.3.7 shipped a wrong `createTrialTenant()` path (public trial registration answered 401/`40000502` from the gateway) and are deprecated on npm. Verify with `npm ls @autional/core`; upgrade with `npm install @autional/core@^0.3.8`.

Confirm before installing: "Will install Autional SDK packages (≈ 50 KB, ≈ 15 KB gzipped). Continue? [yes] [no]". Use the package manager detected in Phase 0 (pnpm/yarn → equivalent `add` commands).

### 4.2 Configuration injection

Detect the env-file location and the framework's public env prefix:

| Framework | Prefix |
|---|---|
| Vite (React/Vue) | `VITE_` |
| Next.js | `NEXT_PUBLIC_` |
| Create React App | `REACT_APP_` |
| Node | plain (`AUTIONAL_`) |

Append to the public env file (never touch other keys):

```env
AUTIONAL_APP_ID=<appId from 5.2b — do NOT prefill with auth-config's portal_oauth_client_id (legacy oauth_client_id)>
AUTIONAL_ISSUER=https://api.autional.com
AUTIONAL_API_URL=https://api.autional.com/bff
```

Then generate `src/autional.ts` from the template in `https://www.autional.com/ai/references/config-template.md` with the real `appId` / `issuer` / `apiUrl` filled in.

## Phase 5 · Code integration (option-based)

### 5.0 Risk disclosure + change preview (mandatory)

Before modifying code, show the file list:

```
⚠️ Confirm before changes:

Will be added:
  + src/autional.ts — SDK config
  + AUTIONAL_CHANGES.md — change log
Will be modified:
  ~ <app entry> — provider wrapping (e.g. src/App.tsx, src/main.ts)
  ~ <login page> — login replacement
  ~ .env — append public config
Will NOT be touched:
  ✓ your business logic
  ✓ your user-table data
  ✓ existing .env contents

Continue? [Continue] [Cancel]
```

Wait for an explicit [Continue] before touching files.

### 5.1 Provider wrapping

```tsx
import { AutionalProvider, RequireAuth } from '@autional/react';

<AutionalProvider config={autionalConfig}>
  <Router>
    <Route path="/login" element={<Login />} />
    <Route path="/dashboard" element={<RequireAuth><Dashboard /></RequireAuth>} />
  </Router>
</AutionalProvider>
```

> **API surface note:** the exports above are this procedure's target API surface. If the installed package version exports different names, read the package's `.d.ts`/README and wire the equivalent provider/hook/guard — keep the same structure (provider wraps the app, guard protects routes). Vue: `createAutional(config)` + install; Next.js: provider in the app root + middleware for protected routes.

### 5.2 Login page

Detect the existing login page, then:

```
[A] Replace with Autional login (recommended) — hashed transmission + CAPTCHA + full token management
[B] Keep yours, add Autional login beside it — gradual migration
[C] Keep yours, only add the route guard — migrate later
```

Label each option with invasiveness / gain / rollback difficulty.

### 5.2b · SSO login — "Continue with Autional"

The app shows one "Continue with Autional" button; the login form and all social providers live on the Autional auth page. Use it when several apps must share one user pool, or when the app should not implement password/social login itself.

1. **Register the app** (admin console https://admin.autional.com/{TENANT_SLUG}/oauth-clients): note the `client_id` (this is the SDK `appId`); set the app's public callback URL as `redirect_uri` (e.g. `https://app.example.com/oauth/callback`); add the app's origin to the client's `cors_origins` (e.g. `https://app.example.com`). **CORS registration is mandatory** — without it the browser blocks the token exchange. Also fill in the app's **website** (public https URL) and optional **short alias / logo**: the end-user portal renders the "Authorized apps" tile from them — **without a website the tile is display-only (not clickable)**.
   - This `client_id` **IS** the SDK `appId`: write it to `.env` (`AUTIONAL_APP_ID`) and `src/autional.ts`, **overwriting** any value taken earlier.
   - `redirect_uris` must match the callback **character for character** (the SDK default is `<origin>/oauth/callback` — no trailing slash). A trailing slash turns the entry into a path-prefix rule; a mismatch fails at login with `invalid_request: redirect_uri not registered`.
2. **Login button**:
   ```ts
   await autional.loginWithOAuth({});   // client_id = appId; PKCE (S256) + state handled by the SDK
   ```
   Optional: `redirectUri`, `scope`, `extraParams` (e.g. `{ prompt: 'consent' }`), `authorizeUrl` override.
   **Same-origin proxy forms must pin `authorizeUrl`.** The default authorize URL only rewrites an `api.` host into `auth.` (e.g. `api.autional.com` → `auth.autional.com`); any other host — a same-origin `/bff` proxy, a custom gateway — carries through unchanged and lands on the API plane, which serves no login UI. Pin the auth plane explicitly:
   ```ts
   await autional.loginWithOAuth({
     authorizeUrl: 'https://auth.autional.com/oauth/api/v1/oauth/authorize',
   });
   ```
3. **Callback route** at the same path as `redirect_uri`:
   ```ts
   const { user } = await autional.handleOAuthCallback(window.location.href);
   ```
   The SDK verifies `state`, exchanges the code with the stored PKCE verifier (no client_secret — public client), fetches userinfo, and stores the session in SSO mode (token refresh then goes to the OAuth refresh endpoint automatically).
4. **Preflight the client before wiring the callback** — one request proves the client and the callback are registered:
   ```bash
   export APP_ID=<client_id from step 1>
   export CB=https://app.example.com/oauth/callback
   curl -sS -o /dev/null -w "%{http_code} %{redirect_url}\n" -H "Accept: application/json" \
    "https://api.autional.com/oauth/api/v1/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=$(node -p 'encodeURIComponent(process.env.CB)')&scope=openid&state=preflight&code_challenge=mR_nE7EeJopQmji77eZUdD4_rLzw5A5HdlRSiLQQCzU&code_challenge_method=S256"
   ```
   PASS = `302` **and** `redirect_url` starts with your callback **and** contains `error=login_required`. FAIL = `400` + `redirect_uri not registered` (wrong `client_id` — usually the portal client — or a callback that is not byte-identical). A `200` means you hit the auth-site SPA shell instead of the API — that is **not** proof.
   `https://api.autional.com` is the **API** plane (e.g. `https://api.autional.com`); `/oauth/...` and `/bff/oauth/...` are both reachable. Do not swap the host for the `auth.` plane — that path is rewritten to the SPA and returns HTML.
   Skipping this step is how a build "succeeds" and the first real login fails.
5. **Preflight CORS before touching the browser** — the token exchange, userinfo and refresh calls are cross-origin, so the browser sends a preflight first. One request proves the origin is registered **and** already live:
   ```bash
   ORIGIN=https://app.example.com
   curl -sS -o /dev/null -D - -X OPTIONS "https://api.autional.com/oauth/api/v1/oauth/token" \
     -H "Origin: $ORIGIN" -H 'Access-Control-Request-Method: POST' \
     -H 'Access-Control-Request-Headers: content-type' | grep -i 'HTTP/\|access-control-allow-origin'
   ```
   PASS = `204` **and** `Access-Control-Allow-Origin: $ORIGIN` (exactly one such header).
   No header means one of three things: the origin is not on **this client's** `cors_origins`; the registered value is not byte-identical to the browser Origin (`https://host[:port]` — no path, no trailing slash); or the registration is newer than the propagation window (the API caches the registered origin set for **≤ 60 s** — wait a minute and retry).
   A failing preflight is a hard stop: the browser blocks the token exchange, so the first real login fails even though the build passes. The `/bff/oauth/...` form of the token endpoint is the same CORS decision.

Social providers (Google / WeChat / …) are configured once on the Autional side; all apps share the same authorize entry.

### 5.2c · Logout — end the SSO session, not just the local copy

A local-only logout is a half logout. Clearing the app's own tokens leaves the Autional session alive: the next "Continue with Autional" click signs the same user straight back in — and switching accounts takes an incognito window. OIDC standardizes the real thing as RP-Initiated Logout; do not invent a private logout endpoint. The flow is two steps, in this order:

1. **Clear the local session** — `await autional.logout()`: memory + persisted tokens, plus any cookie your app mirrors the token into.
2. **Full-page redirect to the OP's `end_session_endpoint`** — a browser navigation, not an XHR:

   ```
   GET https://api.autional.com/oauth/api/v1/oauth/logout
       ?id_token_hint=<id_token>              (Recommended — omit when expired)
       &client_id=<client_id>
       &post_logout_redirect_uri=<app URL>    (must be pre-registered — 5.2b step 1)
       &state=<random string>
   ```

   | Parameter | Value | Why it matters |
   |---|---|---|
   | `id_token_hint` | the `id_token` from the login response | Lets the OP verify — and end — the right session. Omitting it is allowed; an **expired** token must be omitted — the OP rejects it (400). |
   | `client_id` | your SDK `appId` | The OP resolves the client for the redirect whitelist. |
   | `post_logout_redirect_uri` | where the user lands afterwards | Must be registered on the client; an unregistered URL is ignored (no open redirect). |
   | `state` | random string | Echoed back on the landing URL — the same CSRF pattern as login. |

When the session is in SSO mode and the discovery document announces an `end_session_endpoint`, `autional.logout()` performs both steps for you (optional `postLogoutRedirectUri` / `state`; defaults: the app origin and a generated value). Before redirecting, it also revokes the current access/refresh tokens at the discovery `revocation_endpoint` (RFC 7009) — time-boxed at 1.5 s, so an unreachable endpoint never blocks the logout. If the endpoint is not available, logout falls back to clearing locally only.

Apps whose stored session may not survive a reload (for example the novel demo: an SSR proxy that re-seeds localStorage from a cookie on every load) should not rely on step 2 happening automatically — build the URL with `buildEndSessionUrl(endpoint, params)` (or `URLSearchParams`) and redirect unconditionally after the local clear:

```ts
const logoutUrl = `https://api.autional.com/oauth/api/v1/oauth/logout?` + new URLSearchParams({
  ...(idToken && !expired(idToken) ? { id_token_hint: idToken } : {}),
  client_id: appId,
  post_logout_redirect_uri: window.location.origin + '/',
  state: crypto.randomUUID(),
});
await autional.logout();            // step 1 — clear local + cookie mirror
window.location.assign(logoutUrl);  // step 2 — always
```

What the user sees: with a valid `id_token_hint` the OP revokes the session and redirects to `post_logout_redirect_uri` (with `state` appended) — provided that URI is registered, otherwise it returns a plain `logged_out` response. Without a hint in a browser navigation, the OP shows a logout **confirmation page** first, then redirects. A machine caller without a hint and without a session gets 401.

> **No page transition is possible.** The endpoint lives on the API host (`https://api.autional.com`), so logout always leaves your app for another origin — the browser shows its own loading state. Give local feedback before navigating (disable the button, set a pending label, fade the shell): that is all the UI can do.

> **Multi-tab:** with `syncTabs` on (the default), logout and token refresh broadcast across tabs. A refresh in one tab makes the others re-read storage instead of reusing a stale refresh token — the server treats reuse as compromise and revokes the whole token family. Keep the default.

### 5.3 Existing user table

```
[A] Keep the table, add external_id linking to the Autional user id (recommended — no data loss)
[B] Drop the table, use Autional only (data-loss risk)
[C] Dual-write during a transition window
```

Migration template (option A, PostgreSQL):

```sql
-- 1. Add the linking column
ALTER TABLE users ADD COLUMN external_id VARCHAR(26);
CREATE UNIQUE INDEX idx_users_external_id ON users(external_id);

-- 2. Link existing users (after their accounts exist in Autional)
-- UPDATE users SET external_id = '01KW...' WHERE email = 'user@example.com';

-- 3. Optional: drop auth columns once migration is verified
-- ALTER TABLE users DROP COLUMN password_hash;
-- ALTER TABLE users DROP COLUMN reset_token;
```

```ts
async function findOrCreateLocalUser(autionalUser: { id: string; email: string }) {
  let local = await db.user.findFirst({ where: { externalId: autionalUser.id } });
  if (!local) {
    local = await db.user.findFirst({ where: { email: autionalUser.email } });
    if (local) {
      await db.user.update({ where: { id: local.id }, data: { externalId: autionalUser.id } });
    } else {
      local = await db.user.create({ data: { email: autionalUser.email, externalId: autionalUser.id } });
    }
  }
  return local;
}
```

### 5.4 User center

After login, users manage their own account:

| Feature | SDK call | Page |
|---|---|---|
| Change password | `changePassword()` | /settings/password |
| Profile | `getProfile()` / `updateProfile()` | /settings/profile |
| Forgot password | `forgotPassword()` / `resetPassword()` | /forgot-password |
| Devices/sessions | `getSessions()` / `revokeSession()` / `revokeOtherSessions()` | /settings/sessions |
| Delete account | `deleteAccount()` | /settings/delete |

> Sensitive operations (`deleteAccount()`) run step-up re-authentication internally (`X-StepUp-Token`; call `reauthenticate()` directly for custom flows); these endpoints currently support `plain` / `hash` transmission modes only.

### 5.5 Build verification (mandatory)

```
npm run build (or the project's build command)
  → pass ✅ → Phase 6
  → fail ❌ → checkpoint commit → analyze → fix → retry
  → 3 consecutive failures → git checkout -- <changed files> → analyze → fix → retry
If the project has `npm test`, run it; fix only failures caused by this integration.
```

## Phase 6 · Unit tests

| Module | Cases | Verify |
|---|---|---|
| TokenManager | store / expire / decode | write → read → expire → null |
| AuthClient | login / register / logout | mock HTTP → correct results |
| ApiClient | GET/POST + 401 refresh | mock 401 → auto refresh → retry |
| Password transmission | hash mode | SHA-256 computed correctly |
| Route guard | RequireAuth | logged-out → redirect |

Retry policy: every failure → show cause → fix → retry. Only stop on import failure (missing dependency) or compile error (syntax) — those need a real fix first.

## Phase 7 · Integration tests

Run against the live endpoints:

```
1. Discovery — GET https://api.autional.com/.well-known/openid-configuration → 200, issuer === https://api.autional.com
2. Register  — sdk.register() creates a user
3. Login     — sdk.login() returns a token set
4. Profile   — sdk.getProfile() returns the registered user
5. Refresh   — sdk.refreshToken() rotates the token set
6. Logout    — sdk.logout() ends the OP session, landing on the logout endpoint (5.2c)
7. Revoked   — the old access token is rejected (401 on a protected call)
8. Config    — fetchAuthConfig() returns the tenant policy chosen in Phase 2
```

Failure map:

```
network timeout → "Cannot reach Autional. Check network or status page (https://status.autional.com)." → wait → retry
401/403         → read the response body's `service` and `code` FIRST (see 7.1); never blanket-label as credentials
404             → "Endpoint missing — check the apiUrl/issuer values." → verify against the discovery document
500             → "Autional service error." → wait 30 s → retry
other           → show details → retry
```

### 7.1 Reading a 401/403 (split by `service` + `code`, not by HTTP status)

| status | body signals | meaning | what to do |
|---|---|---|---|
| 401 | `service:"middleware"` (gateway) | Routing / audience / auth-header problem: wrong path, a path that fell through to a broader prefix, missing token, wrong entry plane. **Not a credential error.** | Stop retrying. Report `status` + `code` + `service` + `request_id` + the exact URL. A 401 on a **public** endpoint (`/public/…`, trial registration) can never be a credential problem — public surfaces carry no credentials. |
| 401 | `service:"identity-service"` + `invalid credentials` | Real credential mismatch | Ask the user to confirm the account / appId / issuer |
| 403 | `code:40000503` · "not accessible from this entry plane" | Wrong entry plane (e.g. an admin endpoint called with a user-plane token) | Switch to the correct portal/plane. Do not retry. |
| 403 | any other | Insufficient permission | Surface to the user |

Only network unreachability and **identity-side** credential errors stop the automatic retry loop. A gateway 401 also stops it, but it must be reported as a **platform/routing defect** (with `request_id`), never as "your password or appId is wrong".

## Phase 8 · Wrap-up

### 8.1 File inventory

```
Created:  src/autional.ts · AUTIONAL_SETUP.md · AUTIONAL_CHANGES.md · .env.local (gitignored)
Modified: .env (public keys) · .gitignore · app entry · login page · README.md (integration note)
```

### 8.2 Portal guide (for AUTIONAL_SETUP.md)

Console portals are tenant-scoped — always write links with the tenant slug (`/{TENANT_SLUG}/`, from the tenant created in Phase 3, or the first path segment of a path-style issuer). A bare portal root drops the user on the tenant picker ("find your tenant") — the exact friction this skill exists to remove. Developer/Status are global sites: never append a slug to them.

| Portal | Address | Who | What |
|---|---|---|---|
| Admin | https://admin.autional.com/{TENANT_SLUG}/ | administrator | users / roles / security policy / audit log |
| User | https://user.autional.com/{TENANT_SLUG}/ | end users | change password / devices / MFA |
| Security | https://security.autional.com/{TENANT_SLUG}/ | security ops | anomaly detection / login trends / log export |
| Developer | https://developer.autional.com | developer | API docs / quickstart guides (global — no slug) |
| Status | https://status.autional.com | everyone | service health (global — no slug) |

Common operations: add user → Admin › Users › Add; reset password → Admin › Users › user › Reset; security policy → Admin › Security settings; create app → Admin › OAuth clients; audit log → Admin › Audit.

### 8.3 Final reminders

```
✅ Integration complete. Confirm:
  1. AUTIONAL_SETUP.md written
  2. Admin password stored in a password manager (shown once)
  3. .env.local NOT committed (gitignored automatically)
  4. npm run dev → exercise login → dashboard → logout
  5. Production: make sure the app origin is allowed for the app in the developer portal
  6. After a few days: check login success rate and token-refresh metrics
```

### 8.4 Branch delivery gate (explicit confirmation required)

```
Integration complete on branch feature/autional. Nothing has been merged.

Deliverables:
  branch:   feature/autional — <N> checkpoint commits ahead of <default>
  changes:  <diffstat summary>
  evidence: build <pass/fail> · unit tests <pass/fail> · integration tests <pass/fail>

Choose:
  [A] Merge into <default>
  [B] Keep the branch for review — merge later yourself
  [C] Discard the branch

Then return to the default branch; restore stashed user work if any (0.2 step 5 [B]):
  [A] git checkout <default> && git merge --no-ff feature/autional && git stash pop
  [B] git checkout <default> && git stash pop          (feature/autional stays)
  [C] git checkout <default> && git branch -D feature/autional && git stash pop
  (conflicts on git stash pop → hand to the user)
Never merge, push, or delete the branch before the user explicitly picks an option.
```

## Verification checklist

**Functional**

| Story | Acceptance | How |
|---|---|---|
| New user registers | welcome → first login works | integration test |
| Existing user logs in | email+password → dashboard | integration test |
| Token expiry | silent refresh, no login bounce | unit test |
| Multi-tab | logout in tab A → tab B notices | unit test |
| Wrong password | friendly error, no crash | integration test |
| CAPTCHA | 3 failures → challenge → retry | integration test |
| SSO login | authorize returns `302` pointing back to the callback, **not** `invalid_request` | preflight in 5.2b step 4 |
| SSO token exchange over CORS | preflight returns `204` with `Access-Control-Allow-Origin` = the app origin | preflight in 5.2b step 5 |

**Security**: password never in URL (hash mode) · tokens not in plaintext localStorage · logout ends the OP session, not just the local copy (5.2c) · an account switch shows no previous-account data — local cleanup runs even when the session ended without a logout (1.6 guard) · refresh-replay detected · CAPTCHA slows brute force.

**Config**: .env.local gitignored · password shown once · AUTIONAL_SETUP.md generated · summary contains everything.

## Edge cases

| Case | Handling |
|---|---|
| Third-party auth (Firebase/Auth0) | analyze switching cost → migrate or coexist |
| No user system at all | skip 1.2–1.4, create from scratch |
| Non-standard build tool | degrade to manual config, print every command |
| Autional endpoints unreachable | mark blocked, wait for network, surface to user |
| Tenant name unavailable 100× | suggest a new naming strategy |
| User refuses to replace the login page | guard-only mode (5.2 option C) |
| Several projects, one tenant | skip Phase 3 (reuse tenantId/issuer), but **register one client per app** (5.2b) — never reuse another app's `appId` |
| Login page renders zero input fields (or only an OAuth card) | tenant auth-policy dead end: `login_methods` declares a method with no matching provider configured → write the tenant auth policy (`PUT /admin/tenants/:id/auth-policy`; `login_methods` is a JSON string), then re-read the login page — a 200 from identity `PUT /admin/security/auth-config` does not prove the change took effect |
| Authorize page hangs (~30 s), then lands on the brand portal with no login form | this region's auth host is unhealthy (its backend proxy is failing) → cross-check the other region (swap the region suffix in `auth.autional.com`); pin the healthy region's `authorizeUrl` (5.2b) and report the outage |
| Tenant portal sign-in shows "This organization's sign-in is not configured" | the tenant's portal OAuth client was never supplied by the platform's auto-provisioning chain (tenant creation → client provisioning; tenants created before the fix, or a provisioning failure). Verify: `GET auth.autional.com/bff/identity/api/v1/public/auth-config/by-slug/{slug}` → `oauth_client_id` present (new tenants: within seconds). If absent → report to platform support for backfill; do not substitute an unrelated client |
| Browser console: `No 'Access-Control-Allow-Origin' header is present` | three checks in order: ① the origin is on **this client's** `cors_origins` — byte-identical `https://host[:port]`, no path, no trailing slash; ② registered just now → the API refreshes its registered-origin set every **≤ 60 s**, wait a minute and retry; ③ re-run the 5.2b step 5 preflight. Still no `Access-Control-Allow-Origin` → report to platform support with the Origin, the `client_id` and a timestamp |
| Consent page: "redirect URI is not registered" | the `client_id` is the tenant **portal** client (`Default Client for <slug>`) or the callback is not byte-identical. Verify by behaviour: the same client with a portal callback returns `302 login_required`, while your callback returns `400 invalid_request`; `GET /oauth/api/v1/oauth/client/{id}` returning `kind=portal` (or a name starting with `Default Client for`) corroborates | register a dedicated app client (5.2b) and use its `client_id` — never add your callback to the portal client |

### Existing tenant (skip Phase 3)

If the project already has Autional config:

```
"Found existing Autional config — reusing it.
  tenantId: <tenantId> · issuer: https://api.autional.com
  This will be app #N on this tenant. All apps share one user pool (SSO),
  but each app gets its OWN OAuth client: register it now (5.2b) and take
  its client_id as appId — never reuse another app's appId.
  [Continue with a new client] [Create new tenant]"
```

### Rollback / uninstall

```bash
npm uninstall @autional/core @autional/react @autional/api-identity
rm src/autional.ts
# remove AUTIONAL_* keys from .env
# remove the provider wrapper from the app entry
git checkout <login page>   # if option A was used
```

Note: users already registered in Autional are not migrated back into the local table.

## References

- `https://www.autional.com/ai/references/compliance-matrix.md` — compliance matrix (NIST / PCI / GDPR / HIPAA and more)
- `https://www.autional.com/ai/references/config-template.md` — config, env and AUTIONAL_SETUP.md templates
- Human-readable tutorial: https://www.autional.com/ai
- Scaffold CLI (alternative to the manual phases): `npx @autional/onboard`
- This file: https://www.autional.com/ai/skill.md · checksum: https://www.autional.com/ai/skill.md.sha256

> Mirror: https://cdn.autional.com/ai/latest/SKILL.md (Autional CDN) — keep the mirror in sync with the canonical URL above.

## Completion checklist (the agent must tick every box)

```
Phases:
  ☐ Phase 0 preparation (default branch detected; dirty-tree decision recorded; isolation branch active)
  ☐ Phase 1 analysis
  ☐ Phase 2 strategy confirmed by the user
  ☐ Phase 3 tenant + app + credentials saved
  ☐ Phase 4 dependencies installed
  ☐ Phase 5 integration + build passed
  ☐ Phase 6 unit tests passed
  ☐ Phase 7 integration tests passed
  ☐ Phase 8 wrap-up

Artifacts:
  ☐ src/autional.ts written
  ☐ 3 test accounts created (or explicitly declined)
  ☐ portal guide generated (admin / user / security / developer / status)
  ☐ .env contains the public config
  ☐ AUTIONAL_SETUP.md exists
  ☐ admin password flagged as "shown once"
  ☐ AUTIONAL_CHANGES.md exists
  ☐ OAuth client preflight returns 302 → callback (not "redirect_uri not registered")
  ☐ npm run dev boots the app

Workspace + delivery:
  ☐ checkpoints committed at each phase boundary
  ☐ default branch untouched (no merge / no push without the exit-gate choice)
  ☐ stashed user work restored (git stash pop), or conflicts handed to the user
  ☐ data-isolation finding disclosed; any change only with explicit user consent
  ☐ exit gate answered: merge / keep / discard

Final summary to the user:
  App ID: <appId> · Issuer: https://api.autional.com · Admin: <admin email>
  Login page: <url>/login · Start: npm run dev · Docs: AUTIONAL_SETUP.md
```

Option B — Scaffold with npx

Prefer generated code over agent-driven integration? Run the onboarding CLI in your project:

npx @autional/onboard

Option C — Own-CDN mirror

If the whole site is unreachable, the skill file has a same-named mirror on the own CDN, byte-identical to this site:

https://cdn.autional.com/ai/latest/SKILL.md

Files the agent reads

The skill contains zero secrets — every endpoint in it is a public URL. Only fetch these files from www.autional.com; never follow third-party mirrors.