engineering 8 min read

Demo Mode: Safe Showcasing for Intelligence Platforms

Building a deterministic demo system with simulation engine, DemoModeGuard, and environment-variable activation for safe platform demonstrations.

Tomas Korcak (korczis) 577 words

Demonstrating an intelligence platform to a potential client is a minefield. You cannot show real investigation data. You cannot trigger live OSINT queries against actual targets during a sales call. You cannot risk a production error interrupting a demo. Yet the demo must feel real enough to convey the platform's capabilities.

Prismatic's demo mode solves this with three components: a simulation engine that produces deterministic fake data, a guard module that blocks all production-impacting operations, and environment-variable activation that ensures demo mode can never accidentally leak into production.

Activation

Demo mode is controlled by a single environment variable:

# config/runtime.exs
config :prismatic, :demo_mode,
  enabled: System.get_env("PRISMATIC_DEMO_MODE") == "true",
  scenario: System.get_env("PRISMATIC_DEMO_SCENARIO", "default")

The Prismatic.Demo module provides the runtime check:

defmodule Prismatic.Demo do
  @moduledoc """
  Demo mode switch. Controls whether the platform operates
  in demonstration mode with simulated data.
  """

  @spec enabled?() :: boolean()
  def enabled? do
    Application.get_env(:prismatic, :demo_mode)[:enabled] == true
  end

  @spec scenario() :: atom()
  def scenario do
    Application.get_env(:prismatic, :demo_mode)[:scenario]
    |> String.to_existing_atom()
  end
end

When demo mode is disabled (the default), the Demo module is effectively inert. No code paths change, no data is intercepted, no routes are modified.

DemoModeGuard

The guard is the safety mechanism. It intercepts operations that would have production side effects and either blocks them or redirects them to the simulation engine:

defmodule Prismatic.Demo.Guard do
  @moduledoc """
  Blocks production-impacting operations during demo mode.
  Redirects blocked operations to simulation alternatives.
  """

  @blocked_operations [
    :osint_query,
    :external_api_call,
    :email_send,
    :webhook_dispatch,
    :database_write,
    :file_export
  ]

  @spec check(atom(), map()) :: :allow | {:redirect, module(), atom(), list()}
  def check(operation, params) do
    if Prismatic.Demo.enabled?() and operation in @blocked_operations do
      {:redirect, Prismatic.Demo.SimulationEngine, :simulate, [operation, params]}
    else
      :allow
    end
  end

  @spec guard(atom(), map(), function()) :: term()
  def guard(operation, params, real_fn) do
    case check(operation, params) do
      :allow -> real_fn.()
      {:redirect, mod, fun, args} -> apply(mod, fun, args)
    end
  end
end

Integration points throughout the codebase use the guard:

# In an OSINT adapter
def execute_search(query, opts) do
  Prismatic.Demo.Guard.guard(:osint_query, %{query: query}, fn ->
    # Real OSINT query execution
    do_real_search(query, opts)
  end)
end

This pattern ensures that every external-facing operation passes through the guard. In production mode, the guard is a no-op (the check/2 function returns :allow immediately). In demo mode, it redirects to the simulation engine.

Simulation Engine

The simulation engine produces deterministic data for three built-in scenarios:

Scenario 1: Czech Company Investigation

A due diligence investigation on a fictional Czech company. The simulation produces ARES registry data, ownership chains, sanctions screening results, and adverse media findings. All data is pre-computed and stored in a static map, ensuring identical results across demo runs.

Scenario 2: Person Profile

A comprehensive person investigation with professional history, corporate affiliations, and digital footprint analysis. Demonstrates the platform's entity resolution and network visualization capabilities.

Scenario 3: Financial Analysis

A financial due diligence scenario with transaction analysis, related-party identification, and risk scoring. Demonstrates the decision engine's scoring and recommendation pipeline.

defmodule Prismatic.Demo.SimulationEngine do
  @moduledoc """
  Deterministic simulation engine for demo mode.
  Produces consistent fake data for three scenarios.
  """

  @spec simulate(atom(), map()) :: {:ok, map()} | {:error, term()}
  def simulate(operation, params) do
    scenario = Prismatic.Demo.scenario()
    data = load_scenario_data(scenario)

    case operation do
      :osint_query ->
        simulate_osint(data, params)

      :external_api_call ->
        simulate_api_response(data, params)

      :database_write ->
        simulate_write(data, params)

      :email_send ->
        {:ok, %{status: :simulated, message: "Email would be sent to #{params[:to]}"}}

      _ ->
        {:ok, %{status: :simulated, operation: operation}}
    end
  end

  defp simulate_osint(data, %{query: query}) do
    # Deterministic: same query always returns same results
    results = Map.get(data.osint_results, normalize_query(query), [])

    # Add realistic delay
    Process.sleep(Enum.random(200..800))

    {:ok, %{
      source: :demo,
      query: query,
      results: results,
      timestamp: DateTime.utc_now()
    }}
  end

  defp load_scenario_data(scenario) do
    path = Application.app_dir(:prismatic, "priv/demo/#{scenario}.json")

    case File.read(path) do
      {:ok, content} -> Jason.decode!(content, keys: :atoms)
      {:error, _} -> default_scenario_data()
    end
  end
end

Determinism

Determinism is essential. If a demo operator shows the platform on Monday and the client asks for a follow-up on Thursday, the demo must produce identical results. This rules out random data generation. Instead, all simulation data is pre-built and stored as JSON fixtures:

{
  "company": {
    "name": "Exemplum s.r.o.",
    "ico": "99887766",
    "address": "Vinohradska 42, Praha 2",
    "established": "2018-03-15"
  },
  "osint_results": {
    "exemplum": [
      {"source": "ares", "data": {"status": "active", "revenue": "45M CZK"}},
      {"source": "justice", "data": {"proceedings": 0, "liens": 1}},
      {"source": "sanctions", "data": {"matches": 0, "screened_lists": 14}}
    ]
  }
}

Demo LiveViews

Two dedicated LiveViews provide the demo experience:

  • /demo/showcase - Guided walkthrough of platform capabilities with scenario selection.
  • /demo/sandbox - Free-form exploration where all operations are simulated.

These routes are only mounted when demo mode is active:

# router.ex
if Prismatic.Demo.enabled?() do
  scope "/demo", PrismaticWebWeb.Demo do
    pipe_through [:browser, :demo_layout]

    live "/showcase", ShowcaseLive
    live "/sandbox", SandboxLive
  end
end

Safety Guarantees

The demo system provides four safety guarantees:

  1. No external side effects: All OSINT queries, API calls, emails, and webhooks are intercepted by the guard.
  2. No data contamination: Database writes in demo mode go to an in-memory store, not the production database.
  3. No route leakage: Demo routes only exist when the environment variable is set.
  4. No configuration drift: Demo mode cannot be toggled at runtime. It requires a process restart with the environment variable set, preventing accidental activation.
# This is intentionally NOT a runtime toggle
# Changing demo mode requires restarting the application
@compile {:inline, enabled?: 0}
def enabled? do
  Application.get_env(:prismatic, :demo_mode)[:enabled] == true
end

Testing the Demo

The demo system itself is tested with 35 tests covering:

  • Guard blocking for all operation types
  • Simulation engine determinism (same input = same output)
  • Route availability only when demo mode is active
  • LiveView rendering for all three scenarios
  • Edge cases: unknown operations, missing scenario data, malformed queries
describe "DemoModeGuard" do
  test "blocks OSINT queries in demo mode" do
    result = Guard.check(:osint_query, %{query: "test"})
    assert {:redirect, SimulationEngine, :simulate, _} = result
  end

  test "allows operations in production mode" do
    # Demo mode disabled in test config
    result = Guard.check(:osint_query, %{query: "test"})
    assert result == :allow
  end
end

The demo system reflects a broader principle: features that protect production integrity should be simple, explicit, and impossible to misconfigure. A single environment variable, a single guard module, and deterministic simulation data make the system easy to reason about and hard to break.