Skip to content

feat: Alpha/beta version expiry and build lifecycle management #632

@amiable-dev

Description

@amiable-dev

Summary

Pre-release builds (alpha, beta) should have a time-limited expiry window so that outdated versions don't remain in use indefinitely. After expiry, the app should prompt the user to update rather than silently continuing to run stale code.

This is distinct from freemium tier gating (#631) — expiry applies to ALL users of a pre-release build regardless of tier. It's about build lifecycle, not feature access.

Proposed Design

Expiry Mechanism

Embed a build timestamp and expiry window at compile time:

// build.rs or main.rs
const BUILD_TIMESTAMP: &str = env!("BUILD_TIMESTAMP");  // Set by CI or build script
const EXPIRY_DAYS: u64 = 90;  // Alpha/beta builds expire after 90 days
const BUILD_CHANNEL: &str = env!("BUILD_CHANNEL");  // "alpha", "beta", "release"

On app startup:

  1. Parse BUILD_TIMESTAMP, add EXPIRY_DAYS
  2. Compare against current system time
  3. If expired → show expiry banner/modal
  4. If within 14 days of expiry → show warning banner

Expiry Behaviour (NOT a hard block)

Soft expiry (recommended for alpha/beta):

  • App still launches and functions
  • Persistent banner at top of window: "This alpha build expired on {date}. Please update to the latest version."
  • Banner includes "Download Update" link and "Dismiss" (dismisses for current session only, returns on next launch)
  • Optional: disable certain features (e.g., LLM chat) after expiry to encourage update without breaking core mapping functionality

Hard expiry (alternative, more aggressive):

  • App launches to a full-screen update prompt
  • Core daemon functionality continues (daemon has no expiry — it runs headless)
  • GUI becomes non-functional until updated

Recommendation: soft expiry for alpha, graduating to either approach for beta depending on product needs.

Build Channels

Channel Expiry Auto-update Version suffix
alpha 90 days Check only (manual download) v4.37.0-alpha
beta 180 days Prompt to update v4.37.0-beta
release Never Optional check v4.37.0

Implementation

Build Script (build.rs)

fn main() {
    // Embed build timestamp as compile-time env var
    let timestamp = chrono::Utc::now().to_rfc3339();
    println!("cargo:rustc-env=BUILD_TIMESTAMP={}", timestamp);

    // Channel from env or default to "alpha"
    let channel = std::env::var("BUILD_CHANNEL").unwrap_or_else(|_| "alpha".to_string());
    println!("cargo:rustc-env=BUILD_CHANNEL={}", channel);
}

Tauri Command

#[tauri::command]
fn get_build_info() -> BuildInfo {
    BuildInfo {
        version: env!("CARGO_PKG_VERSION"),
        channel: env!("BUILD_CHANNEL"),
        build_date: env!("BUILD_TIMESTAMP"),
        expires_at: calculate_expiry(),
        is_expired: check_expired(),
        days_remaining: days_until_expiry(),
    }
}

Frontend Banner Component

<!-- ExpiryBanner.svelte -->
{#if expired}
  <div class="expiry-banner error">
    This {channel} build expired on {expiresAt}. 
    <a href={updateUrl}>Download latest version</a>
  </div>
{:else if daysRemaining <= 14}
  <div class="expiry-banner warning">
    This {channel} build expires in {daysRemaining} days.
    <a href={updateUrl}>Check for updates</a>
    <button on:click={dismiss}>✕</button>
  </div>
{/if}

Clock Manipulation Protection

Users could set their system clock back to avoid expiry. Mitigations:

  • Light protection: On each launch, store the latest observed system time in localStorage. If current time is earlier than stored time, treat as suspicious and use the stored time for expiry calculation.
  • No network requirement: Expiry must work offline. Don't depend on NTP or server-side validation.
  • Pragmatic stance: If a user deliberately manipulates their clock to use an expired alpha build, that's acceptable. This isn't DRM — it's a nudge to stay current.

Daemon Independence

The daemon has no expiry. It runs headless, may be managed by launchd/systemd, and should never stop processing MIDI because a build date passed. Expiry is GUI-only, consistent with the "daemon + TOML is self-contained" principle (#559).

What This Issue Does NOT Cover

Related Issues

Priority

P4 — Not urgent while in active development with direct user contact, but should be in place before any public alpha/beta distribution.

Acceptance Criteria

  • Build timestamp embedded at compile time via build.rs
  • Build channel (alpha/beta/release) configurable via environment variable
  • Alpha builds expire after 90 days, beta after 180 days, release never
  • Expiry banner shown in GUI when build is expired or within 14 days of expiry
  • "Download Update" link in banner
  • Expired builds still function (soft expiry) — banner is persistent but dismissable per session
  • Daemon has no expiry awareness
  • Light clock-manipulation protection via stored high-water-mark timestamp
  • Release builds have no expiry check

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions