ExpirableStore

CI

Lightweight expirable value store for Elixir with cluster-wide or local scoping.

Perfect for caching OAuth tokens, API keys, and other time-sensitive data that shouldn't be repeatedly refreshed.

Features

Installation

def deps do
  [{:expirable_store, "~> 0.5.0"}]
end

Quick Example

defmodule MyApp.Expirables do
  use ExpirableStore

  # Cluster-scoped, lazy refresh
  expirable :github_access_token do
    fetch fn -> GitHubOAuth.fetch_access_token() end
    scope :cluster
    refresh :lazy
  end

  # Node-local, eager refresh (30s before expiry)
  expirable :datadog_agent_token do
    fetch fn -> DatadogAgent.fetch_local_token() end
    scope :local
    refresh {:eager, before_expiry: :timer.seconds(30)}
  end

  # Never expires (cached until explicitly cleared)
  expirable :static_config do
    fetch fn -> {:ok, load_config(), :infinity} end
  end

  # Keyed: independent cache and timer per GitHub App installation
  expirable :github_installation_token do
    fetch fn installation_id ->
      {:ok, GitHubApp.get_installation_token(installation_id), System.system_time(:millisecond) + :timer.hours(1)}
    end
    keyed true
    scope :cluster
    refresh {:eager, before_expiry: :timer.minutes(5)}
  end
end
# Non-keyed: named functions (recommended)
{:ok, token, expires_at} = MyApp.Expirables.github_access_token()
token = MyApp.Expirables.github_access_token!()

# Non-keyed: generic functions
{:ok, token, _} = MyApp.Expirables.fetch(:github_access_token)
MyApp.Expirables.clear(:github_access_token)
MyApp.Expirables.clear_all()

# Keyed: pass a runtime key
{:ok, token, _} = MyApp.Expirables.github_installation_token(123)
token = MyApp.Expirables.github_installation_token!(123)
MyApp.Expirables.clear(:github_installation_token, 123)  # clear specific key
MyApp.Expirables.clear(:github_installation_token)       # clear all keys

DSL Options

Option Values Default Description
fetchfn -> {:ok, value, expires_at} | :error endrequired Fetch function. Use 1-arity when keyed: true. expires_at is Unix timestamp in ms or :infinity
keyedtrue, falsefalse When true, each unique key gets its own independent cache entry and timer
refresh:lazy, {:eager, before_expiry: ms}:lazy Refresh strategy
scope:cluster, :local:cluster Scope of the store

Refresh Strategies

Scope Options

Keyed Expirables

When keyed: true, the fetch function receives the key as its argument. Each unique key has its own independent Agent and refresh timer. The key can be any Erlang term:

MyApp.Expirables.github_installation_token(123)        # integer
MyApp.Expirables.github_installation_token(:tenant_a)  # atom
MyApp.Expirables.github_installation_token({:org, 42}) # tuple

Generated Functions

For expirable :name (keyed: false):

For expirable :name (keyed: true):

Generic functions (always available):

Key Behaviors

When to Use

Good for lightweight, time-sensitive data: OAuth tokens, API keys, FX rates, per-tenant credentials.

Not recommended for: high-traffic scenarios, large values.

Development

mix test        # single-node tests
mix test.all    # includes distributed multi-node tests