# Arrowstack Character Mobile-first Vue 3 / TypeScript character-sheet PWA. Pinia manages state, Dexie stores characters in IndexedDB, Zod validates saves and JSON imports. A small Express gateway protects the application with a confidential Keycloak OIDC client using authorization code flow, PKCE, state and nonce verification. ## v0.3 Create/edit multiple characters, abilities, skill/expertise and save proficiency, HP/temp HP, armour class, speed, manually entered attacks, spellbook/preparation, generic resources, rest preview, inventory, notes, d20 rolls, account-separated local saves, JSON import/export. Official CC-BY SRD 5.1 and 5.2.1 reference content is bundled as an edition-separated catalogue. Account-backed PostgreSQL storage and offline-first sync are available. Existing local characters upload after sign-in; conflicts retain offline edits as a new local copy instead of overwriting remote changes. Short rest resets declared short-rest resources. Long rest also resets long-rest resources, restores HP and clears temporary HP. Hit dice and class-specific rest rules are manual. These are configurable sheet operations, not a complete edition-specific rules engine. ## Authentication and offline behavior Use existing Arrowstack accounts in the dev realm. Any authenticated realm user can use the app; no control-plane role is required. Keycloak tokens and the confidential client secret stay server-side. The encrypted 12-hour session uses a Secure, HttpOnly, SameSite=Lax, host-only cookie. Protected HTML requires a session; unauthenticated /api/session returns 401. Static JS/CSS and PWA metadata are public, but contain no character data or secrets. After successful sign-in, the worker caches the app shell and immutable build assets for offline use. The browser retains only the last subject/name/expiry, not OIDC tokens. Browser offline access expires with the session after at most 12 hours. Revocation cannot be checked while offline; reconnecting rechecks the gateway session. Character storage is namespaced by Keycloak subject, not encrypted at rest. Treat the device as trusted. Sign-out removes offline identity and cached shell while preserving per-account character backups. Sign-out ends the app session; the Arrowstack SSO session may still exist. Clearing site data removes unsynced device saves. Account-synced sheets download after sign-in. Export JSON backups before clearing browser storage. ## Build and deploy `npm ci`, `npm test`, `npm run build`. Docker executes tests and type-check/build, then runs as an unprivileged node user on 8080. No Gitea Actions builder is required. Runtime configuration: DATABASE_URL (injected by Arrowstack tenant provisioning), APP_ORIGIN=https://dnd.arrowstack.dev, KEYCLOAK_ISSUER, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET. Provision confidential client `dnd-character` with standard authorization code flow only, exact redirect `https://dnd.arrowstack.dev/auth/callback`, PKCE S256. Use Arrowstack's secret handoff to inject credentials directly into Coolify. /health exposes status/version/revision, never secrets. Canonical source is private in Admin/dnd-character-pwa. For now Coolify builds the public Admin/dnd-character-deploy mirror main branch over HTTPS; runtime secrets are never committed. See deployment.md for resource IDs and release steps. Redeploy a prior mirror commit through Coolify to roll back; database tables initialize additively at startup. Keep the database when rolling back; old app versions cannot expose synced sheets. Local production smoke test needs the same HTTPS origin and Keycloak configuration. Do not add an authentication bypass to production. Vite development uses its proxy for /auth and /api. ## MCP Connect a Streamable HTTP client to `https://dnd.arrowstack.dev/mcp`. Discovery is at `/.well-known/oauth-protected-resource/mcp` and `/.well-known/oauth-authorization-server`. Public/confidential OAuth client registration is supported at `/register`; authorization code with mandatory S256 PKCE, resource binding and exact registered redirects is handled by the MCP SDK. Keycloak sign-in uses the existing confidential app client, followed by a per-client consent screen. HTTPS redirects or HTTP loopback redirects only. Scopes: `characters:read` (default) and `characters:write` (explicit consent). Tokens are encrypted, audience-bound, short-lived and checked against persisted grants. Refresh tokens rotate; authorization codes and refresh tokens are one-time use. Grants expire with the 12-hour browser sign-in period. `/revoke` revokes a client grant; browser sign-out ends the browser session and does not revoke previously approved MCP grants. MCP access tokens cannot act as browser sessions. Tools: list_characters, get_character, create_character, update_character, change_hit_points, apply_rest, set_resource, delete_character, roll_dice. Resource: `character://collection`. Writes require an expected revision. Read a sheet again after a conflict; do not retry stale writes. Character rules remain manually configured, with an edition-separated official SRD reference catalogue. PWA sync runs after sign-in, after edits, on reconnect and every 30 seconds. Local IndexedDB remains available offline; server data is account-scoped by Keycloak subject. Sync stores deletion tombstones to prevent another device reviving deleted sheets. Stale offline modifications are preserved as a new local copy; deleting a stale sheet retains the newer account version. Database initialization is additive; do not remove tables or character data during rollback. ## Campaigns and roles Choose Campaigns to create a table or redeem a single-use invitation (valid seven days). Every campaign has at least one DM; memberships and player/DM roles are campaign-specific. DMs can invite players/co-DMs, manage members, save NPCs/monsters and secrets, plan party-visible sessions, and run initiative encounters. Owners explicitly assign saved PCs and can revoke optional DM HP delegation. Party views share name, class, species, level, abilities and combat stats; personal notes, inventory, spellbook and resources remain private. Private campaign notes are author-only, party notes are member-visible, and DM notes/hidden NPCs require a current DM role. Visible NPC notes remain DM-only. Removed members lose access and their PCs are unassigned. Role changes take effect server-side, including MCP access. Encounter participants and HP are party-visible snapshots; snapshot damage does not alter saved PC sheets. Actual party HP editing needs current DM membership, explicit owner delegation and the latest sheet revision. Campaign writes use optimistic revisions; stale writes require a refresh. Campaigns are online-only; existing personal sheets retain offline support. Twenty additional MCP tools cover campaigns, invites, membership, PC assignment, notes, NPCs, sessions, scenes, prepared encounters, combat and delegated HP. Existing characters:read / characters:write scopes also gate campaign operations, with membership and role checks on every call. Consent discloses both characters and campaigns. The private plugin retains its identity and endpoint. ## Official rules catalogue Browse SRD on Actions, Spells and Gear. Select 2014 (SRD 5.1) or 2024 (SRD 5.2.1); edition-specific identifiers prevent collisions. The seed contains all 319/339 SRD spells, 10/12 standard actions, and 386/382 equipment and magic-item entries. Equipment coverage includes weapon tables, armor, original adventuring-gear table and revised named gear descriptions; it is not a promise to include every tool, vehicle, variant or item in the books. Table content is transcribed as printed values and plaintext rows, with no automated rule application. Each entry carries source URL, page and edition. The gzip seed is generated by scripts/import-srd.py from the official PDFs; source SHA-256 digests and attribution are inside it. Regenerate using PyMuPDF and 2014.pdf / 2024.pdf in a source directory. The Docker runtime only needs the committed seed, not Python or the PDFs. Startup seeds rules_catalogue transactionally, keyed by stable ids, once per seed checksum. Repeated starts do not duplicate entries or modify character sheets. Authenticated API: GET /api/catalogue?edition=2024&kind=spell&q=Fire%20Bolt (name search, optional spell level/item category, 40-entry pages via offset); GET /api/catalogue/:id reads the complete entry. MCP search_rules_catalogue and get_rules_entry offer the same source-backed read-only lookup. Browsing needs a connection; entries copied into sheets retain their normal offline support. Spells copy into the spellbook as unprepared. Weapon/action selection opens the editor for the user to set their attack modifier. Item selection appends inventory notes and preserves existing equipment. ## Inventory Gear uses structured inventory entries with quantities, equipment and attunement markers, item notes and SRD provenance. Repeated catalogue selections increment the same entry; editions remain distinct. Existing inventory text remains in the inventory notebook. The additive optional `inventoryItems` field is preserved when older clients omit it, under the existing account and revision guards. Explicitly sending an empty array clears the collection. Equipment markers do not automatically change character stats. ## DM fieldbook Campaigns open to Session for DMs and Party for players. Session combines active initiative, snapshot damage/healing, manual conditions, a session agenda, DM-only quick notes and completed session/encounter history. Party shows owner names and saved-HP delegation. Campaign holds single-use player/co-DM invitations and member management; Journal holds notes/handouts and NPCs. Condition markers do not automate effects or update saved characters. Existing encounters without a condition list remain usable. The existing MCP advance_campaign_encounter tool accepts action `conditions`, participant_id and a replacement conditions array (empty clears), guarded by campaign DM role and revision. Loot proposals and richer adventure preparation are later stages. ## Session preparation Select a session fieldbook, prepare scenes and link campaign NPCs, accessible journal notes and that session’s prepared encounters. Scenes are ordered by creation and may be marked current or complete. Making a scene current does not reveal it. Explicit reveal/hide controls share only title and description; scene secrets/links and all encounter preparation are filtered server-side for players. Starting a preparation creates an independent fresh combat snapshot with a session association; repeated or stale starts cannot create another active instance of the same preparation. Templates retain their original HP and can be reused after combat finishes. NPC and note links are pruned when those entries are deleted. Existing campaigns gain empty additive collections without a destructive migration. Four additional MCP tools use the same campaign-role, write-scope and revision checks: save_campaign_scene, advance_campaign_scene, save_prepared_encounter and start_prepared_encounter (31 total with catalogue enabled). Loot acceptance remains the next stage. ## Coin purse Gear tracks gold (gp), silver (sp) and copper (cp) separately, with integer add/spend controls and direct amount editing. Total value uses copper arithmetic (100 CP = 10 SP = 1 GP) without exchanging denominations. Currency is an optional additive sheet field, retained when older clients omit it. Existing inventory notes are untouched; no free-text coin amounts are imported automatically. Currency remains private in party projections and is included in account sync, backups and character MCP reads/updates. ## Keycloak MCP user authorization The MCP uses Keycloak for OAuth issuance via confidential authorization-code flow with S256 PKCE. ChatGPT client: `dnd-character-mcp` in dev realm, exact redirect `https://chatgpt.com/connector_platform_oauth_redirect`. The app verifies signed Keycloak JWTs via JWKS and accepts only tokens with `azp=dnd-character-mcp`, validates issuer and expiration. The authenticated user's Keycloak `sub` is used directly as sheet/campaign owner, matching the PWA. Machine/service account tokens are not supported. OAuth registration, authorization, consent and token endpoints are on Keycloak, not D&D; legacy in-app OAuth source remains inert. The website retains its separate confidential client and browser session. The MCP application does not mint tokens or store OAuth secrets. ## Character portraits The character editor and hero portrait frame accept device photo and illustration uploads (JPEG, PNG, WebP, supported HEIC/HEIF). Browser canvas centre-crops at 4:5 and compresses to JPEG/WebP, max 320,000 data URL characters. Unsupported, oversized (>12 MB) or undecodable images are rejected. No raw photo uploads or external image service: compressed portrait is an optional, nullable part of the validated character, so offline IndexedDB, JSON backups, MCP sheet reads and authenticated cross-device sync all retain it. Explicit null removes it; older clients omitting the optional field preserve existing images through the storage merge. Photos remain private to the character owner and are never shared in campaign party projections.