@duabalabs/reviews-sdk
Framework-agnostic client for the centralized 3y3anaa reviews platform — the
shared review/rating store for the Duaba ecosystem. Reviews target a subject
namespaced by (sourceApp, subjectType, externalId); reviewers are external
identities (no Parse account needed). Your app authenticates with a per-app key.
Call this server-side. The app key (X-App-Key) is secret and must never reach
a browser bundle.
Install
npm i @duabalabs/reviews-sdk
# or
pnpm add @duabalabs/reviews-sdkShips CJS + ESM with bundled .d.ts types. No runtime dependencies — it uses
the global fetch.
createReviewsClient(config)
import { createReviewsClient } from '@duabalabs/reviews-sdk';
const reviews = createReviewsClient({
serverUrl: process.env.THREEYANAA_API_URL!, // https://api.3y3anaa.com/parse
appId: process.env.THREEYANAA_PARSE_APP_ID!, // X-Parse-Application-Id
javascriptKey: process.env.THREEYANAA_PARSE_JS_KEY!, // X-Parse-Javascript-Key (required; not secret)
appKey: process.env.THREEYANAA_APP_KEY!, // X-App-Key (secret, per-app scoping)
});Config
| Field | Type | Required | Notes |
|---|---|---|---|
serverUrl | string | ✅ | Parse server base URL (trailing slash trimmed). |
appId | string | ✅ | Sent as X-Parse-Application-Id. |
appKey | string | ✅ | Sent as X-App-Key. Secret — per-app scoping/auth. |
javascriptKey | string | ⚠️ | Sent as X-Parse-Javascript-Key. Required when the server has a JS key configured (it does). Not secret. |
fetch | typeof fetch | — | Custom fetch impl (Node < 18 / tracing wrappers). Defaults to global fetch. |
If no fetch is available and none is supplied, the constructor throws a
ReviewsApiError.
Methods
The client returns an object with the following methods. Each POSTs to a Parse
cloud function and unwraps the { result } envelope.
submitReview(input)
Create or edit a review. One review exists per (app, subject, author) — calling
again with the same subject + author edits the existing one.
const { review, aggregate } = await reviews.submitReview({
subjectType: 'product',
externalId: 'prod_123',
subjectDisplayName: 'Kente Tote Bag', // recorded on the subject (first time)
subjectImageUrl: 'https://cdn.example/…',
subjectUrl: 'https://sellub.com/p/prod_123',
author: { externalUserId: 'user_42', displayName: 'Ama', role: 'buyer' },
rating: 5, // 1–5
title: 'Lovely bag',
body: 'Beautiful craftsmanship, fast delivery.',
verified: true, // e.g. verified purchase
});
// → { review: Review, aggregate: ReviewAggregate }SubmitReviewInput: subjectType, externalId, author, rating (required);
title?, body?, verified?, subjectDisplayName?, subjectImageUrl?, subjectUrl?, subjectMeta? (optional). Maps to submitAppReview.
Server-side, a review whose computed qualityScore is < 35 is set to
under_review rather than published. Inspect review.status after submitting.
getReviews(input)
Paginated reviews for a subject, scoped to your app, with the aggregate.
const { items, total, hasMore, aggregate } = await reviews.getReviews({
subjectType: 'product',
externalId: 'prod_123',
limit: 20, // ≤ 100, default 20
skip: 0,
sort: 'newest', // 'newest' | 'helpful' | 'highest' | 'lowest'
});Maps to getAppReviews.
getAggregate(input)
The aggregate for a single subject.
const agg = await reviews.getAggregate({ subjectType: 'product', externalId: 'prod_123' });
// → { subjectType, externalId, avgRating, reviewCount, ratingBreakdown }Maps to getAppReviewAggregate.
getAggregates(subjects)
Batch aggregates for product grids / cards (≤ 200 per call).
const aggs = await reviews.getAggregates([
{ subjectType: 'product', externalId: 'prod_123' },
{ subjectType: 'product', externalId: 'prod_456' },
]);
// → Array<{ subjectType, externalId, avgRating, reviewCount, ratingBreakdown }>Maps to getAppReviewAggregates (the SDK unwraps { aggregates }).
markHelpful(input)
One helpful vote per external voter.
const { helpfulCount, alreadyVoted } = await reviews.markHelpful({
reviewId: 'abc123',
voterExternalUserId: 'user_99',
});Maps to markAppReviewHelpful.
respondToReview(input)
The subject owner’s public response to a review.
const { review } = await reviews.respondToReview({
reviewId: 'abc123',
responderExternalUserId: 'seller_7',
responderDisplayName: 'Accra Crafts',
text: 'Thanks! We’ve refunded the delivery fee.',
});Maps to respondToAppReview.
getReviewsByAuthor(input)
An author’s own reviews (“my reviews”), scoped to your app.
const { items, total, hasMore } = await reviews.getReviewsByAuthor({
authorExternalUserId: 'user_42',
limit: 20,
skip: 0,
});Maps to getAppReviewsByAuthor.
Types
type SubjectType =
| 'artisan' | 'business' | 'client'
| 'product' | 'shop' | 'supplier' | 'courier'
| (string & {}); // open union — any string accepted, these are canonical
type ReviewerRole =
| 'artisan' | 'business' | 'client'
| 'buyer' | 'seller' | 'courier'
| (string & {});
interface ReviewAuthor {
externalUserId: string;
displayName?: string;
role?: ReviewerRole;
}
interface RatingBreakdown { 1: number; 2: number; 3: number; 4: number; 5: number }
interface ReviewAggregate {
avgRating: number;
reviewCount: number;
ratingBreakdown: RatingBreakdown;
}
interface Review {
id: string;
subjectType: SubjectType;
externalId: string;
rating: number;
title: string | null;
body: string | null;
verified: boolean;
status: 'published' | 'under_review' | 'rejected';
author: { externalUserId: string; displayName: string | null; role: ReviewerRole | null };
helpfulCount: number;
response: { text: string; byDisplayName: string; at: string } | null;
createdAt: string;
}Error handling
Methods throw a ReviewsApiError on network failure or when the server returns
an error/!ok response. The error carries an optional numeric code (the Parse
error code or HTTP status).
import { ReviewsApiError } from '@duabalabs/reviews-sdk';
try {
await reviews.submitReview({ /* … */ });
} catch (e) {
if (e instanceof ReviewsApiError) {
console.error('reviews error', e.code, e.message);
// common: rate limit ("slow down"), invalid app key, out-of-scope subjectType, validation
}
// degrade gracefully — don't block your checkout/page on a reviews failure
}Common server-side errors: invalid/inactive app key, out-of-scope
subjectType (the credential’s scopes don’t include it), rate limit exceeded
(per-app rateLimitPerMin), and validation (rating not 1–5, missing
subjectType/externalId/author.externalUserId).
Environment
| Env var | Maps to config / header | Secret? |
|---|---|---|
THREEYANAA_API_URL | serverUrl | no |
THREEYANAA_PARSE_APP_ID | appId → X-Parse-Application-Id | no |
THREEYANAA_PARSE_JS_KEY | javascriptKey → X-Parse-Javascript-Key | no |
THREEYANAA_APP_KEY | appKey → X-App-Key | yes |
Worked example — Sellub
Sellub integrates the reviews platform server-side from its storefront. The
pattern (mirrored in sellub-storefront/src/lib/reviews/client.ts, which will swap
to this package once published):
Verified-purchase product review — after an order settles, Sellub submits a
review on behalf of the buyer, marked verified:
// server action / route handler — never client-side
await reviews.submitReview({
subjectType: 'product',
externalId: order.productId, // Sellub's own product id
subjectDisplayName: product.name,
subjectUrl: `https://sellub.com/p/${product.slug}`,
author: {
externalUserId: order.customerId, // Sellub's user id
displayName: order.customerName,
role: 'buyer',
},
rating,
title,
body,
verified: true, // gated on a real purchase
});Shop review — the same call with subjectType: 'shop' and the seller’s channel
id as externalId.
Read on the product page (server component, fail-soft):
const { items, aggregate } = await reviews.getReviews({
subjectType: 'product',
externalId: product.id,
sort: 'helpful',
});
// render aggregate.avgRating / aggregate.reviewCount + itemsProduct grid — one batched call for all visible cards:
const aggs = await reviews.getAggregates(
products.map((p) => ({ subjectType: 'product', externalId: p.id })),
);Sellub’s credential is provisioned with
scopes: ['product','shop','supplier','courier'], so submitting a courier or
supplier review later needs no new key. 3y3anaa, as the host, can then surface
Sellub’s reviews of a business alongside every other app’s via getSubjectReviews.
Related
- SDK overview — package summary and who uses it.