These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Elixir?
Elixir is a dynamic, functional programming language designed for building scalable and maintainable applications. Elixir runs on the Erlang Virtual Machine (BEAM) and uses Erlang’s robust concurrency and fault-tolerance features.
Elixir is ideal for systems that require high availability, real-time processing, and concurrency, making it popular for web development, distributed systems, and telecommunications. Elixir’s syntax is clean and easy to learn, while its built-in support for concurrency allows developers to handle many tasks simultaneously without sacrificing performance. With a strong focus on developer productivity and reliability, Elixir is a powerful choice for modern applications that demand scalability.
Why Integrate Elixir with Strapi
Integrating Elixir with Strapi creates a powerful foundation for modern web applications, combining Strapi's intuitive content management capabilities with Elixir's scalability and fault tolerance. This integration effectively addresses complementary needs: Strapi offers a user-friendly admin panel and flexible APIs, while Elixir excels in high-concurrency environments, making it perfect for content-heavy applications that need to scale.
Elixir’s functional programming approach ensures stable, predictable applications. Elixir’s immutable data structures and actor-based concurrency allow efficient handling of millions of concurrent users, ensuring high availability. When paired with Strapi’s flexible APIs (REST and GraphQL), Elixir’s concurrent processing capabilities shine, allowing multiple API requests to be processed in parallel. This setup is particularly useful for managing complex content relationships and aggregating data from multiple sources.
Strapi’s API flexibility, combined with Elixir’s fault-tolerant architecture, creates a robust system where failures are automatically managed, ensuring content delivery remains uninterrupted. The coexistence of REST and GraphQL APIs in Strapi further enhances performance, offering developers the flexibility to optimize for specific needs.
Elixir’s Phoenix framework outperforms traditional frameworks like Ruby on Rails in high-concurrency scenarios, improving user experience and reducing infrastructure costs. Together, Strapi and Elixir offer a seamless, scalable solution that optimizes both content management and delivery performance.
How to Integrate Elixir with Strapi
Combining Strapi, an open-source Node.js Headless CMS, with your Elixir application gives you modern content management with Elixir's performance and concurrency. Let's walk through setting up a production-ready integration.
Prerequisites and Environment Setup
You'll need Node.js 18+ for Strapi with Yarn or npm. For Elixir, use version 1.15+ with Phoenix 1.7+ for web applications. Set up PostgreSQL or SQLite for your database.
Install HTTPoison for API communication in your Elixir project:
defp deps do
[
{:httpoison, "~> 2.0"},
{:jason, "~> 1.4"}
]
endConfigure environment variables for API keys and database credentials from the start. Never hardcode sensitive values.
Keep in touch with the latest Strapi and Elixir updates
Creating a Strapi Project
Create a new Strapi project with the CLI:
npx create-strapi-app@latest my-cms-project --quickstartFor production-like development, use Docker Compose:
version: '3'
services:
strapi:
image: strapi/strapi
environment:
DATABASE_CLIENT: postgres
DATABASE_HOST: db
DATABASE_PORT: 5432
DATABASE_NAME: strapi
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: strapi
volumes:
- ./app:/srv/app
ports:
- '1337:1337'
depends_on:
- db
db:
image: postgres:13
environment:
POSTGRES_DB: strapi
POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi
volumes:
- strapi-data:/var/lib/postgresql/data
volumes:
strapi-data:Admin Setup and Access Token Creation
Navigate to http://localhost:1337/admin and create your administrator account. Go to Settings > API Tokens > Create new API Token. Use "Full access" for development, but implement Strapi's user roles and permissions for production.
Store the token in your environment:
export STRAPI_API_TOKEN="your_generated_token_here"
export STRAPI_BASE_URL="http://localhost:1337"Content Modeling in Strapi
Design content types that align with your Elixir application's data structures. Use the Content-Type Builder to create models. For a blog, create an "Article" content type with title, content, author, and publication date fields.
Create reusable components for content that appears across multiple types. Follow API design best practices by grouping related fields and avoiding deeply nested structures.
Connecting Elixir to Strapi APIs
Create a dedicated module to integrate Elixir with Strapi:
defmodule MyApp.StrapiClient do
@base_url System.get_env("STRAPI_BASE_URL")
@api_token System.get_env("STRAPI_API_TOKEN")
def get_articles do
"/api/articles"
|> build_url()
|> HTTPoison.get!(headers())
|> handle_response()
end
def get_article(id) do
"/api/articles/#{id}"
|> build_url()
|> HTTPoison.get!(headers())
|> handle_response()
end
defp build_url(path), do: @base_url <> path
defp headers do
[
{"Authorization", "Bearer #{@api_token}"},
{"Content-Type", "application/json"}
]
end
defp handle_response(%HTTPoison.Response{status_code: 200, body: body}) do
Jason.decode!(body)
end
defp handle_response(%HTTPoison.Response{status_code: status_code}) do
{:error, "API request failed with status #{status_code}"}
end
endKeep in touch with the latest Strapi and Elixir updates
Build a Complete Elixir-Strapi News Platform
This news platform demonstrates production-ready integration patterns between Phoenix and Strapi. The architecture handles thousands of concurrent readers while maintaining sub-100ms response times through strategic caching and Elixir's actor model. When building such platforms, it's important to consider critical factors, much like when choosing a CMS for e-commerce, to ensure scalability, performance, and maintainability.
Architecture Overview
The platform combines Strapi for Phoenix's headless CMS capabilities with Phoenix's real-time features. Content editors manage articles through Strapi's admin interface while readers experience fast page loads and live content updates.
Core Integration Features:
- Secure Authentication: JWT-based API communication with token rotation
- Intelligent Caching: Multi-layer Redis caching that reduces Strapi API calls by 85%
- Real-time Content: Phoenix Channels broadcast new articles instantly to active readers
- Fault Tolerance: Circuit breakers and exponential backoff prevent cascade failures
- Content Preloading: Background processes warm caches before traffic spikes
Critical Implementation Details
The StrapiClient module abstracts all CMS communication, facilitating the integration of Elixir with Strapi:
defmodule NewsApp.StrapiClient do
@strapi_url System.get_env("STRAPI_URL")
@jwt System.get_env("STRAPI_JWT")
def fetch_articles do
HTTPoison.get!(
"#{@strapi_url}/api/articles?populate=*",
[{"Authorization", "Bearer #{@jwt}"}],
recv_timeout: 5_000
)
|> handle_response()
|> cache_articles()
end
endJWT authentication implementation with automatic token rotation ensures secure API communication:
defmodule NewsApp.Auth.TokenManager do
use GenServer
alias NewsApp.Auth.JwtClient
# Client API
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{token: nil, expires_at: nil}, name: __MODULE__)
end
def get_valid_token do
GenServer.call(__MODULE__, :get_token)
end
# Server callbacks
def init(state) do
# Fetch initial token on startup
{:ok, refresh_token(state)}
end
def handle_call(:get_token, _from, %{token: token, expires_at: exp} = state) do
now = DateTime.utc_now() |> DateTime.to_unix()
# Refresh token if it expires in less than 5 minutes
state = if exp - now < 300, do: refresh_token(state), else: state
{:reply, state.token, state}
end
defp refresh_token(_state) do
# Get new token from Strapi
{:ok, %{token: token, expires_in: expires_in}} = JwtClient.fetch_token()
expires_at = DateTime.utc_now() |> DateTime.add(expires_in, :second) |> DateTime.to_unix()
# Schedule token refresh before expiration
Process.send_after(self(), :refresh_token, (expires_in - 300) * 1000)
%{token: token, expires_at: expires_at}
end
endA dedicated GenServer polls Strapi every 10 minutes for fresh content. When new articles publish, Phoenix Channels notify subscribers immediately:
defmodule NewsApp.ContentSupervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def init(_init_arg) do
children = [
{NewsApp.ContentPoller, poll_interval: 10 * 60 * 1000},
{NewsApp.CacheWarmer, schedule: [hour: [8, 12, 16]]}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
defmodule NewsApp.ContentPoller do
use GenServer
alias NewsApp.StrapiClient
alias NewsApp.Endpoint
# Client API
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
# Server callbacks
def init(opts) do
schedule_poll(0) # Poll immediately on startup
{:ok, %{poll_interval: opts[:poll_interval]}}
end
def handle_info(:poll, state) do
# Poll for new content
with {:ok, articles} <- StrapiClient.fetch_articles() do
# Find new articles by comparing with previous fetch
new_articles = find_new_articles(articles)
# Broadcast to Phoenix channels if new articles exist
if length(new_articles) > 0 do
Endpoint.broadcast("content:updates", "new_articles", %{articles: new_articles})
end
end
schedule_poll(state.poll_interval)
{:noreply, state}
end
defp schedule_poll(interval) do
Process.send_after(self(), :poll, interval)
end
endThe Redis-based caching system reduces API load and speeds up content delivery:
defmodule NewsApp.Cache do
alias NewsApp.Redis
@default_ttl 60 * 60 * 2 # 2 hours in seconds
def get_articles(filters \\ nil) do
cache_key = build_key("articles", filters)
case Redis.get(cache_key) do
{:ok, nil} ->
# Cache miss - fetch from Strapi and cache result
articles = NewsApp.StrapiClient.fetch_articles(filters)
set_articles(articles, filters)
articles
{:ok, data} ->
# Cache hit
Jason.decode!(data)
{:error, reason} ->
# Redis error - fallback to direct API call
Logger.error("Redis cache error: #{inspect(reason)}")
NewsApp.StrapiClient.fetch_articles(filters)
end
end
def set_articles(articles, filters \\ nil) do
cache_key = build_key("articles", filters)
Redis.set(cache_key, Jason.encode!(articles), ex: @default_ttl)
end
def invalidate_articles(article_id \\ nil) do
if article_id do
# Invalidate specific article
Redis.del(build_key("articles", %{id: article_id}))
else
# Invalidate all articles using pattern matching
Redis.eval("return redis.call('DEL', unpack(redis.call('KEYS', ARGV[1])))",
0, "articles:*")
end
end
defp build_key(resource, nil), do: "#{resource}:all"
defp build_key(resource, filters) when is_map(filters) do
filter_string = filters
|> Enum.map(fn {k, v} -> "#{k}:#{v}" end)
|> Enum.join(":")
"#{resource}:#{filter_string}"
end
endThe system implements circuit breakers to prevent cascade failures when Strapi is unavailable:
defmodule NewsApp.CircuitBreaker do
use GenServer
@timeout 5_000 # Circuit reset timeout
@threshold 5 # Number of failures before opening circuit
@retry_window 60_000 # Time window for retry after circuit opens
# Client API
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def call(service, func, args) do
case GenServer.call(__MODULE__, {:check, service}) do
:ok ->
try do
result = apply(func, args)
GenServer.cast(__MODULE__, {:success, service})
{:ok, result}
rescue
e ->
GenServer.cast(__MODULE__, {:failure, service})
{:error, e}
end
{:error, :circuit_open} ->
{:error, :service_unavailable}
end
end
# Server callbacks
def init(_) do
{:ok, %{circuits: %{}}}
end
def handle_call({:check, service}, _from, state) do
circuit = Map.get(state.circuits, service, %{status: :closed, failures: 0, last_failure: nil})
response = case circuit.status do
:closed -> :ok
:open ->
now = System.monotonic_time(:millisecond)
last_failure = circuit.last_failure || 0
if now - last_failure > @retry_window do
# Try half-open state
:ok
else
{:error, :circuit_open}
end
end
{:reply, response, state}
end
def handle_cast({:success, service}, state) do
circuits = Map.update(state.circuits, service, %{status: :closed, failures: 0}, fn circuit ->
%{circuit | status: :closed, failures: 0}
end)
{:noreply, %{state | circuits: circuits}}
end
def handle_cast({:failure, service}, state) do
now = System.monotonic_time(:millisecond)
circuits = Map.update(state.circuits, service,
%{status: :closed, failures: 1, last_failure: now},
fn circuit ->
failures = circuit.failures + 1
status = if failures >= @threshold, do: :open, else: circuit.status
%{circuit |
status: status,
failures: failures,
last_failure: now}
end)
{:noreply, %{state | circuits: circuits}}
end
endStrapi Open Office Hours
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation and the Elixir documentation.