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"
}
]
}
| Field | Notes |
|---|---|
privacy | Audit 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_model | CPUID brand string (CPUID leaves 0x80000002..04). |
system.os_release | Windows build via RtlGetVersion (survives compatibility shims). |
gpus[].vendor_name | Normalized: nvidia, amd, intel, microsoft, other. |
gpus[].vendor_id / device_id | Raw DXGI integers — useful for joining against external GPU catalogs. |
gpus[].driver_version | Raw Windows form, e.g. 32.0.15.9597. |
gpus[].driver_version_nvidia | NVIDIA-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:
- First NVIDIA discrete adapter (
vendor_id == 0x10DEanddedicated_video_memory_bytes > 0). - Otherwise, adapter with the highest
dedicated_video_memory_bytes. - 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.jsonnever lands (e.g. recording aborted before finalize), the session row stays withhw_snapshot_id IS NULLand that's the right semantics — there is no canonical snapshot for that session. - If the user changes hardware between recordings, the next
_meta.jsonproduces a new fingerprint → newdevice_hw_snapshotsrow → 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_uploadLambda 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 withhw_snapshot_id = null. - Sidecar size cap.
_meta.jsonis 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 walkings3_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 viaGET /api/hw/snapshotdirectly to a Supabase write).
References
- Producer:
recording_metadata_enricher.cpp::collectSafeRecordingHardwareMetadata - HTTP endpoint:
GET /api/hw/snapshot(http_server.cpp) - Populator:
lambda_src/on_upload/handler.py - Migration:
supabase/migrations/20260517145200_device_hw_snapshots.sql - Related: Upload Validation Pipeline, Bug Reporting, Core Signals
- Linear: EC-20