How Plausible Analytics Generates and Validates API Tokens for External Integrations
Plausible Analytics uses SHA-256 hashed tokens with human-readable prefixes, transmitted via HTTP Basic authentication, where raw tokens are revealed only once during creation and validated through constant-time hash comparison.
The plausible/analytics repository implements a lightweight, token-based authentication scheme specifically designed for third-party integrations such as WordPress plugins and Zapier. This system generates cryptographically secure tokens in lib/plausible/plugins/api/token.ex, stores only their hashes in the database, and validates incoming requests through a dedicated Phoenix plug that minimizes write operations to the plugins_api_tokens table.
Token Generation and Secure Hash Storage
Token creation begins in lib/plausible/plugins/api/token.ex, where the generate/1 function produces a cryptographically random byte string and prefixes it with plausible-plugin (or plausible-plugin-<env> in non-production environments) to aid secret-scanning tools. This raw token is immediately hashed using SHA-256 via Erlang's :crypto.hash/2, ensuring that compromised database access cannot reveal functional tokens.
# lib/plausible/plugins/api/token.ex
def generate(random_bytes \\ random_bytes()) do
raw = prefixed(random_bytes) # → "plausible-plugin-<rand>"
hash = :crypto.hash(:sha256, raw) # stored hash
%{raw: raw, hash: hash}
end
The high-level create/3 function in lib/plausible/plugins/api/tokens.ex orchestrates persistence. It accepts a %Site{} struct, description, and the generated token map, returning a tuple containing the database record and the raw token string. This raw value is displayed exactly once in the LiveView UI (lib/plausible_web/live/plugins/api/token_form.ex) and never persisted or transmitted again.
# lib/plausible/plugins/api/tokens.ex
def create(%Site{} = site, description, generated_token \\ Token.generate()) do
changeset = Token.insert_changeset(site, generated_token, %{description: description})
case Repo.insert(changeset) do
{:ok, saved_token} -> {:ok, saved_token, generated_token.raw}
end
end
Token Extraction from HTTP Requests
External integrations authenticate via the Authorization: Basic header. The AuthorizePluginsAPI plug in lib/plausible_web/plugs/authorize_plugins_api.ex extracts credentials using Base64 decoding, accepting either user:token or standalone token formats.
# lib/plausible_web/plugs/authorize_plugins_api.ex
defp extract_token(conn) do
with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
{:ok, decoded} <- Base.decode64(encoded) do
case :binary.split(decoded, ":") do
[_user, token] -> {:ok, token}
[token] -> {:ok, token}
end
else
_ -> {:unauthorized, conn}
end
end
Missing or malformed headers immediately return 401 Unauthorized responses, rejecting the request before it reaches the controller.
Token Validation and Last-Seen Tracking
Validation occurs through constant-time hash comparison in Tokens.find/1, which queries the plugins_api_tokens table using the SHA-256 hash of the supplied raw token and preloads the associated Site. Upon successful lookup, the system updates the last_used_at timestamp only if the previous value exceeds 5 minutes old, significantly reducing database write churn for high-traffic integrations.
# lib/plausible_web/plugs/authorize_plugins_api.ex
defp authorize(conn, token_value) do
case Tokens.find(token_value) do
{:ok, token} ->
{:ok, token} = Tokens.update_last_seen(token)
{:ok, Plug.Conn.assign(conn, :authorized_site, token.site)}
{:error, :not_found} -> {:unauthorized, conn}
end
end
The update_last_seen/2 logic in lib/plausible/plugins/api/tokens.ex implements this 5-minute throttle, ensuring administrators retain usage visibility without overwhelming the database with timestamp updates on every API call.
Practical Implementation Examples
Creating Tokens Programmatically
Generate tokens from Mix tasks or administrative scripts using the internal API:
site = Plausible.Repo.get_by!(Plausible.Site, domain: "example.com")
{:ok, token, raw} = Plausible.Plugins.API.Tokens.create(site, "Zapier integration")
IO.puts("Store this token safely – it will never be shown again:")
IO.puts(raw)
Authenticating External Requests
Send requests with the raw token as the Basic auth password (username is ignored):
RAW_TOKEN="plausible-plugin-abc123def456..."
curl -u ":${RAW_TOKEN}" \
-H "Accept: application/json" \
https://plausible.io/api/v1/sites/example.com/stats
Revoking Tokens
Delete tokens through the UI or directly via Tokens.delete/2, which removes entries matching both the site ID and token ID:
# lib/plausible/plugins/api/tokens.ex
def delete(site, token_id) do
Repo.delete_all(from(t in Token, where: t.site_id == ^site.id and t.id == ^token_id))
:ok
end
Summary
- SHA-256 hashing: Raw tokens are hashed before storage in the
plugins_api_tokenstable; only hashes persist in the database. - One-time display: The unhashed token appears only once during creation in
Plausible.Plugins.API.Tokens.create/3, after which it cannot be retrieved. - HTTP Basic auth: The
AuthorizePluginsAPIplug extracts tokens fromAuthorization: Basicheaders, supporting bothuser:tokenand standalone token formats. - Throttled metadata updates: The
last_used_attimestamp updates only every 5 minutes to prevent database overload while tracking activity. - Site scoping: Validated tokens automatically preload their associated
Siteintoconn.assigns.authorized_sitefor downstream use.
Frequently Asked Questions
What hashing algorithm does Plausible use for API token storage?
Plausible uses SHA-256 cryptographic hashing implemented via Erlang's :crypto.hash/2 function. The raw token is hashed immediately in Token.generate/1 and only the resulting hash is stored in the database, ensuring that database breaches do not compromise active tokens.
How does Plausible minimize database load when tracking API token usage?
The system implements a 5-minute throttle in Tokens.update_last_seen/2. When a token is used for validation, the function compares the current time against the existing last_used_at timestamp and only performs a database write if more than 5 minutes have elapsed since the last update. This prevents write amplification from high-frequency API integrations.
Can I retrieve a raw API token after creating it?
No. Plausible follows a "show once" security model where Tokens.create/3 returns the raw token only during the initial creation phase. This value is displayed in the LiveView modal (token_form.ex) and then discarded from memory; subsequent database queries only return the SHA-256 hash, making it impossible to leak raw tokens through API responses or database dumps.
Why does the token have a readable prefix?
The plausible-plugin prefix (added via Token.prefixed/1) serves dual purposes: it aids secret-scanning tools like GitHub Secret Scanner in identifying accidentally exposed tokens, and it allows environment-specific variants (e.g., plausible-plugin-dev) for staging environments, configured in Token.prefix/0.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →