Skip to content

I. Shared Platform Opportunities

← Back to README


Principles for Shared Code

Before listing candidates, three principles must be stated clearly:

  1. Only extract when 2+ repos consume it with divergent release cycles. If only one app uses something, it stays in that app.
  2. Shared packages must have an owner. No ownerless packages — they become junk drawers.
  3. Primitives only at the shared layer. Types, config schemas, HTTP clients. Never business logic.

The greatest risk in any shared-code initiative is the "shared junk drawer" — a package that accumulates utilities because no one wants to own them, resulting in an untested, poorly documented catch-all.


Package Candidates

@soreto/types — Shared TypeScript Types

Priority: HIGH

Consumers: soreto-melissa, reverb-backend (after TS migration), soreto-zoe

What to include:

// Entity types (from reverb-backend API response shapes)
interface User { id: string; email: string; roles: Role[]; ... }
interface Role { id: string; name: RoleName; }
interface Campaign { id: string; clientId: string; name: string; ... }
interface CampaignVersion { id: string; campaignId: string; ... }
interface Order { id: string; campaignVersionId: string; status: OrderStatus; ... }
interface SharedUrl { id: string; userId: string; campaignVersionId: string; ... }
interface Reward { id: string; orderId: string; status: RewardStatus; ... }

// Enum types
type RoleName = 'admin' | 'user' | 'client' | 'clientUser' | 'financial' | 'guest' | 'system' | 'sales' | 'mpUser' | 'tech' | 'saas';
type OrderStatus = 'PENDING' | 'CONFIRMED' | 'CANCELLED' | ...;
type RewardStatus = 'ADDED' | 'DONE' | 'BLOCKED_REWARD_LIMIT' | 'EXPIRED';

// API response envelope
interface ApiResponse<T> { data: T; total?: number; page?: number; }
interface PaginatedResponse<T> extends ApiResponse<T[]> { totalPages: number; }

Source: Extract from soreto-melissa/types/ and soreto-melissa/shared/constants.ts, then validate against reverb-backend's actual API responses.

What to exclude: Database model types (stay in reverb-backend), Next.js-specific types (stay in melissa), Bull job types (stay in zoe).


@soreto/api-client — Typed reverb-backend HTTP Client

Priority: HIGH

Consumers: soreto-zoe (replaces local_modules/reverb), potentially soreto-melissa (replaces raw axios calls)

What to include:

// A typed, versioned replacement for local_modules/reverb/index.js
export interface ReverbClientOptions {
  apiBaseUrl: string;
  username: string;
  password: string;
}

export class ReverbClient {
  static create(options: ReverbClientOptions): Promise<ReverbClient>;

  postReward: {
    getCampaignVersionsEnabledV2(daysBack: number): Promise<CampaignVersion[]>;
    getOrders(campaignVersionId: string, minutesBack: number, page: number, pageSize: number): Promise<Order[]>;
    createOrderPostReward(payload: CreateOrderPostRewardPayload): Promise<OrderPostReward>;
    // ...
  };

  notification: {
    send(data: NotificationPayload): Promise<void>;
    // ...
  };

  zendesk: {
    leads: {
      create(payload: ZendeskLeadPayload): Promise<ZendeskLead>;
      // ...
    };
  };
}

Why this matters: The current local_modules/reverb client has no types. If reverb-backend changes an endpoint, soreto-zoe breaks silently at runtime. With @soreto/api-client, breaking changes are caught at compile time.


@soreto/constants — Shared Constants

Priority: MEDIUM

Consumers: soreto-melissa (already has some), reverb-backend (after TS migration), soreto-zoe

What to include:

export enum RoleName {
  ADMIN = 'admin',
  USER = 'user',
  CLIENT = 'client',
  CLIENT_USER = 'clientUser',
  // ...
}

export const CAMPAIGN_TYPES = {
  ON_SITE_REFERRAL: 'on_site_referral',
  MARKETPLACE: 'marketplace',
} as const;

export const COUNTRIES: Country[] = [/* ... */];

export const ORDER_STATUSES = {
  ADDED: 'ADDED',
  DONE: 'DONE',
  BLOCKED_REWARD_LIMIT: 'BLOCKED_REWARD_LIMIT',
  EXPIRED: 'EXPIRED',
} as const;

Source: soreto-melissa/shared/constants.ts + constants from reverb-backend/config/constants.js + soreto-zoe processor constants.


@soreto/config — Environment Variable Loading

Priority: MEDIUM

Consumers: All four repos (as they are refactored to use standard env vars)

What to include:

// Validated environment variable loading with clear error messages
export function loadConfig(schema: ConfigSchema): Config;

// Standard config schemas
export const reverb_backend_schema: ConfigSchema;
export const zoe_schema: ConfigSchema;

Why: The current four approaches (inline defaults, @@PLACEHOLDER@@, config.js functions, .env files) are all different. Standardizing on .env with validated loading (e.g., using zod or envalid) ensures secrets fail loudly when missing rather than silently using defaults.


Priority: LOW-MEDIUM

Consumers: reverb-backend (replace soreto-cookie-jam), soreto-melissa (if cookie handling is needed)

What to include: - Cookie packing/unpacking (from soreto-cookie-jam) - JWT decode utilities - Session token validation helpers

Note: Authentication logic (passport strategies, login flows) stays in reverb-backend. Only generic, stateless utilities belong here.


Tooling Packages

These are infrastructure packages that reduce configuration drift:

Package What it provides Priority
@soreto/eslint-config Shared ESLint baseline (extends eslint:recommended + TypeScript) HIGH
@soreto/tsconfig Shared TypeScript base configs (tsconfig.base.json) HIGH
@soreto/jest-config Shared Jest configuration with standard transforms MEDIUM
@soreto/prettier-config Shared Prettier rules LOW

@soreto/tsconfig example:

// packages/tsconfig/base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}

Frontend Shared Packages

These are specific to the frontend apps:

@soreto/ui — Shared PrimeReact Component Wrappers

Priority: MEDIUM (becomes relevant once reverb-react modernization on Next.js 16 is complete)

Once both apps/legacy-portal/ (modernized reverb-react) and apps/platform-ui/ (melissa) are on Next.js 16 + React 19 + PrimeReact 10, a shared UI component wrapper package becomes viable. Both apps will share the same React version and component library, making extraction worthwhile. Don't extract prematurely — wait until the rewrite is complete and duplication is confirmed.

@soreto/hooks — Shared React Hooks

Priority: LOW (defer until a second frontend exists)

Same reasoning — premature extraction without a second consumer creates a junk drawer.


What Should NOT Become Shared

Item Reason
Database migration files Each app owns its schema — migrations must stay colocated
Passport.js authentication strategies Implementation detail of reverb-backend
Affiliate processor logic (Awin, Rakuten, etc.) Business logic specific to soreto-zoe
Email templates Context-specific, managed by the service that sends them
Express middleware App-specific request/response concerns
Business service implementations (campaign.js, reward.js) Domain logic, not infrastructure
Next.js app/ route files Not shareable by framework design
Soreto-specific UI components Wait for second consumer before extracting

Avoiding the Junk Drawer

Rules to enforce:

  1. Every shared package must have a CODEOWNERS entry mapping it to a specific team or person.
  2. Every shared package must have a CHANGELOG.md updated on each release.
  3. No shared package may depend on another shared package's business logic. Dependencies between packages: @soreto/api-client may depend on @soreto/types. @soreto/types may not depend on @soreto/api-client.
  4. If a shared package is only imported by one app, move it back. Run pnpm why @soreto/constants quarterly.
  5. Shared packages must have their own tests. No test coverage = no shipping.

Proposed Package Dependency Graph

@soreto/types         (no soreto deps)
      │
      ▼
@soreto/constants     (depends on @soreto/types for type annotations)
      │
      ▼
@soreto/api-client    (depends on @soreto/types for response types)
      │
      ▼
apps/zoe              (uses api-client, types, constants)
apps/platform-ui      (uses types, constants, ui-components)
apps/platform-api     (uses types, constants — after TS migration)

@soreto/eslint-config (no soreto deps — pure tooling)
@soreto/tsconfig      (no soreto deps — pure tooling)
@soreto/jest-config   (no soreto deps — pure tooling)