Memento: a friendlier Elixir API for the Mnesia database
Simple + Powerful interface to the Mnesia Distributed Database ๐พ
At a glance
- What is it?
- Memento wraps Erlang's Mnesia in an Elixir API that takes away most of the friction: tables are modules, queries are function calls, and every read and write goes through an explicit transaction. It has no external dependencies, it stays compatible with Mnesia itself, and at version 0.6 it is still changing shape.
- Who is it for?
- Memento suits an Elixir team that has decided to run on Mnesia, for reasons of distribution or real time reads, and wants an API that does not require learning MatchSpec and QLC before the first query. It is the wrong choice if you want a relational model, because Mnesia's tables and schemas are not that and the wrapper does not change it, and it is the wrong choice if you want to avoid writing any transaction discipline, since every query in Memento has to sit inside one.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 134 days ago.
- What is it written in?
- Mainly Elixir, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Mnesia with the awkward parts removed
Memento is an Elixir wrapper around Mnesia, the Erlang Distributed Realtime Database. The reason it exists is stated plainly in the project page: the original Mnesia API in Erlang is convoluted and unorganized, and combined with the complex MatchSpec and QLC query language it is hard to work with in Elixir, especially for beginners. Memento attempts to define a simple API to work with schemas, removing most of that complexity.
What sits underneath is a serious piece of infrastructure. Mnesia is a distributed, real time, transactional database that ships with the Erlang runtime, and the features Memento exposes are its features: extremely fast real time data searches even across many nodes, data that can easily be replicated on several nodes, a schema kept coherently on disc and in memory, and a series of operations grouped into a single atomic transaction.
Two of the listed properties are worth singling out. The first is atomicity, which is the property that makes it a database rather than a key value store, and Memento keeps it by requiring transactions rather than by offering an unsafe alternative. The second is Mnesia compatibility: you can still use Mnesia methods for schemas and tables created by Memento, so the wrapper is a layer rather than a wall.
And there are zero external dependencies. Only the built-in Mnesia module is used, which means a service that adopts it does not acquire a dependency tree.
The project is MIT licensed and described as free for personal and commercial use, and the documentation with examples for all methods is on HexDocs, with the package published on Hex as memento.
Tables are modules you define
The schema story is the part of Memento that is genuinely nicer than doing it by hand in Mnesia.
You start by defining a module as a Memento table by specifying its attributes, type and other options. At least two attributes are required, where the first one is the primary key of the table. A simple definition looks like this:
defmodule Blog.Author do
use Memento.Table, attributes: [:username, :fullname]
endA slightly more complex definition uses more options:
defmodule Blog.Post do
use Memento.Table,
attributes: [:id, :title, :content, :status, :author],
index: [:status, :author],
type: :ordered_set,
autoincrement: true
# You can also define other methods
# or helper functions in the module
endThe point of the second example is the index option. In Mnesia, deciding which attributes are indexed determines which queries can be answered without a table scan, and getting that wrong is the classic performance mistake with the raw API. Writing the index list next to the attributes, in the same module, makes it a decision you make once rather than a detail you remember later.
The type option chooses the table type, here an ordered set, and autoincrement generates keys. The comment at the end is also part of the design: the module is yours, so you can add helper functions and other methods to it, and they sit next to the schema rather than in a separate layer.
Once the schemas are defined they have to be created before you can interact with them, which is an explicit call rather than an implicit side effect of first use.
Every query has to live inside a transaction
This is the single rule that shapes how you write code against Memento, and it is enforced by the API rather than by convention.
An API for all of the read, write and delete operations is exposed in the Memento.Query module, but those methods can't be called directly. Instead, they must always be called inside a Memento.Transaction:
Memento.transaction! fn ->
Memento.Query.all(Blog.Author)
end
# => [
# %Blog.Author{username: :sye, fullname: "Sheharyar Naseer"},
# %Blog.Author{username: :jeanne, fullname: "Jeanne Bolding"},
# %Blog.Author{username: :pshore, fullname: "Paul Shore"},
# ]The basic operations are then ordinary function calls: all for every record in a table, read for a specific record by its primary key, write for a record, delete by primary key, and delete_record when you pass the full object.
There is a note in the documentation that this gets skipped in most examples for the sake of succinctness, but that transactions are still required. That is an honest warning, because the transaction wrapper is the thing that makes the API safe to use, and code written from examples that omit it will fail at runtime in a way that looks like a library bug.
For a team used to Ecto, the shape is familiar and the difference is instructive. Ecto gives you a changeset, a query DSL and a repository boundary. Memento gives you a transaction function and a set of calls that must live inside it, which is a smaller and more explicit contract, and one that maps directly onto what Mnesia actually guarantees.
select/3 replaces most of the MatchSpec surface
The query language is where the raw Mnesia API is hardest, and it is where Memento does the most work.
For more complex read operations, Memento exposes a select/3 method that lets you chain conditions using a simplified version of the Erlang MatchSpec. The documentation gives two examples for a Movie table.
Getting all movies named Rush:
Memento.Query.select(Movie, {:==, :title, "Rush"})And getting all movies directed by Tarantino before the year 2000:
guards = [
{:==, :director, "Quentin Tarantino"},
{:<, :year, 2000},
]
Memento.Query.select(Movie, guards)The simplification is the interesting part. A full MatchSpec expresses a match specification that Erlang's runtime can execute against a table, and it is a tuple language with several forms for filtering, joining and projecting. Memento keeps the guard part of it, the part that says this field equals that value or this field is less than that number, and wraps the rest.
For a single condition the call is a tuple. For several, the guards become a list and the same function takes them. The result is that a query which would be a page of MatchSpec in raw Mnesia is two lines in Elixir, and the syntax a beginner meets first looks like a comparison rather than a specification language.
The reference for the guard operators and further examples lives in the Query.select/3 documentation, and the wider query module documentation is linked from the same place.
Disk persistence is still the awkward part
The documentation is honest about the one part of Mnesia that Memento does not make easy.
Setting up disk persistence in Mnesia has always been a bit weird. It involves stopping the application, creating schemas on disk, restarting the application and then creating the tables with certain options. The steps are given explicitly:
# List of nodes where you want to persist
nodes = [ node() ]
# Create the schema
Memento.stop
Memento.Schema.create(nodes)
Memento.start
# Create your tables with disc_copies (only the ones you want persisted on disk)
Memento.Table.create!(TableA, disc_copies: nodes)
Memento.Table.create!(TableB, disc_copies: nodes)
Memento.Table.create!(TableC)Note the split at the end. Tables created with disc_copies for the listed nodes are persisted on disk, and TableC is created without it, so it lives in memory. That per table choice is the mechanism Memento provides for the mixed workload, a hot index in memory and durable records on disc, and it is worth being deliberate about which tables land on which side.
The configuration step is separate and equally important. It is highly recommended that a custom path to the Mnesia database location is specified, even on the local dev environment, and the documentation suggests adding .mnesia to your .gitignore:
# config/config.exs
config :mnesia,
dir: '.mnesia/#{Mix.env}/#{node()}' # Notice the single quotesThe comment about the single quotes is not cosmetic. The path interpolates the Mix environment and the node name, and the quotes are what keep the interpolation from being evaluated at compile time instead of being passed through to Mnesia.
The supervisor detail that decides whether it starts
There is one instruction in the installation section that deserves more attention than it gets.
It is preferable to only add :memento and not :mnesia along with it. This will ensure that OTP calls to Mnesia go through the Supervisor spec specified in Memento.
That is the whole difference between a working application and one whose tables are mysteriously empty or whose schema was created by a different supervisor than the one your code expects. Mnesia is started by an OTP application, and if your application list includes :mnesia directly, the runtime may start Mnesia with its own default specification rather than the one Memento sets up, which means the schema and the tables do not end up where Memento expects them.
Adding the dependency itself is short. Add :memento to your list of dependencies in your Mix file:
def deps do
[{:memento, "~> 0.6.0"}]
endAnd if your Elixir version is 1.3 or lower, also add it to your applications list:
def application do
[applications: [:memento]]
endThe Elixir 1.3 caveat dates the library's origins. It also explains the more general point: this is a wrapper for an Erlang database, and the Erlang conventions about application supervision still apply underneath it.
The repository reflects an ordinary Elixir project with a mix.exs and mix.lock, a config directory, a lib directory, a test directory, a .formatter.exs, an .iex.exs for the interactive shell and a .tool-versions file for the version manager.
Editorial conclusion
Memento suits an Elixir team that has decided to run on Mnesia, for reasons of distribution or real time reads, and wants an API that does not require learning MatchSpec and QLC before the first query. It is the wrong choice if you want a relational model, because Mnesia's tables and schemas are not that and the wrapper does not change it, and it is the wrong choice if you want to avoid writing any transaction discipline, since every query in Memento has to sit inside one. Verify three things before you adopt it: that you are comfortable with Mnesia's own model underneath, that the version you pin matches the API you write against given that 0.5 and 0.6 both shipped on the same day in June 2026, and that your disk path configuration is right in every environment, because getting that wrong means starting again with the data.
Frequently asked questions
What is Memento?
It is an Elixir wrapper around Mnesia, the Erlang Distributed Realtime Database, providing a simple and intuitive API for schemas, queries and transactions. It has zero external dependencies, uses only the built-in Mnesia module, and is MIT licensed for commercial use.
How do I add Memento to a Mix project?
Add it to your dependencies with a def deps block containing {:memento, "~> 0.6.0"}. The documentation asks you to add only :memento and not :mnesia, so that OTP calls to Mnesia go through the supervisor specification Memento provides, and on Elixir 1.3 or lower you also add :memento to your applications list.
Why do Memento queries have to run inside a transaction?
The methods in Memento.Query cannot be called directly, they must always be called inside a Memento.Transaction, which is what gives the group of operations atomicity. The documentation notes that transactions are omitted in most examples for brevity but are still required.
How do I write a conditional query in Memento?
You use the select/3 method with a simplified version of the Erlang MatchSpec. A single condition is a tuple such as {:==, :title, "Rush"}, and several conditions are passed as a list of guards, for example a match on director together with a comparison on year.
How do I make Memento tables persistent on disk?
Stop the application, create the schema with Memento.Schema.create for the nodes you want, start the application again, then create the tables with disc_copies for those nodes. Tables created without disc_copies stay in memory, and you should also set a custom Mnesia dir in your config with .mnesia added to your gitignore.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sheharyarn-memento)Was this useful?