16 KiB
SHD.build architecture
This project is intentionally being built in vertical slices.
Planned stack
- SvelteKit 2 / Svelte 5 with TypeScript
- Embedded Turso Database via
@tursodatabase/databasefor 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
localStorageonly 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.
Authorization must be checked on the server for every mutation. Hiding an admin link is not 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 are stored as Argon2id PHC strings with a unique random salt per password. Current work factors follow OWASP's 19 MiB / 2 iteration / single-lane minimum profile and can be increased over time; successful login will eventually rehash credentials whose parameters are outdated.
Database runtime
The application uses Turso Database in-process through its native Node package. It opens a local SQLite-compatible database file and 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.
Increment plan
- Product shell and anonymous local-draft builder.
- Embedded Turso migrations, repositories, and seed data.
- Credentials and Discord authentication.
- Full equipment/stat builder driven by database definitions.
- Publishing, discovery, ratings, comments, favorites, and copy flow.
- Rich build guides and moderation.
- 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.
Low-downtime development migrations
The development database is a shared working data source: contributors may be entering ISAC records while application development continues. Database changes therefore use a prepared migration window rather than taking the server down at the beginning of implementation.
Before the window, finish the code and idempotent migration, batch related
schema changes, identify verification queries, and run npm run check plus
npm run build while the server remains available. Prefer additive,
backward-compatible tables and safely defaulted or nullable columns. Coordinate
with known contributors so the stop does not interrupt an unsaved submission.
The offline window is deliberately narrow:
- Inspect running processes and stop only this project's development server.
- Run and verify
npm run db:backup. - Run the already-prepared migration.
- Verify expected schema state, record counts, and critical entities.
- Restart the development server immediately.
- Confirm a basic page or health check, then perform extended browser and regression testing with the service online.
Code editing, builds, formatting, documentation review, and noncritical UI testing do not belong inside this window. Destructive, data-rewriting, or long-running migrations require a separately communicated maintenance window and a verified recovery plan. Every migration handoff reports the backup path, migration outcome, successful server restart, critical checks, and the approximate offline interval when available.
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:
- The operator must independently hold
audit.revert. - The original action must be explicitly supported and contain a usable before-snapshot.
- The UI requires a second, explicit confirmation.
- The server reloads the target and compares it with the original after-snapshot, ignoring bookkeeping timestamps. A mismatch means newer work exists and blocks automatic compensation.
- Referenced roles, permissions, and relationships are revalidated before restoration. Restoring administrator authority additionally requires full administrative access.
- The server restores the previous state and appends an
audit.revertedevent whose target is the original audit-event ID.
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.
7. A successfully reverted event cannot be reverted again.
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.
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, while 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.
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 only 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.
Game-data icon storage
Talents, brand sets, OS Protocols, and Skill Chips reference uploaded artwork through the
polymorphic game_data_icons metadata table. Original uploads are never
retained. The shared server pipeline accepts transparent SVG, PNG, WebP, and
AVIF sources, rejects active or externally referenced SVG content, and uses
Sharp’s asynchronous native worker pool to rasterize and normalize every
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
only safe icon metadata—not image bytes—in their 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
ItemIcon component; the placeholder is never inserted into
game_data_icons. A later edit or proposal can attach the real icon without
recreating or replacing the canonical item.