13 KiB
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.
ChatGPT predefined confidential OAuth client
ChatGPT installations that require a Client ID and Client Secret use the server's predefined confidential client rather than DCR. The MCP OAuth issuer remains https://dnd.arrowstack.dev, not Keycloak; Keycloak only authenticates the person at the consent step. Configure MCP_CHATGPT_CLIENT_ID and MCP_CHATGPT_CLIENT_SECRET in Coolify through the secure Arrowstack Keycloak-client secret handoff. Use an independent Keycloak client dnd-character-chatgpt with all Keycloak login/grant flows disabled; it serves as the secret's management and rotation home, not as the OAuth issuer. The backend reads its credential pair to authenticate the predefined ChatGPT client at /token.
Redirect URI is exactly https://chatgpt.com/connector_platform_oauth_redirect, auth method client_secret_post, required S256 PKCE, authorization-code flow. Client credentials must never appear in source control or application logs. In Keycloak Admin Console → Clients → dnd-character-chatgpt → Credentials, retrieve the secret privately for ChatGPT's plugin form. Do not reuse the website's dnd-character secret. Upon rotation, hand the changed secret back to Coolify and redeploy. DCR remains available to clients that support it.