✨ From vibe coding to vibe deployment. UBOS MCP turns ideas into infra with one message.

Learn more
Carlos
  • Updated: March 17, 2026
  • 7 min read

Implementing OpenClaw Plugin Rating and Review System on UBOS

The OpenClaw Plugin Rating and Review System can be implemented on UBOS by following a clear architecture, designing RESTful APIs, creating a relational database schema, and deploying either self‑hosted Docker containers or a one‑click UBOS‑hosted app.

1. Introduction

AI‑agents are reshaping how developers build, test, and market SaaS products. The Moltbook social network, a fast‑growing community for AI‑enhanced creators, has already integrated AI‑driven recommendation engines. In this ecosystem, a robust rating and review system for plugins becomes a strategic asset: it fuels social proof, guides users toward high‑quality extensions, and provides data for automated moderation.

Earlier we published a high‑level overview of the OpenClaw plugin on UBOS. This guide dives deeper, delivering a step‑by‑step developer tutorial that covers architecture, API design, database schema, code snippets, and deployment options.

2. Architecture Overview

Core Components

  • Frontend UI – React/Vue component embedded in the UBOS marketplace.
  • Backend Service – Node.js/Express micro‑service handling rating logic.
  • Database – PostgreSQL managed by UBOS or self‑hosted.
  • UBOS Integration Layer – Manifest file, auto‑updates, and API gateway.

Interaction Flow

  1. User clicks “Rate” on a plugin card.
  2. UI sends a POST /ratings request to the backend.
  3. Backend validates the JWT token issued by UBOS.
  4. Rating is persisted, and an audit entry is created.
  5. Frontend refreshes the average score in real time.

3. API Design

The API follows REST conventions and uses JSON payloads. All endpoints are protected by UBOS‑issued JWT tokens.

3.1 Endpoints

MethodEndpointPurpose
GET/plugins/:id/ratingsFetch average rating & recent reviews.
POST/ratingsCreate a new rating & optional review.
PUT/ratings/:idUpdate an existing rating.
DELETE/ratings/:idRemove a rating (admin only).

3.2 Request / Response Schemas

// POST /ratings payload
{
  "plugin_id": "uuid‑of‑plugin",
  "user_id": "uuid‑of‑user",
  "score": 4,               // integer 1‑5
  "review": "Clear docs, fast install.",
  "tags": ["documentation", "performance"]
}

// Successful response
{
  "status": "created",
  "rating_id": "uuid‑of‑rating",
  "created_at": "2024‑12‑01T12:34:56Z"
}

3.3 Authentication & Permissions

  • All calls require an Authorization: Bearer <JWT> header.
  • Standard users can POST and PUT their own ratings.
  • Admins (identified by the role claim) can DELETE any rating.

4. Database Schema

The relational model is deliberately MECE: each table has a single responsibility.

4.1 Tables Overview

TableKey FieldsPurpose
pluginsid, name, owner_idCatalog of UBOS plugins.
usersid, email, roleUBOS platform users.
ratingsid, plugin_id, user_id, scoreNumeric rating (1‑5).
reviewsid, rating_id, text, tagsOptional free‑form review.
audit_logid, action, user_id, timestampImmutable trail for moderation.

4.2 ER Diagram (simplified)

users 1---* ratings *---1 plugins
ratings 1---1 reviews
users 1---* audit_log

5. Code Snippets

5.1 Node.js/Express Route Handlers

// rating.routes.js
const express = require('express');
const router = express.Router();
const { verifyToken, isAdmin } = require('../middleware/auth');
const Rating = require('../models/Rating');

// POST /ratings
router.post('/', verifyToken, async (req, res) => {
  const { plugin_id, score, review, tags } = req.body;
  const user_id = req.user.id;

  const rating = await Rating.create({ plugin_id, user_id, score });
  if (review) {
    await rating.createReview({ text: review, tags });
  }
  res.status(201).json({ status: 'created', rating_id: rating.id });
});

// PUT /ratings/:id
router.put('/:id', verifyToken, async (req, res) => {
  const { score, review, tags } = req.body;
  const rating = await Rating.findOne({ where: { id: req.params.id, user_id: req.user.id } });
  if (!rating) return res.status(404).json({ error: 'Not found' });

  rating.score = score ?? rating.score;
  await rating.save();

  if (review) {
    const rev = await rating.getReview();
    rev.text = review;
    rev.tags = tags;
    await rev.save();
  }
  res.json({ status: 'updated' });
});

// DELETE /ratings/:id (admin only)
router.delete('/:id', verifyToken, isAdmin, async (req, res) => {
  await Rating.destroy({ where: { id: req.params.id } });
  res.json({ status: 'deleted' });
});

module.exports = router;

5.2 SQL Migration Script (PostgreSQL)

-- 2024_10_01_create_rating_schema.sql
CREATE TABLE plugins (
    id UUID PRIMARY KEY,
    name TEXT NOT NULL,
    owner_id UUID REFERENCES users(id)
);

CREATE TABLE users (
    id UUID PRIMARY KEY,
    email TEXT UNIQUE NOT NULL,
    role TEXT NOT NULL CHECK (role IN ('user','admin'))
);

CREATE TABLE ratings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    plugin_id UUID REFERENCES plugins(id) ON DELETE CASCADE,
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    score SMALLINT CHECK (score BETWEEN 1 AND 5),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

CREATE TABLE reviews (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    rating_id UUID REFERENCES ratings(id) ON DELETE CASCADE,
    text TEXT,
    tags TEXT[],
    created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

CREATE TABLE audit_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    action TEXT NOT NULL,
    user_id UUID REFERENCES users(id),
    details JSONB,
    timestamp TIMESTAMP WITH TIME ZONE DEFAULT now()
);

5.3 React Rating Component

import { useState } from 'react';
import axios from 'axios';

export default function RatingBox({ pluginId }) {
  const [score, setScore] = useState(0);
  const [review, setReview] = useState('');
  const [status, setStatus] = useState('');

  const submit = async () => {
    try {
      const token = localStorage.getItem('ubos_jwt');
      await axios.post(
        '/api/ratings',
        { plugin_id: pluginId, score, review },
        { headers: { Authorization: `Bearer ${token}` } }
      );
      setStatus('Thanks for your feedback!');
    } catch (e) {
      setStatus('Error submitting rating.');
    }
  };

  return (
    

Rate this plugin

{[1,2,3,4,5].map(num => ( ))}