The definitive Rails-native DataSource backend.
Define a Resource once and get filtering, sorting, searching, pagination, authorization, serialization, schema endpoints, dynamic form metadata, and broadcasting — all without writing boilerplate controllers.
Works with any frontend: Stimulus · React · Vue · Alpine · HTMX · Turbo · TanStack Table · Kendo UI
Developers should never manually write pagination, filtering, sorting, searching, includes, serialization, or authorization inside every controller.
# Define once
class UserResource < ActiveDataSource::Resource
model User
searchable :name, :email
sortable :name, :created_at
filterable :status, :organization_id
includable :organization, :roles
serializer UserSerializer
field :status do
type :select
options User.statuses.keys
required true
end
default_sort :created_at, direction: :desc
end
# Use everywhere
class UsersController < ApplicationController
include ActiveDataSource::Controller
resource UserResource
endThat's all. Every standard REST action is provided automatically.
| Feature | Description |
|---|---|
| 🎛 Resource DSL | Declare filters, sorts, search, includes, fields, relations, auth, serializer in one place |
| 🔍 30+ Filter Operators | eq, contains, between, in, null, today, this_week, nested associations, custom scopes |
| 📄 Pagination | Pagy, Kaminari, WillPaginate, cursor-based, offset |
| 🔒 Authorization | Pundit, CanCanCan, attribute-level permissions, policy_scope auto-applied |
| 🏢 Multi-Tenancy | acts_as_tenant, Apartment, automatic scoping |
| 🔎 Search | Ransack, Searchkick, PgSearch, Meilisearch |
| 📦 Serialization | Blueprinter, jsonapi-serializer, AMS, Jbuilder, plain JSON |
| 📡 Broadcasting | Turbo Streams + ActionCable after create/update/destroy |
| 📋 Schema Endpoint | GET /schema for dynamic forms, tables, and AI interfaces |
| 🚀 Bulk Actions | bulk_update, bulk_delete built-in |
| 📤 Export | CSV export with one DSL flag |
| 🧩 TypeScript Client | Fluent DataSource and QueryBuilder for any frontend |
| 🧪 Testing Helpers | RSpec matchers: be_filterable_by, be_sortable_by, allow_including |
| 🔌 Pluggable | Every adapter, parser, responder, and pipeline step is replaceable |
# Gemfile
gem "active_data_source"
# Optional adapters — only install what you use
gem "pagy" # pagination
gem "ransack" # filtering + search
gem "pundit" # authorization
gem "blueprinter" # serialization
gem "acts_as_tenant"bundle install# config/initializers/active_data_source.rb
ActiveDataSource.configure do |config|
config.pagination = :pagy # :pagy | :kaminari | :will_paginate
config.search = :ransack # :ransack | :searchkick | :pg_search
config.authorization = :pundit # :pundit | :cancancan | :none
config.serializer = :blueprinter # :blueprinter | :jsonapi_serializer | :active_model_serializer | :none
config.multi_tenancy = :acts_as_tenant # :acts_as_tenant | :apartment | :none
config.response_format = :json # :json | :json_api | :graphql
config.default_page_size = 25
config.maximum_page_size = 200
endclass UserResource < ActiveDataSource::Resource
model User
searchable :name, :email
sortable :name, :email, :created_at
filterable :status, :organization_id, :created_at
includable :organization, :roles
serializer UserSerializer
field :id
field :name
field :email
field :status do
type :select
options User.statuses.keys
required true
default "active"
end
computed_field :display_name do |record|
"#{record.name} <#{record.email}>"
end
belongs_to :organization, resource: OrganizationResource
has_many :roles, resource: RoleResource
default_sort :created_at, direction: :desc
default_page_size 25
maximum_page_size 100
exportable
broadcastable
endclass UsersController < ApplicationController
include ActiveDataSource::Controller
resource UserResource
# Optional: override permitted params
private
def ads_permitted_params
params.require(:user).permit(:name, :email, :status, :organization_id)
end
endresources :users do
collection do
get :schema
get :statistics
get :export
post :bulk_update
delete :bulk_delete
end
endGET /users
?page=2
&per_page=25
&sort=-created_at,name
&search=john
&include=organization,roles
&fields=id,name,email
&filter[name][contains]=john
&filter[status][eq]=active
&filter[age][gte]=18
&filter[organization.name][contains]=OpenAI
&group=organization_id
&aggregate=count,avg:age
| Operator | SQL Equivalent | Example |
|---|---|---|
eq |
= ? |
filter[status][eq]=active |
neq |
!= ? |
filter[status][neq]=inactive |
contains |
LIKE '%?%' |
filter[name][contains]=john |
starts_with |
LIKE '?%' |
filter[name][starts_with]=J |
ends_with |
LIKE '%?' |
filter[email][ends_with]=@gmail.com |
gt / gte / lt / lte |
comparison | filter[age][gte]=18 |
in |
IN (?) |
filter[status][in]=active,pending |
not_in |
NOT IN (?) |
filter[status][not_in]=inactive |
between |
BETWEEN ? AND ? |
filter[age][between]=18,65 |
null |
IS NULL |
filter[deleted_at][null]=1 |
not_null |
IS NOT NULL |
filter[confirmed_at][not_null]=1 |
today |
today's date range | filter[created_at][today]=1 |
this_week |
this week range | filter[created_at][this_week]=1 |
this_month |
this month range | filter[created_at][this_month]=1 |
Nested association filters:
filter[organization.name][contains]=OpenAI
{
"data": [
{ "id": 1, "name": "Alice", "email": "alice@example.com", "status": "active" }
],
"meta": {
"page": 1,
"per_page": 25,
"total": 512,
"pages": 21,
"from": 1,
"to": 25,
"resource": "users"
},
"links": {
"self": "/users?page=1&per_page=25",
"next": "/users?page=2&per_page=25",
"last": "/users?page=21&per_page=25"
},
"included": [],
"aggregates": { "count": 512, "avg_age": 32.4 },
"errors": []
}GET /users/schema
Returns field types, options, validation rules, relationships, and permissions — enabling dynamic form generation:
{
"resource": "UserResource",
"fields": [
{
"name": "status",
"type": "select",
"options": ["active", "inactive", "pending"],
"required": true,
"default": "active"
}
],
"filterable": ["status", "organization_id"],
"sortable": ["name", "created_at"],
"pagination": { "default_page_size": 25, "maximum_page_size": 100 }
}npm install @active-data-source/clientimport { DataSource } from "@active-data-source/client";
const users = new DataSource<User>("/users", {
csrfToken: () => document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content ?? "",
});
// Fluent API
const result = await users
.eq("status", "active")
.contains("name", "john")
.sort("created_at", "desc")
.include("organization")
.page(2)
.per(25)
.fetch();
console.log(result.data); // User[]
console.log(result.meta); // AdsMeta
// CRUD
const created = await users.create({ name: "Bob", email: "bob@example.com" });
const updated = await users.update(1, { status: "inactive" });
await users.destroy(1);
// Schema for dynamic forms
const schema = await users.schema();| Category | Adapters |
|---|---|
| Pagination | Pagy, Kaminari, WillPaginate, Cursor |
| Search | Ransack, Searchkick, PgSearch, Meilisearch |
| Authorization | Pundit, CanCanCan |
| Multi-Tenancy | acts_as_tenant, Apartment |
| Serialization | Blueprinter, jsonapi-serializer, AMS |
| Broadcasting | Turbo Streams, ActionCable |
| Rails | Ruby | Status |
|---|---|---|
| 8.0 | 3.2, 3.3, 3.4 | ✅ Supported |
| 7.1 | 3.2, 3.3, 3.4 | ✅ Supported |
# spec/resources/user_resource_spec.rb
RSpec.describe UserResource do
it { is_expected.to be_filterable_by(:status) }
it { is_expected.to be_filterable_by(:name).with_operator(:contains) }
it { is_expected.to be_sortable_by(:created_at) }
it { is_expected.to be_searchable }
it { is_expected.to allow_including(:organization) }
end- Installation Guide
- Quick Start
- Resource Guide
- Controller Guide
- Query Language Reference
- Operator Reference
- Adapter Guide
- Customization Guide
- Dynamic Forms Guide
- Frontend Integration Guide
- API Reference
- Upgrade Guide
- Architecture Decision Records
MIT