Skip to main content

What is Rhino?

Rhino is a full-stack library that turns your models into a complete REST API — with React hooks to consume it. No boilerplate controllers, no manual route definitions, no hand-written fetch calls. Available for Laravel, Ruby on Rails, and NestJS.

Register a model. Get an API. Use a hook.

Pick your stack​

Jump straight to the docs for your framework:

Laravel​

Eloquent models, policies, validation, multi-tenancy.

Get started →

Rails​

ActiveRecord, Pundit policies, the same powerful features.

Get started →

NestJS​

TypeScript, Prisma, decorators, dependency injection.

Get started →

React Client​

Hooks for auth, CRUD, pagination, includes.

Get started →

Or keep reading for the architecture overview and a Hello World example.

How It Works​

┌─────────────────────────┐         ┌──────────────────────────┐
│ React / React Native │ HTTP │ Laravel, Rails, or │
│ │◄───────►│ NestJS Server │
│ useModelIndex('posts') │ │ │
│ useModelStore('posts') │ │ Register a model │
│ useModelUpdate('posts')│ │ │
│ useModelDelete('posts')│ │ Automatic CRUD routes │
│ useAuth() │ │ Validation, Policies │
└─────────────────────────┘ └──────────────────────────┘
@rhino-dev/rhino-react rhino-project/rhino-laravel (Laravel)
rhino-rails (Ruby on Rails)
@rhino-dev/rhino-nestjs (NestJS)

Features​

  • Automatic REST API — register a model, get full CRUD endpoints with zero controllers
  • React Hooks — TanStack Query hooks for every endpoint (index, show, store, update, delete)
  • Role-Based Permissions — layered: a shared per-org role layer plus per-user grant/deny overrides (deny wins), with wildcard support (posts.*, *)
  • Role-Based Validation — different fields allowed per role (admin vs editor vs viewer)
  • Multi-Tenancy — built-in organization scoping via URL prefix or subdomain
  • Soft Deletes — trash, restore, and force-delete with separate permissions
  • Audit Trail — automatic change logging (who changed what, when)
  • Nested Operations — atomic multi-model transactions with cross-references
  • Invitation System — invite users to organizations with role assignment
  • Blueprint Generator — define permissions in YAML, generate fully working policies, tests, and seeders deterministically
  • Query Builder — filters, sorts, search, pagination, includes, field selection
  • Named Scopes — clients select a model-whitelisted ?scope= (with a model-declared default); available on Laravel, Rails, and NestJS
  • React Native Support — same hooks with platform-adapted storage and networking

Quick Install​

Server (Laravel)​

terminal
composer require rhino-project/rhino-laravel:^4.0
php artisan rhino:install

Server (Ruby on Rails)​

terminal
bundle add rhino-rails -v "~> 4.0"
rails rhino:install

Server (NestJS)​

terminal
npm install @rhino-dev/rhino-nestjs@^4.0
npx rhino install

Client (React)​

terminal
npm install @rhino-dev/rhino-react@^4.0 @tanstack/react-query axios

Hello World Example​

1. Register a model on the server​

Laravel:

config/rhino.php
return [
'models' => [
'posts' => \App\Models\Post::class,
],
];

Ruby on Rails:

config/initializers/rhino.rb
Rhino.configure do |c|
c.model :posts, 'Post'
end

Node.js (NestJS):

src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { RhinoModule } from '@rhino-dev/rhino-nestjs';

const prisma = new PrismaClient();

@Module({
imports: [
RhinoModule.forRoot({
prismaClient: prisma,
models: {
posts: { model: 'post' },
},
}),
],
})
export class AppModule {}

This automatically creates these endpoints:

MethodEndpointDescription
GET/api/postsList with filters, sorts, search
POST/api/postsCreate with validation
GET/api/posts/{id}Show single record
PUT/api/posts/{id}Update with validation
DELETE/api/posts/{id}Soft delete

2. Use it from React​

src/components/Posts.tsx
import { useModelIndex, useModelStore } from '@rhino-dev/rhino-react';

function Posts() {
// Fetch posts with pagination and sorting
const { data: response, isLoading } = useModelIndex('posts', {
page: 1,
perPage: 10,
sort: '-created_at',
});

// Create a new post
const createPost = useModelStore('posts');

const handleCreate = () => {
createPost.mutate({ title: 'My First Post', content: 'Hello world!' });
};

if (isLoading) return <div>Loading...</div>;

return (
<div>
<button onClick={handleCreate}>New Post</button>
{response?.data.map(post => (
<h2 key={post.id}>{post.title}</h2>
))}
</div>
);
}