Flop turns query parameters into filtered, sorted and paginated Ecto queries
Filtering, ordering and pagination for Ecto
At a glance
- What is it?
- Flop is an Elixir library that applies filtering, ordering and pagination to Ecto queries, with allow-lists of filterable and sortable fields, both offset and keyset pagination, and Relay style connections. The security model depends on the schema configuration you write, and the MySQL caveats are specific enough to plan around.
- Who is it for?
- Flop suits a Phoenix application that exposes list endpoints with user supplied filter and sort parameters, since the allow-list mechanism and the validation step are what keep a query builder from becoming a way to read arbitrary columns. It is the wrong choice if you need full text search or ad hoc reporting, and MySQL users should read the collation note before enabling cursor pagination.
- 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 6 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
The problem is user supplied query parameters
Flop applies filtering, ordering and pagination to Ecto queries. The reason it exists is a specific and common problem: a list endpoint in a web application usually takes query parameters from the browser, and those parameters decide which records come back and in what order.
Doing that by hand means writing filter parsing, sort parsing, limit and offset handling, plus a metadata struct carrying the total count and the page information. Doing it unsafely means letting a query parameter name a database column, which lets a caller read columns you never meant to expose.
Flop's answer is to make the safe path the short one. Its feature list names offset pagination with offset and limit or page and page_size parameters, cursor pagination in the keyset style with first and after and last and before, the same arguments Relay uses. Sorting works across several fields, each with its own direction, with nulls first or last. Filtering takes a list of filters, each with a field, an operator and a value.
The library is MIT licensed, published on Hex as flop, and its topics are ecto, elixir, filter, ordering and pagination.
Allow-lists are what make it safe to accept from a browser
The design point worth understanding is the allow-list. The README is explicit that only fields configured as filterable or sortable can be used, and that using Flop.Schema ensures that filtering and sorting parameters are applied only to the fields you have explicitly configured.
The configuration lives on the Ecto schema itself:
defmodule MyApp.Pet do
use Ecto.Schema
use Flop.Schema
@flop_options [
filterable: [:name, :species],
sortable: [:name, :age, :species]
]
schema "pets" do
field :name, :string
field :age, :integer
field :species, :string
field :social_security_number, :string
end
endThe example is chosen well. The schema has a social_security_number field, and it is in neither list, so a caller who asks to filter or sort by it gets an error rather than that column.
The step is described as optional but highly recommended, particularly when the parameters come from users. The counter-case is stated too: if you have not set up Flop.Schema, the for option can be omitted, but that is not recommended unless all parameters are generated internally and are guaranteed to be safe. That is a fair framing of where the risk actually sits.
Validation and execution, together or apart
The main entry point is Flop.validate_and_run/3, which validates the parameters, fetches the data and returns the pagination metadata in one operation:
def list_pets(params \\ %{}) do
Flop.validate_and_run(Pet, params, for: Pet)
endThe return value is an ok tuple whose second element is the data and the metadata:
{:ok, {[%Pet{}], %Flop.Meta{}}}The alternative is to split the work. Flop.validate/2, Flop.validate!/2 and Flop.run/3 let you validate first, then manipulate the validated parameters, modify the query depending on those parameters, or move validation to a different layer of the application:
with {:ok, flop} <- Flop.validate(params, for: Pet) do
Flop.run(Pet, flop, for: Pet)
endThere is a trap here, and the documentation points at it directly. The lower level functions Flop.all/3, Flop.meta/3 and Flop.count/3 do not validate. They are the right choice for advanced requirements, but if the parameters are generated from user input they must always be validated first with Flop.validate/2 or Flop.validate!/2 to ensure safe execution.
That distinction is the one to remember when you read other people's Flop code. A function that calls Flop.all directly may be perfectly safe if the parameters were built by the server, and unsafe if they came from a query string.
Custom fields, joins and compound fields
Beyond the basic filter and sort, the schema configuration supports several field types that cover cases the default ones do not.
Join fields let you filter and sort by fields of joined tables, and the library only adds the joins a given request actually needs, which matters when a query could otherwise pull in a wide join for a single sort key. Compound fields let you filter several string fields as one, for example a full name assembled from a first and last name. Custom fields let you supply your own filter or order expression for anything the other field types do not cover. Alias fields are also supported, along with maximum and default limits.
There is also a companion package, Flop Phoenix, which provides pagination links, sortable tables and filter forms. That separation is sensible: the core library is about turning parameters into a query, and the Phoenix package is about the view layer that produces those parameters in the first place.
Relay connections are supported in the core, giving edges, nodes and page info in the Relay format, which is what makes the cursor pagination arguments first, after, last and before natural to use here.
MySQL needs a collation and loses some index usage
The database support section is short but the MySQL part is the section to read carefully.
Flop builds queries with Ecto.Query and only uses a couple of fragments to bridge the gap between databases, and the tested set is Postgres, MySQL and SQLite, all supported and tested in CI. Other Ecto adapters might work, but they are not tested, and some behaviour depends on the adapter in use.
For MySQL there is a configuration requirement with a concrete failure mode. You have to configure the repo with the charset and collation your columns use, or cursor pagination over a string column raises an Illegal mix of collations error:
config :my_app, MyApp.Repo,
charset: "utf8mb4",
collation: "utf8mb4_0900_ai_ci"There is also a performance note. MySQL cannot use an index for the array operators, which compile to JSON_CONTAINS, or for the asc_nulls_last and desc_nulls_first order directions, because those add an IS NULL sort key, MySQL having neither NULLS FIRST nor NULLS LAST. So two of Flop's features quietly cost you an index scan on MySQL while keeping it on Postgres.
One more MySQL specific gap: the array type with a binary_id does not work, because the adapter writes a binary_id as raw bytes and the JSON encoder rejects them. The README points at Flop.Filter for the array operators in that case.
Versions, diagnostics and where the work happens
The install is a dependency entry, and the version in the README matches the newest release, 0.29.1:
def deps do
[
{:flop, "~> 0.29.1"}
]
endDiagnostics are opt-in, enabled in config/dev.exs and config/test.exs:
config :flop, diagnostics: trueand a default repo can be set globally, or you can avoid global configuration entirely by using a configuration module or by passing the repo directly to the functions. The three functions that take a repo are validate_and_run, all and meta, and each has a repo option.
On compatibility, the package is tested against the Elixir and OTP versions that are still supported upstream. Older versions down to the requirement in mix.exs may still work, but they are not covered by CI and are not officially supported.
The release history shows regular work: 0.29.1 on 2026-10-17, 0.29.0 on 2026-09-03 and 0.28.0 on 2026-08-24, with the repository not archived and the last push on 2026-10-17. The project also keeps a SECURITY.md, a CHANGELOG.md, a .credo.exs for static analysis, a .dialyzer_ignore.exs, and a compose.yaml with Postgres 18 Alpine and MySQL 9.7 for local testing.
Editorial conclusion
Flop suits a Phoenix application that exposes list endpoints with user supplied filter and sort parameters, since the allow-list mechanism and the validation step are what keep a query builder from becoming a way to read arbitrary columns. It is the wrong choice if you need full text search or ad hoc reporting, and MySQL users should read the collation note before enabling cursor pagination. Before adopting it, add flop to mix.exs, enable diagnostics in dev and test, declare Flop.Schema on the schemas you expose, and confirm that your Ecto and OTP versions are still supported upstream.
Frequently asked questions
How do I add Flop to an Elixir project?
Add the dependency to mix.exs with {:flop, "~> 0.29.1"}. You can also enable diagnostics in config/dev.exs and config/test.exs with config :flop, diagnostics: true, and set a default repo with config :flop, repo: MyApp.Repo.
How does Flop stop users from querying columns I did not expose?
You declare the allowed fields on the schema with Flop.Schema, using filterable and sortable options. The README recommends this step particularly when parameters come from users, because only configured fields can be filtered or sorted on, and the example schema includes a field that is in neither list.
What are the MySQL limitations of Flop?
The repo must be configured with the charset and collation your columns use, or cursor pagination over a string column raises Illegal mix of collations. MySQL also cannot use an index for the array operators, which compile to JSON_CONTAINS, or for the asc_nulls_last and desc_nulls_first order directions, and the array type with a binary_id does not work.
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/woylie-flop)Was this useful?