Skip to content

ADR-0016: Hybrid YAML/DB Profile Management

Status: Accepted • Date: 2026-02-18

1. Context & Problem

Silvasonic requires precise configuration for audio hardware ("Microphone Profiles") to ensure scientific-grade recordings: * Stability: We ship "known good" profiles for supported hardware (e.g., Dodotronic Ultramic, generic USB) that must work out-of-the-box. * Flexibility: Users may bring custom hardware or experimental microphones that need custom profiles without rebuilding container images. * Dynamic Editing: The Web-Interface (Tier 1, future) needs to create or edit profiles at runtime via the database.

Previously, profiles were loaded directly from YAML files at startup. This made dynamic editing impossible without complex file editing capabilities inside the container.

Related Decisions: * ADR-0005: YAML seed files in the repository = World A (immutable code). DB state = World B (mutable state). This ADR is a concrete application of the Two-Worlds principle. * ADR-0012: The config JSONB field is validated via Pydantic V2 models. * ADR-0013: The Recorder receives its profile configuration via Profile Injection (environment variables). The Recorder itself has no database access.

2. Decision

We chose: A Hybrid YAML-to-Database Bootstrapping model.

Reasoning:

  1. Database is the Single Source of Truth at Runtime:

    • The Controller reads profiles exclusively from the microphone_profiles database table.
    • The Recorder never accesses the database — it receives its configuration via Profile Injection (environment variables set by the Controller at container creation time, see ADR-0013).
    • The Web-Interface (future) can trivially CRUD profiles via the database.
  2. YAML Files as Seed Data:

    • System-default profiles are maintained as YAML files in the repository (services/controller/config/profiles/). The seeds live with the Controller — not the Recorder — because the Controller is their sole consumer (ProfileBootstrapper). The Recorder never reads these files.
    • YAML files MUST be parsed using pyYAML with strict safe_load (see AGENTS.md §5).
    • On every Controller startup, a ProfileBootstrapper:
      • Reads the YAML files.
      • Inserts them into the database if they do not exist (ON CONFLICT DO NOTHING).
      • Marks them as is_system=True.
    • This ensures "Repo is Truth" for initial system profiles, but protects user modifications (e.g., custom gain settings) from being overwritten on restart.
  3. Strict Device Linking:

    • The devices table has a Foreign Key (profile_slug) to microphone_profiles.slug.
    • Profile configuration is stored as validated JSONB (Pydantic V2) rather than loosely in a JSON column on the device.

2.1. Implementation Status

The database schema is implemented: * SQLAlchemy model: profiles.pymicrophone_profiles table with slug, name, description, match_pattern, config (JSONB), is_system (Boolean). * Device FK: system.pydevices.profile_slug → microphone_profiles.slug.

The ProfileBootstrapper and YAML seed files are implemented since v0.3.0 in seeder.py.

3. Options Considered

  • YAML-Only (No Database):
    • Rejected because: Makes runtime editing (Requirement 3) impossible without file-editing capabilities inside immutable Tier 2 containers.
  • Database-Only (No YAML Seeds):
    • Rejected because: Loses GitOps compatibility — system profiles would not be version-controlled and deployments would require manual database seeding.
  • Configuration via Environment Variables Only:
    • Rejected because: Profiles contain complex nested configuration (sample rates, channel mappings, match patterns) that is impractical to express as flat environment variables.

4. Consequences

  • Positive:
    • API Ready: The Controller can expose CRUD endpoints for user-defined profiles via the Web-Interface.
    • GitOps Compatible: Changes to system profiles in the repository automatically propagate on Controller restart.
    • Data Integrity: Foreign keys prevent deleting a profile that is in use by a device.
    • Two-Worlds Alignment: Seed data (World A) bootstraps runtime state (World B).
    • Generic Fallback (v0.4.0+): A generic_usb seed profile (48 kHz, 1 ch, S16LE, no processing) ensures that unknown microphones can record immediately with safe defaults. Users can switch to a better profile via the Web-Interface (v0.9.0+).
  • Negative:
    • Precedence Complexity: YAML seeds only apply if the profile does not exist. If a system profile needs a mandatory bugfix update from the repository, an explicit migration script or manual user deletion is required to force a re-seed.
    • Startup Penalty: Small overhead to parse YAMLs and sync to PostgreSQL on every boot (negligible for expected profile counts < 100).

4.1. Detection Strategy: Polling Only

Device detection uses 1-second polling (/proc/asound/cards + sysfs pathlib). Event-based detection (e.g., pyudev.Monitor) is explicitly rejected because:

  • Polling is simpler, robust, and has no external C dependencies.
  • Rootless Podman containers do not have access to the host udev socket.
  • The ~1s detection latency is imperceptible for the use case (microphones are connected for hours/days).
  • The sysfs/pathlib approach is pure Python and works on all Linux distributions without libudev.

5. References