Skip to main content

Device HW Inventory

device_hw_snapshots is the per-device hardware catalog built from recording artifacts. One row per distinct (user_id, hw_fingerprint). Sessions link to a snapshot via recording_sessions.hw_snapshot_id, so quality issues can be correlated with hardware specs without leaving Supabase.

Background: this system ships EC-20. The earlier state stored hardware inside bug_reports.hw_config (sparse — only users who file reports) or only on S3 inside each _meta.json (not queryable from Supabase). Neither was enough for cohort analytics like "all failed CS2 sessions on NVIDIA driver branch X".

Pipeline

The same hardware payload appears in three places — *_meta.json::hardware on S3, device_hw_snapshots.snapshot on Supabase, and bug_reports.hw_config — because they all originate from a single producer: collectSafeRecordingHardwareMetadata() in playroll-core.

Canonical schema

The payload schema (from playroll-core::recording_metadata_enricher.cpp):

{
"privacy": {
"hostname": "excluded",
"username": "excluded",
"serial_numbers": "excluded",
"mac_addresses": "excluded",
"adapter_luid": "excluded",
"device_instance_paths": "excluded"
},
"system": {
"platform": "windows",
"cpu_architecture": "x64",
"logical_processors": 20,
"page_size_bytes": 4096,
"cpu_model": "Intel(R) Core(TM) Ultra 7 255HX",
"os_release": "10.0.26200",
"physical_memory_bytes": 33738285056,
"physical_memory_mib": 32175
},
"gpus": [
{
"index": 0,
"description": "NVIDIA GeForce RTX 5070 Ti Laptop GPU",
"vendor_name": "nvidia",
"vendor_id": 4318,
"device_id": 12056,
"dedicated_video_memory_bytes": 12884901888,
"dedicated_video_memory_mib": 12288,
"dedicated_system_memory_bytes": 0,
"shared_system_memory_bytes": 16869142528,
"output_count": 1,
"is_software_adapter": false,
"driver_version": "32.0.15.9597",
"driver_version_nvidia": "595.97"
}
]
}
FieldNotes
privacyAudit block: which sensitive identifiers were intentionally excluded. Hostname, Windows username, MAC, hardware serials, DXGI LUID, and device instance paths are never collected — see collectSafeRecordingHardwareMetadata().
system.cpu_modelCPUID brand string (CPUID leaves 0x80000002..04).
system.os_releaseWindows build via RtlGetVersion (survives compatibility shims).
gpus[].vendor_nameNormalized: nvidia, amd, intel, microsoft, other.
gpus[].vendor_id / device_idRaw DXGI integers — useful for joining against external GPU catalogs.
gpus[].driver_versionRaw Windows form, e.g. 32.0.15.9597.
gpus[].driver_version_nvidiaNVIDIA-branded XXX.YY form. Populated only for NVIDIA adapters (vendor_id = 0x10DE). Formula: last5 = (build_word % 10)*10000 + rev_word%10000, then major = last5/100, minor = last5%100.

Table

create table public.device_hw_snapshots (
id uuid primary key,
created_at timestamptz,
updated_at timestamptz,
user_id text not null,
hw_fingerprint text not null,
-- Denormalised quick-filter columns:
cpu_model text,
cpu_architecture text,
logical_processors integer,
physical_memory_mib bigint,
os_release text,
primary_gpu_vendor text,
primary_gpu_name text,
primary_gpu_driver text,
primary_gpu_driver_nvidia text,
-- Full payload, verbatim:
snapshot jsonb,
source text default 'core_meta_json',
constraint device_hw_snapshots_unique unique (user_id, hw_fingerprint)
);

The denormalised columns mirror the most useful fields out of snapshot so dashboards can filter and group without jsonb operators. The full payload remains available in snapshot for any analysis that needs the long tail (e.g. multi-GPU machines: only the primary is denormalised, the rest stays in the array).

recording_sessions.hw_snapshot_id is the nullable FK from session → snapshot, with ON DELETE SET NULL so a snapshot purge does not lose the sessions themselves.

Primary GPU selection

When a machine has multiple adapters (very common on laptops: NVIDIA discrete + Intel iGPU + Microsoft Basic), the denormalised primary_gpu_* columns describe the encoding adapter, not the boot adapter or the display adapter. Selection order in on_upload:

  1. First NVIDIA discrete adapter (vendor_id == 0x10DE and dedicated_video_memory_bytes > 0).
  2. Otherwise, adapter with the highest dedicated_video_memory_bytes.
  3. Otherwise null.

This matches what the pure-v2 NVENC pipeline actually encodes from. The full gpus[] array stays in snapshot for cases where it matters.

Fingerprint

hw_fingerprint = sha256(canonical_fields). The canonical fields and the format used to build the blob (verbatim from lambda_src/on_upload/handler.py::_compute_hw_fingerprint):

cpu_architecture=<v>|cpu_model=<v>|logical_processors=<v>|physical_memory_mib=<v>|os_release=<v>|gpu_vendor=<v>|gpu_name=<v>|gpu_driver=<v>

Properties:

  • Stable across recordings on the same device — same machine produces the same fingerprint forever, so 100 sessions deduplicate to 1 row + 100 FKs.
  • Splits naturally on real-world changes you want to see as a cohort boundary: driver upgrade, RAM upgrade, new GPU, OS feature update.
  • Privacy-safe: none of the input fields are PII. The fingerprint does not include LUID, MAC, hardware serials, or hostname.
  • Not cryptographically committing: it's a dedup key, not an identity proof. A malicious actor with the same HW could collide intentionally, which is fine — the field is for inventory, not authentication.

Lifecycle decoupling

device_hw_snapshots is populated by on_upload when the *_meta.json sidecar lands on S3. That happens after the recording ends and after the mp4 and csv artifacts have uploaded. The recording_sessions row, on the other hand, is created upfront by auth_presigned::handle_session_started when the recording begins.

These two lifecycles are intentionally independent:

  • The session row exists long before the snapshot arrives, so the linking step is a simple UPDATE recording_sessions SET hw_snapshot_id = ... WHERE session_id = ....
  • If the *_meta.json never lands (e.g. recording aborted before finalize), the session row stays with hw_snapshot_id IS NULL and that's the right semantics — there is no canonical snapshot for that session.
  • If the user changes hardware between recordings, the next _meta.json produces a new fingerprint → new device_hw_snapshots row → subsequent sessions link to the new row. Before/after cohort comparisons fall out for free.

RLS

alter table public.device_hw_snapshots enable row level security;

-- Users see only their own snapshots:
create policy device_hw_snapshots_select_own
on public.device_hw_snapshots for select to authenticated
using (user_id = (auth.uid())::text);

-- All writes go through the service-role Lambda — no client mutation:
create policy device_hw_snapshots_insert_service_only ...
create policy device_hw_snapshots_update_service_only ...
create policy device_hw_snapshots_delete_service_only ...

The Lambda authenticates with SUPABASE_SERVICE_ROLE_KEY. Users get read-only access to their own rows (useful for a future "your devices" UI). Operators query via the dashboard with the service role.

Query patterns

Sessions on a specific NVIDIA driver branch

select rs.session_id, rs.game_slug, rs.uploadability_status,
d.primary_gpu_name, d.primary_gpu_driver_nvidia
from public.recording_sessions rs
join public.device_hw_snapshots d on d.id = rs.hw_snapshot_id
where d.primary_gpu_driver_nvidia like '560.%'
and rs.recording_started_at > now() - interval '7 days';

Distribution of NVIDIA driver branches in the pilot

select primary_gpu_driver_nvidia,
count(distinct user_id) as users,
count(*) as devices
from public.device_hw_snapshots
where primary_gpu_driver_nvidia is not null
group by primary_gpu_driver_nvidia
order by users desc;

Users whose driver is below the NVENC SDK floor

The floor is logged by core at startup as service.startup_context_flags.nvenc_sdk_min_driver_version. To compare in SQL we have to parse the XXX.YY strings:

with floor as (select '570.00'::numeric as v)
select user_id, primary_gpu_driver_nvidia
from public.device_hw_snapshots, floor
where primary_gpu_driver_nvidia is not null
and primary_gpu_driver_nvidia::numeric < floor.v;

(The floor constant is currently 570.00 — see Core Signals → Startup context fields.)

Multi-device users

select user_id, count(*) as device_count
from public.device_hw_snapshots
group by user_id having count(*) > 1
order by device_count desc;

This is also the data shape needed for future device-specific gating (key/reward redemption guards, multi-device checks). (user_id, hw_fingerprint) is the precise identifier — same user from a different machine is a different hw_fingerprint.

Operational notes

  • Best-effort population. The on_upload Lambda treats every step (fetch, parse, upsert, link) as non-fatal. Failures log via OTel (hw_snapshot.linked, hw_snapshot.upsert_failed, hw_snapshot.meta_unreadable) but never break the upload pipeline. Sessions whose meta fails to parse simply stay with hw_snapshot_id = null.
  • Sidecar size cap. _meta.json is capped at 256 KiB by the Lambda to defend against a bogus oversized upload. Real sidecars are a few KiB.
  • Backfill. Pre-deploy sessions (hw_snapshot_id IS NULL) are not populated automatically. A one-shot script walking s3_uploads WHERE filename LIKE '%_meta.json' would be straightforward to write if needed; it is intentionally out of scope as of 2026-05-17.
  • Producer label. source = 'core_meta_json' is the only producer today. The column is reserved for future producers (e.g. an onboarding-time snapshot via GET /api/hw/snapshot directly to a Supabase write).

References