Files
shd.build/docs/architecture.md
T
JB 5a67b252aa
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 19s
Validate and deploy production / deploy (push) Successful in 1m0s
Validate and deploy staging / deploy (push) Successful in 36s
feat: add prelaunch site controls and refresh branding
2026-07-27 02:24:13 -04:00

25 KiB
Raw Blame History

SHD.build architecture

The project is built in vertical slices.

Planned stack

  • SvelteKit 2 / Svelte 5 with TypeScript
  • Embedded Turso Database via @tursodatabase/database for durable application data
  • Auth.js-compatible server authentication with Discord OAuth and credentials providers
  • Argon2id password hashing, server-side sessions, CSRF protection, and role checks
  • Browser localStorage only for anonymous build drafts

Product boundaries

  • Public: browse, sort, and view published builds and guides.
  • Anonymous: create and edit local drafts.
  • Authenticated: publish, rate, comment, favorite, copy, and manage owned builds.
  • Editor: manage builder source data.
  • Admin: manage users, roles, source data, and audit history.

Server code checks authorization for each mutation. Hiding an admin link does not grant authorization.

Weapon and gear range calculations must preserve unrounded decimal endpoints and follow the empirically validated progression rules in STAT_LEVEL_SCALING.md. In particular, leveling is linear rather than compounded and game-matching integer values are truncated only at the presentation boundary.

Local credentials use Argon2id PHC strings with a unique random salt per password. Current work factors meet OWASP's 19 MiB / 2 iteration / single-lane minimum profile. Successful login rehashes credentials with outdated parameters.

Database runtime

The application uses Turso Database in-process through its native Node package. It opens a local SQLite-compatible database file. The application does not use Turso Cloud, libSQL HTTP, remote replicas, auth tokens, or synchronization.

The database defaults to ./data/shd-build.db. Production deployments must provide a persistent writable filesystem and can override the location with TDR_DATABASE_PATH. This rules out ephemeral serverless filesystems unless a persistent volume is attached.

Site launch controls

The singleton site_launch_settings row stores the public launch gate, account creation state, and availability for News, Database, Builds, Guides, and Builder. Schema migration creates the row with Coming Soon enabled, account creation enabled, and all five sections enabled. The application caches the row for five seconds and clears the process cache after an operator saves changes.

Agents with site.settings.manage edit the row from ISAC → System Configuration → Site Controls. The save action requires an explicit value for all seven controls and a confirmation token. A successful update records system_configuration / site_access.updated with the operator, target row, and complete before/after snapshots.

The request hook applies the launch gate to anonymous traffic and section availability to every request:

  • With Coming Soon enabled, anonymous GET and HEAD requests redirect to /coming-soon. Anonymous mutations outside exempt routes receive a JSON 404.
  • Authenticated agents bypass the Coming Soon gate and enter the application.
  • A disabled public section blocks its route family for every user, including authenticated agents: /news, /database, /builds, /guides, or /builder. GET and HEAD requests redirect to /coming-soon with the unavailable section identified. Mutations receive a JSON 404.
  • Authentication callbacks, sign-in, contact, legal pages, health endpoints, scheduler endpoints, application assets, and the prelaunch page remain reachable without a session.

The global navigation omits disabled sections for every user. Server checks remain authoritative for direct URLs and mutations. An authenticated operator may open /coming-soon to preview the promotional page, but the preview does not override section availability.

Account creation uses a separate control. Closing registration blocks password registration, a magic-link request for an unknown email, and Discord OAuth for an unknown Discord identity. Existing password, email, and Discord identities can still sign in. The sign-in page hides the password registration form and explains registration closure. This guard runs on the server, so a crafted form submission or OAuth callback cannot create an account while registration is closed.

Increment plan

  1. Product shell and anonymous local-draft builder.
  2. Embedded Turso migrations, repositories, and seed data.
  3. Credentials and Discord authentication.
  4. Full equipment/stat builder driven by database definitions.
  5. Publishing, discovery, ratings, comments, favorites, and copy flow.
  6. Rich build guides and moderation.
  7. Admin editors, validation, audit log, and data import/export.

Administrator bootstrap

The first successfully authenticated user is promoted to admin if the database does not contain an administrator. Promotion uses one conditional database update so concurrent first sign-ins cannot both become administrators. Once an administrator exists, later sign-ins never alter roles.

Database backup and restore

Embedded Turso databases contain contributor-entered source data and must be preserved across development changes.

Create a timestamped development backup:

npm run db:backup

Create a production backup:

npm run db:backup:production

The scripts write versioned JSON documents beneath data/backups/ by default. That directory is ignored by Git because backups contain private user data, including password hashes.

To restore, first create an empty database and apply the schema. Then run:

npm run db:restore -- data/backups/<backup-file>.json

The restore operation refuses to import into any database containing records. This guard prevents accidental merging or overwriting of contributor data. Stop the application before backup or restore so the embedded database file is not locked by another process.

Development database maintenance

Treat the development database as persistent data. Stop this project's development server before backup, export, restore, migration, or other work requiring exclusive access. Create and verify a JSON backup, perform the prepared operation, verify schema state and critical record counts, then restart and health-check the application. Prefer additive, repeatable migrations and retain the verified backup if maintenance fails.

Production and staging remain shared persistent environments and retain the deployment, backup, synchronization, and maintenance safeguards documented in docs/DEPLOYMENT.md.

ISAC audit and compensating reversals

Every successful ISAC mutation appends an event to audit_log through the shared server audit utility. Events identify the operator, operational category, specific action, target entity, timestamp, and useful before/after evidence. The audit registry is read-only and requires audit.view; the seeded Auditor role grants that permission without granting mutation capabilities.

Audit history is immutable. “Revert” is implemented as a new compensating mutation rather than deletion or modification of the original event:

  1. The operator must independently hold audit.revert.
  2. The original action must be explicitly supported and contain a usable before-snapshot.
  3. The UI requires a second, explicit confirmation.
  4. The server reloads the target and compares it with the original after-snapshot, ignoring bookkeeping timestamps. A mismatch indicates newer work and blocks automatic compensation.
  5. Referenced roles, permissions, and relationships are revalidated before restoration. Restoring administrator authority additionally requires full administrative access.
  6. The server restores the previous state and appends an audit.reverted event whose target is the original audit-event ID.
  7. A reverted event cannot be reverted again.

Moderated game-data contributions

ISAC data access is split into data.view, data.propose, data.manage, and data.approve. Data Viewers can inspect registries. Data Contributors can submit the same editor forms, but the request is serialized into data_change_proposals instead of mutating canonical tables. Data Oversight reviews the queue and must explicitly confirm approval or rejection.

Approval replays the stored form through its original SvelteKit server action. This preserves canonical validation, relational updates, icon processing, and the normal game-data audit event. Proposal creation and review decisions also produce separate data_moderation audit events. Failed canonical validation marks a proposal as failed without changing live data. Pending counts appear alongside each data registry in ISAC so collaborators can see concurrent suggestions.

The protected seeded roles are Data Viewer, Data Contributor, Data Editor, and Data Oversight. Data Editor retains direct-write access for trusted operators; Data Oversight combines direct-write access with queue review. New ISAC data modules must use the shared moderation interception path and audit both the final mutation and proposal lifecycle.

Every authenticated, active registered agent receives isac.access, data.view, and data.propose as baseline permissions in the authorization layer. This baseline is intentionally implicit rather than a removable role assignment, so it applies consistently to existing users and every account creation method. Registered agents can inspect live source data and submit moderated proposals, but cannot directly mutate canonical records, review the queue, or access unrelated ISAC operations without additional permissions. Unsupported destructive identity operations, account deletion, incomplete snapshots, stale targets, and events whose dependencies no longer exist are marked non-reversible or rejected. Recovery for those cases requires a specialized workflow rather than inferred database writes.

System metrics

ISAC uses /isac as its canonical route namespace. The server permanently redirects legacy /admin URLs to their matching /isac location so old notifications and external bookmarks remain usable.

The read-only /isac/metrics route requires metrics.view and collects a point-in-time process-local health snapshot. Node and operating-system APIs provide process CPU and memory, event-loop utilization and delay, host memory, load average, core count, platform, and uptime. Embedded Turso pragmas provide page allocation, page size, and free-page information; aggregate SQL queries provide application record volumes.

The interface refreshes on demand and may poll every ten seconds while the operator keeps auto-refresh enabled. CPU and event-loop values are deltas from the previous server-side sample, so the first or infrequently requested sample is directional rather than a monitoring-grade time series. Production alerting and historical analysis should eventually use a dedicated external telemetry system. This surface deliberately omits secrets, environment-variable values, and full database paths.

The same surface reports remove.bg paid credits and free API calls as separate capacity pools. The scheduler reconciles the account balance no more often than the configured interval, while successful image processing applies the provider's X-Credits-Charged value to the persisted estimate. Warning and critical transitions create one alert episode with a unique delivery for each active Administrator. A later critical transition may alert again; repeated scheduler runs within the same episode do not. Provider errors preserve the last known balance and never expose the API key or raw response body.

Weapon-owned exotic talents

Standard weapon talent pools remain globally reusable and classified by Talent 1 / Top or Talent 2 / Bottom. An exotic talent is different: one exotic weapon owns one dedicated talent, referenced by weapons.exotic_talent_id.

The weapon editor creates and updates the underlying normalized talents, talent_tiers, trigger links, effect links, description template, and Exotic weapon scope together with the weapon. Server validation rejects an exotic talent ID not already owned by the edited weapon and rejects any talent shared by another weapon. Converting a weapon back to standard clears the relationship and archives its now-unowned exotic talent. Exotic talent creation and editing are rejected by the shared Talent Registry so ownership cannot be bypassed.

OS Protocol progression

OS Protocols use dedicated normalized tables because their talent progression has six calculated states (base plus five upgrades) rather than the shared two-tier talent model. Each protocol owns one os_protocol_primary_effects record and one os_protocol_talents record. Talent effects and reusable triggers live in their respective join tables.

The three secondary attributes are normalized separately. One os_protocol_secondary_profiles record exists for each configured quality + attribute-type combination, with its three fixed stat/value pairs in os_protocol_secondary_effects. Every matching protocol inherits that profile; secondary values are not copied onto individual protocol records.

The unique primary value uses base × (1 + (level − 1) / 49) for levels 1–50, while talent effects use base × (1 + upgrades / 5) for zero to five upgrades. Server actions validate all relationships and append the full owned graph to the existing game_data audit history.

Weapon Mods and tiered weapon slots

weapon_mod_slots records each weapon's available slot vocabulary per tier. Canonical validation requires four unique slots for T0 and five for T1/T2 for every tier at or above the weapon's native tier. Regular Weapon Mods declare a single slot type and one or more compatible base weapon categories through weapon_mod_compatible_categories; eligibility requires both a category match and the slot to exist on the equipped weapon at its selected tier.

Exotic Weapon Mods instead reference one exotic_weapon_id. They do not carry regular category compatibility, are considered permanently equipped, and may only be leveled. They do not use quality profiles, random primary rolls, secondary pools, or regular Mod Effects. Their canonical contributions are fixed attribute references with explicit level-1 and level-20 values in weapon_mod_exotic_attributes; intermediate values interpolate linearly.

All regular Weapon Mods permit Firepower, Toughness, or Engineering as a builder-time primary choice. weapon_mod_quality_values stores the shared level-1 and level-20 primary roll endpoints for each quality; primary values interpolate linearly from levels 1–20.

weapon_mod_secondary_options is the shared pool from which a build selects two distinct secondary attributes. Every option owns a minimum and maximum at each quality through weapon_mod_secondary_quality_values; secondary availability is not tied to a numbered slot in canonical data. Mod Effects are triggerless, hold one or two attribute references, and store fixed per-quality values in weapon_mod_effect_quality_values.

Gear Mods and tier slot capacity

Gear Mod definitions are standard-quality records only; there is no exotic Gear Mod branch. Every definition supplies primary roll ranges for Standard, Specialized, Superior, and High-end quality at Levels 1 and 20. Values between those endpoints scale linearly. A build instance selects one primary attribute from Firepower, Toughness, Engineering, Health, or Armor.

The two secondary slots draw distinct attributes from one shared option pool. Each available secondary attribute stores its own minimum and maximum for all four qualities. Gear tier determines capacity without per-item configuration: T0 gear has one Gear Mod slot; T1 and T2 gear have two.

Canonical data is stored in gear_mods, gear_mod_quality_values, gear_mod_secondary_options, and gear_mod_secondary_quality_values. Successful mutations use gear_mod.created, gear_mod.updated, gear_mod.archived, and gear_mod.unarchived audit actions. Contributor submissions use the gearmods proposal module and remain non-canonical until approved by Oversight.

Skill Chip quality scaling

Skill Chip sets support Superior and High-end chip instances. Each set owns one 2-piece bonus, distinct primary and secondary stat-option pools, and one 3-piece talent with reusable trigger links and normalized effect rows. Primary and secondary options reference active attributes that define Skill Chip minimum and maximum roll values; a configured chip rolls one primary and two distinct secondary stats from those separate pools.

Every active Skill Chip definition belongs exclusively to one active specialization ability through skill_chip_sets.specialization_ability_id. An ability has capacity for exactly three active definitions. The save and restore actions enforce that capacity, and the foreign key is required for every row. A specialization focus is builder-ready only when each of its three assigned abilities has all three active Skill Chip definitions. Skill Chip mutations refresh readiness for focuses that reference both the previous and resulting ability.

Both set-bonus progression systems use the number of equipped High-end chips as the interpolation step. For h High-end chips, where h is zero through three, a configured minimum and maximum resolve as minimum + (maximum − minimum) × h / 3. Zero High-end chips therefore produce the minimum, while three produce the maximum. The editor stores endpoints and previews all four states, preventing duplicated tier data from diverging. Skill Chip mutations, owned relationships, and safe icon metadata are captured in the game_data audit stream.

Specialization combat effects and loadout state

specialization_combat_effects stores reusable effect identity and description within one Specialization. A node rank attaches that identity through specialization_tree_rank_combat_effects and supplies the trigger count, trigger window, duration, stack limit, and stack gain. The first supported lifecycle uses an enemy-hit counter that resets after a proc. Each stack gain refreshes one shared duration, and expiry clears all stacks.

specialization_tree_rank_combat_modifiers maps each attachment to one or more Attributes with a value per stack. This split lets ranks share effect language while changing their trigger and values. Specialization mutations capture the attachment and modifier sets in the game_data audit event. Contributors can submit effect definitions and node attachments through the existing Specialization proposal workflow.

Loadout schema version 2 stores one focus, the selected rank for each tree node, nine Skill Chips, conditional-effect modes, and encounter assumptions. Each of the three focus abilities owns three chip positions. Runtime readers accept only the exact version 2 structure. Local drafts, saved builds, shared builds, and calibration snapshots reject older or partial structures instead of repairing them. Database migration removes obsolete snapshots after the required environment backup.

Versioned combat calculations

calculation_revisions supplies one active rule set. Draft revisions can change, while active and retired revisions stay immutable. calculation_stages stores the ordered typed pipeline. The base-damage stage holds the first position, and the firing-cycle stage holds the last. attribute_calculation_profiles maps an Attribute to a stage with its weight, optional caps, curve, and weapon-category scope.

The shared combat engine consumes weapon facts, accumulated Attributes, calculation profiles, and conditional effects. It produces four hit values, burst DPS, reload-aware sustained DPS, encounter DPS, and stack telemetry. Encounter mode walks the expected shot timeline from RPM, magazine size, reload time, hit rate, firing activity, and encounter duration. Peak mode applies maximum stacks, and Off mode excludes the effect.

The engine returns INSUFFICIENT_DATA with missing requirement keys when a calculation lacks required inputs or an Attribute binding. Callers must render that state instead of a partial result. Saved and shared builds load the current active revision for each request.

calculation_calibration_cases stores a versioned loadout snapshot, observed hit values, timing values, optional encounter DPS, and the model prediction. Ready cases require the four hit values plus magazine and reload times. The assisted suggestion action compares ready observations with their predictions and creates a draft revision for an operator to review.

Rule activation requires calculation.publish and an explicit confirmation. ISAC records draft creation, draft edits, publication, calibration changes, and assisted suggestions under the calculation_configuration audit category. ISAC data export and environment synchronization include calculation rules, calibration cases, and Specialization combat-effect tables in dependency order.

Game-data icon storage

Talents, brand sets, OS Protocols, and Skill Chips reference uploaded artwork through the polymorphic game_data_icons metadata table. The shared server pipeline accepts transparent SVG, PNG, WebP, and AVIF sources, rejects active or externally referenced SVG content, and uses Sharp’s native worker pool to rasterize and normalize each accepted icon to a transparent 256 × 256 WebP.

Normalized files live beneath the ignored data/game-icons/ directory and are served by the validated /icons/game-data/[type]/[id] endpoint with immutable cache headers and MIME sniffing disabled. ISAC save actions process files before mutation, apply upload or removal after record validation, and include safe icon metadata, rather than image bytes, in audit snapshots.

Icon metadata is optional. Canonical records and contributor proposals may be created without an upload, allowing values and artwork to be contributed in separate dependency-safe changes. Missing artwork is represented at render time by the static /missing-item-icon.svg asset through the shared MISSING_ITEM_ICON_URL constant and ItemIcon component. The placeholder does not enter game_data_icons. A later edit or proposal can attach the real icon without recreating the canonical item.

source_entity_type and source_entity_id link each game_data_icons row to a managed_images record. Both records share one storage key and one WebP file. The managed record adds the searchable library name, category, tags, creator, dimensions, and assignment state. Game-data icon saves upsert this record, keep operator-edited metadata, and merge current entity-context tags. Removing a source icon removes the linked library record and shared file.

The additive database migration registers older game-data icons and selects an administrator or the earliest active account as the migration actor. ISAC and managed-image exports accept linked storage keys. Environment synchronization keeps the library metadata and one copy of each file.

Oversight notification dispatch

New Data Change and Avatar Oversight review items are routed only to agents who currently hold data.approve or avatars.review respectively. Each reviewer chooses Immediate, 15-minute, one-hour, or daily delivery independently for each authorized queue. Immediate alerts are created during submission; consolidated alerts are persisted in oversight_notification_queue.

Production must run the idempotent dispatcher once per minute from exactly one scheduler instance:

* * * * * cd /path/to/shd.build && npm run notifications:oversight:production

The dispatcher groups due rows by reviewer, queue, cadence, and delivery window. Daily rows use the next 10:00 AM wall-clock boundary in America/New_York, including daylight-saving transitions. Dispatched rows retain delivered_at evidence and are excluded from later selection. Existing ISAC notification delivery preferences determine optional browser and email fan-out after the consolidated inbox message is created.

Automated ISAC system messages

Automated message definitions live in automated_system_messages and are managed through ISAC → System Configuration → Automated Messages by agents with system.notifications.manage. Edits are recorded under the system_configuration audit category. Definitions identify a lifecycle event, enabled state, delay in minutes, message content, primary destination, and structured action callouts.

account.created is emitted by password, magic-link, and Discord registration. The delivery table has a unique message/user/event/event-ID constraint, so the same account creation cannot deliver the Welcome Agent message more than once. The default welcome introduces News, Database, Builds, Guides, Builder, ISAC Contributor Mode, and the SHD Discord community.

Immediate definitions are delivered in the registration request. Definitions with a delay are persisted in automated_system_message_deliveries. Production must run the delayed dispatcher once per minute:

* * * * * cd /path/to/shd.build && npm run notifications:system:production

Only one dispatcher instance should run for the embedded database. Delivered rows retain both their delivery timestamp and generated notification ID.