Sanda is a zero-config API boilerplate with Ruby on Rails, PostgreSQL 14.x, Pundit and comes with excellent user and role management API out of the box. Start your next big API project with Sanda, focus on building business logic, and save countless hours of writing boring user and role management API again and again.
Sanda works with Ruby on Rails 8.x., PostgresSQL 14.x, Redis 7.x
- Sanda - Zero Config API Boilerplate with Ruby on Rails
The setups steps expect following tools installed on the system.
- Ruby => 3.3.6
- Rails => 8.x
- PostgreSQL => 14.x or MySQL => 9.x
- Redis => 7.x
It's super easy to get Sanda up and running.
First clone the project and change the directory
git clone https://github.com/hmtanbir/sanda.git
cd sandaThen follow the process using either Docker or without Docker (simple).
- Install project gems
bundle install --path=vendor- Prepare .env file
Copy .env.example file and paste as .env
cp .env.example .env- Generate application secret key and update .env file's
SECRET_KEYvalue.
bundle exec rails secretOpen your .env file and change the DATABASE options. You can start by following these steps
- Create a new database
bundle exec rails db:create- Run Migrations
bundle exec rails db:migrateNow your database has essential tables for user and roles management.
- Run Database Seeders
Run db:seed, and you have your first admin user, normal user, and the relationship correctly setup.
bundle exec rails db:seedPlease note that the default admin user is admin@sanda.project and the default password is sanda-admin-123. You should create a new admin user before deploying to production and delete this default admin user. You can do that using the available Sanda user management API or any DB management tool.
You can run server following command:
bundle exec rails server -p 3000 -b 0.0.0.0That's mostly it! You have a fully running Ruby on Rails installation, all configured.
If you want to use docker container, then you have to follow some steps following below:
You need to prepare docker compose file at first based on database.
If you use postgresql database, then use docker-compose.pg.yml
cp -r docker-compose.pg.yml docker-compose.ymlOR, If you use mysql database, then use docker-compose.mysql.yml
cp -r docker-compose.mysql.yml docker-compose.ymlNow, your docker compose file is ready to build.
Build your docker container following command:
docker-compose build --no-cacheNow, We will run the container following command:
docker-compose up -dNow, We will run the container following command:
docker-compose downIf you want remove container with it's volumes following the command:
docker-compose down --volumes --remove-orphansHere is a list of default routes. Run the following rails command to see this list in your terminal.
bundle exec rails routes
Sanda comes with these admin & user roles out of the box. For details, If you want to add more role, open config/roles.yml file.
Let's have a look at what Sanda has to offer. Before experimenting with the following API endpoints, run your Sanda project using rails server -p 3000 -b 0.0.0.0 command. For the next part of this documentation, we assumed that Sanda is listening at http://localhost:3000
You can make an HTTP POST call to create/register a new user to the following endpoint. Newly created users will have the user role by default.
http://localhost:3000/api/v1/registrationAPI Payload & Response
You can send a Form Multipart payload or a JSON payload like this.
{
"user": {
"name": "Sanda User",
"email": "user@sanda.project",
"password": "sanda-user-123"
}
}Voila! Your user has been created and is now ready to log in!
If this user already exists, then you will receive a 422 Response like this
{
"status": 422,
"message": [
"Email has already been taken"
],
"data": null
}Remember Sanda comes with the default admin user? You can log in as an admin by making an HTTP POST call to the following route.
http://localhost:3000/api/v1/sessionsAPI Payload & Response
You can send a Form Multipart or a JSON payload like this.
{
"user": {
"email": "admin@sanda.project",
"password": "sanda-admin-123"
}
}You will get a JSON response with user token. You need this admin token for making any call to other routes protected by admin ability.
{
"status": 200,
"message": "Successfully data fetched",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3NTM3NjYzMzh9.fuaZL5O2meayrdQT0_YalMhp2hK_mOGPduyBKpsmZz0"
}
}For any unsuccessful attempt, you will receive a 401 error response.
For invalid email address:
{
"status": 404,
"message": "invalid email",
"data": null
}For invalid password:
{
"status": 401,
"message": "invalid password",
"data": null
}You can log in as a user by making an HTTP POST call to the following route
http://localhost:3000/api/v1/sessionsAPI Payload & Response
You can send a Form Multipart or a JSON payload like this
{
"user": {
"email": "user@sanda.project",
"password": "sanda-user-123"
}
}You will get a JSON response with user token. You need this user token for making any calls to other routes protected by user ability.
{
"status": 200,
"message": "Successfully data fetched",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJleHAiOjE3NTM3NjYzMzh9.c3Pp9pSbH0UoY01NmSa9cydqqla2Jmqt0JhnbqPte8Q"
}
}For any unsuccessful attempt, you will receive a 401 error response.
For invalid email address:
{
"status": 404,
"message": "invalid email",
"data": null
}For invalid password:
{
"status": 401,
"message": "invalid password",
"data": null
}To list the all users, make an HTTP GET call to the following route, with Admin Token obtained from Admin Login. Add this token as a standard Bearer Token to your API call.
http://localhost:3000/api/v1/usersAPI request
curl --location 'localhost:3000/api/v1/users' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3NTM3NjcwNzR9.JGNtk8hB2zSDBU05HtDw2BR0__6WHv7D5QFJjV1I4l0'API Payload
No payload is required for this call.
API response You will get a JSON response with all users available in your project.
{
"status": 200,
"message": "Successfully data fetched",
"data": [
{
"id": 1,
"name": "Admin User",
"email": "admin@sanda.project",
"role": "admin",
"created_at": "2025-07-28T05:30:33.658Z",
"updated_at": "2025-07-28T05:30:33.658Z",
"deleted_at": null
},
{
"id": 2,
"name": "Regular User",
"email": "user@sanda.project",
"role": "user",
"created_at": "2025-07-28T05:30:33.853Z",
"updated_at": "2025-07-28T05:30:33.853Z",
"deleted_at": null
}
],
"current_page": 1,
"per_page": 10,
"total_pages": 1,
"total_count": 2,
"next_page": null,
"prev_page": null
}To list the admin users, make an HTTP GET call to the following route, with Admin Token obtained from Admin Login. Add this token as a standard Bearer Token to your API call
and a params role named "admin"
http://localhost:3000/api/v1/users?role=adminAPI request
curl --location 'localhost:3000/api/v1/users?role=admin' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3NTM3NjcwNzR9.JGNtk8hB2zSDBU05HtDw2BR0__6WHv7D5QFJjV1I4l0'API Payload
No payload is required for this call.
API response You will get a JSON response with all users available in your project.
{
"status": 200,
"message": "Successfully data fetched",
"data": [
{
"id": 1,
"name": "Admin User",
"email": "admin@sanda.project",
"role": "admin",
"created_at": "2025-07-28T05:30:33.658Z",
"updated_at": "2025-07-28T05:30:33.658Z",
"deleted_at": null
}
],
"current_page": 1,
"per_page": 10,
"total_pages": 1,
"total_count": 1,
"next_page": null,
"prev_page": null
}To list the admin users, make an HTTP GET call to the following route, with Admin Token obtained from Admin Login. Add this token as a standard Bearer Token to your API call
and a params role named "user"
http://localhost:3000/api/v1/users?role=userAPI request
curl --location 'localhost:3000/api/v1/users?role=admin' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3NTM3NjcwNzR9.JGNtk8hB2zSDBU05HtDw2BR0__6WHv7D5QFJjV1I4l0'API Payload
No payload is required for this call.
API response You will get a JSON response with all users available in your project.
{
"status": 200,
"message": "Successfully data fetched",
"data": [
{
"id": 2,
"name": "Regular User",
"email": "user@sanda.project",
"role": "user",
"created_at": "2025-07-28T05:30:33.853Z",
"updated_at": "2025-07-28T05:30:33.853Z",
"deleted_at": null
}
],
"current_page": 1,
"per_page": 10,
"total_pages": 1,
"total_count": 1,
"next_page": null,
"prev_page": null
}For any invalid token, you will receive a 401 error response.
{
"status": 401,
"message": "Invalid token",
"data": null
}For any unsuccessful attempt or wrong token (other user token who have not any permission), you will receive a 403 error response.
{
"status": 403,
"message": "unauthorized",
"data": null
}Make an HTTP POST request to the following route to create a other role (admin/editor/manager etc.) user. You must include a Bearer token obtained from User/Admin authentication. A bearer admin token can create other user.
http://localhost:8000/api/v1/usersFor example, to update the user with id 2, use this endpoint http://localhost:3000/api/v1/users/2
API request
curl --location --request POST 'localhost:3000/api/v1/users' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE3NTM3NjcwNzR9.MMFIA1OwwXkPpkmaEZbz4P6fGvKJ28Uy8PTsL6RgBa8' \
--header 'Content-Type: application/json' \
--data '{
"user": {
"name": "Sanda admin 1",
"email": "admin1@sanda.project",
"password": "sanda-admin-123",
"role": "admin"
}
}'API Payload
You can include name , email, and role in a JSON payload, just like this
{
"user": {
"name": "Sanda Another Admin",
"email": "another-admin@sanda.project",
"password": "sanda-admin-123",
"role": "admin"
}
}API Response
You will receive the created user if the bearer token is valid.
{
"status": 201,
"message": "Successfully data created",
"data": {
"id": 4,
"name": "Admin",
"email": "admin3@oytrack.project",
"role": "admin",
"created_at": "2025-08-06T07:55:48.536Z",
"updated_at": "2025-08-06T07:55:48.536Z",
"deleted_at": null
}
}For any unsuccessful attempt with an invalid token, you will receive a 401 error response.
{
"status": 401,
"message": "Invalid token",
"data": null
}If a bearer user token attempts to update any other user but itself, a 401 error response will be delivered
{
"status": 401,
"message": "unauthorized",
"data": null
}If role is invalid, then you will get a 500 error response like below:
{
"status": 500,
"message": "'editor' is not a valid role",
"data": null
}Make an HTTP PUT request to the following route to update an existing user. Replace {userId} with actual user id. You must include a Bearer token obtained from User/Admin authentication. A bearer admin token can update any user. A bearer user token can only update only his/her information.
http://localhost:8000/api/v1/users/{userId}For example, to update the user with id 2, use this endpoint http://localhost:3000/api/v1/users/2
API request
curl --location --request PATCH 'localhost:3000/api/v1/users/2' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE3NTM3NjcwNzR9.MMFIA1OwwXkPpkmaEZbz4P6fGvKJ28Uy8PTsL6RgBa8' \
--header 'Content-Type: application/json' \
--data '{
"user": {
"name": "Sanda User 1",
"email": "user@sanda.project"
}
}'API Payload
You can include name or email, or both in a JSON payload, just like this
{
"user": {
"name": "Sanda User 1",
"email": "user@sanda.project"
}
}API Response
You will receive the updated user if the bearer token is valid.
{
"status": 200,
"message": "Successfully data updated",
"data": {
"id": 2,
"name": "Sanda User 1",
"email": "user@sanda.project",
"role": "user",
"created_at": "2025-07-28T05:30:33.853Z",
"updated_at": "2025-07-28T05:30:33.853Z",
"deleted_at": null
}
}For any unsuccessful attempt with an invalid token, you will receive a 401 error response.
{
"status": 401,
"message": "Invalid token",
"data": null
}If a bearer user token attempts to update any other user but itself, a 401 error response will be delivered
{
"status": 401,
"message": "unauthorized",
"data": null
}To delete an existing user, make a HTTP DELETE request to the following route. Replace {userId} with actual user id
http://localhost:3000/api/v1/users/{userId}For example to delete the user with id 2, use this endpoint http://localhost:3000/api/v1/users/2
API Request
curl --location --request DELETE 'localhost:3000/api/v1/users/2' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3NTM3NjcwNzR9.JGNtk8hB2zSDBU05HtDw2BR0__6WHv7D5QFJjV1I4l0'API Payload
No payload is required for this call.
If the request is successful and the bearer token is valid, you will receive a JSON response like this
{
"status": 200,
"message": "Successfully data deleted",
"data": null
}You will receive a 401 error response for any unsuccessful attempt with an invalid token.
{
"status": 401,
"message": "Invalid token",
"data": null
}For any unsuccessful attempt with an invalid user id, you will receive a 404 not found error response. For example, you will receive the following response when you try to delete a non-existing user with id 16.
{
"status": 404,
"message": "Couldn't find User with 'id'=20",
"data": null
}When you run the database seeders, a default admin user is created with the username admin@sanda.project and the password sanda-admin-123. You can login as this default admin user and use the bearer token on next API calls where admin ability is required.
When you push your application to production, please remember to change this user's password, email or simply create a new admin user and delete the default one.
The user role is assigned to them when a new user is created.
There are two default role in Sanda.
| Role Slug | Role Name | Rank |
|---|---|---|
| admin | Admin | 0 |
| user | User | 1 |
This Role variables is configured in config/roles.yml. If you want to add more role, open config/roles.yml file and add your role in below the user role with rank.
Suppose, you want to add a new role named editor. then, it will like below:
# config/roles.yml
admin: 0
user: 1
editor: 2Sanda doesn't invalidate the previously issued access tokens when a user authenticates. So, all access tokens, including the newly created one, will remain valid.
This is very important. To properly receive JSON responses, add the following header to your API requests.
Accept: application/jsonFor example, if you are using curl you can make a call like this.
curl --location 'localhost:3000/api/v1/users/1' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE3NTM3NjcwNzR9.MMFIA1OwwXkPpkmaEZbz4P6fGvKJ28Uy8PTsL6RgBa8' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json'Sanda comes with an excellent logger to log request headers, parameters and response to help debugging and inspecting API calls. Check log details into log folder.
Sanda comes with an excellent code formatter called Rubocop out of the box, with an excellent configuration preset.
To format your code using rubocop, you can run the following command any time from inside your project diretory.
bundle exec rubocop -aAnd that's all for formatting. To know more, check out rubocop documentation at https://github.com/rubocop/rubocop
Sanda comes with 100% test coverage using RSpec. You can check testing following command:
bundle exec rspecTo check coverage, open the coverage file:
open coverage/index.html
So you decided to give Sanda a try and create a new protected API endpoint; that's awesome; let's dive in.
You can create a normal or a resourceful controller. To keep it simple, I am going with a standard controller.
rails generate controller API::V1::BlogsControllerThis will create a new controller file called app/controlers/api/v1/blogs_controller.rb
We will add index function that will return Hello Sanda text.
Open this file app/controlers/api/v1/blogs_controller.rb and add the following code
class Api::V1::BlogsController < ApplicationController
def index
data = "Hello Sanda"
render_json_response(:ok, I18n.t("data.success.fetched"), data)
end
endLet's create a protected route http://localhost:3000/api/v1/blogs to use this API
Open your config/routes file and add the following line at the end.
namespace :api do
namespace :v1 do
get "blogs/index", to: "blogs#index"
end
endLet's create a authorization policy for the BlogsController. We assume that all authenticated user can see the blogs
Create a new policy named blog_policy.rb in app/policies folder add following codes:
class BlogPolicy < ApplicationPolicy
def index?
user.admin? || user.user?
end
endNow implement authorization in blog controller file app/controlers/api/v1/blogs_controller.rb and add following codes:
before_action :authorization_requestand
private
def authorization_request
authorize @current_user, policy_class: BlogPolicy
endHere, we authorize Api::V1::BlogsController for current user with BlogPolicy. Based on BlogPolicy,
current user admin or normal user can see the API result.
Now, blog controller file app/controlers/api/v1/blogs_controller.rb looks like below:
class Api::V1::BlogsController < ApplicationController
before_action :authorization_request
def index
data = "Hello Sanda"
render_json_response(:ok, I18n.t("data.success.fetched"), data)
end
private
def authorization_request
authorize @current_user, policy_class: BlogPolicy
end
endNice! Now we have a route /api/v1/blogs that is only accessible with a valid bearer token.
If you have already created a user, you need his accessToken first. You can use the admin user or create a new user and then log in and note their bearer token. To create or authenticate a user, check the documentation in the beginning.
To create a new user, you can place a curl request or use tools like Postman, Insomnia or HTTPie. Here is a quick example using curl.
curl --location 'http://localhost:3000/api/v1/blogs' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJleHAiOjE3NTM3NzE1NDJ9.5ncIDT8oo0uEhBOwJLaIYjPCcnV4cMCCsMeICMVI8t0' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json'Great! Now we have our users. Let's login as this new user using curl (You can use tools like Postman, Insomnia, or HTTPie)
curl --location 'localhost:3000/api/v1/registration' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data-raw '{
"user": {
"name": "Sanda User ",
"email": "user2@sanda.project",
"password": "sanda-user-123"
}
}'Now you have this user's accessToken in the response, as shown below. Note it.
{
"status": 200,
"message": "Successfully data fetched",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJleHAiOjE3NTM3NzUyMjh9.jq_eTPEVJ8OjIY6OGvezPPRlyh4IITENvoaEHTWjfBU"
}
}The bearer token for this user is eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJleHAiOjE3NTM3NzUyMjh9.jq_eTPEVJ8OjIY6OGvezPPRlyh4IITENvoaEHTWjfBU
Now let's test our protected route. Add this bearer token in your PostMan/Insomnia/HTTPie or Curl call and make a HTTP GET request to our newly created protected route http://localhost:3000/api/v1/blogs. Here's an example call with curl
curl --location 'http://localhost:3000/api/v1/blogs' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJleHAiOjE3NTM3NzUyMjh9.jq_eTPEVJ8OjIY6OGvezPPRlyh4IITENvoaEHTWjfBU' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json'The response will be something like this.
{
"status": 200,
"message": "Successfully data fetched",
"data": "Hello Sanda"
}Great! you have learned how to create your protected API endpoint using Ruby on Rails and Sanda!
Now you know everything to start creating your next big API project with Ruby on Rails our powerful boilerplate project called Sanda. Enjoy!