Architecture Overview
The core principle
Business logic lives once. Every interface — admin UI, REST endpoint, MCP tool, CLI command, webhook — is a thin adapter on top of a service layer.
┌─────────────────────────────────────────────────────────────┐
│ ShipMore Service Layer │
│ │
│ PageService MonetizationService LeadService │
│ TenantService MediaService AnalyticsService │
│ ComposerService ImportService ObservationsService │
│ │
│ (all business logic — uses Payload Local API) │
└──────────┬──────────┬──────────┬──────────┬──────────────────┘
│ │ │ │
┌───────▼──┐ ┌────▼────┐ ┌──▼────┐ ┌─▼──────┐
│ MCP │ │ REST │ │ CLI │ │Webhooks│
│ plugin │ │(Payload │ │ binary│ │ + cron │
│ (agents) │ │ + app) │ │ │ │ │
└──────────┘ └─────────┘ └───────┘ └────────┘When the ecosystem moves — new agent protocol, new tool, new platform — ShipMore extends an interface. Logic doesn’t get re-implemented per surface.
The four pillars
ShipMore is a self-hosted operating system for data products. Operators import a dataset; the platform handles four pillars without per-archetype customization:
| Pillar | What it does | Where it lives |
|---|---|---|
| Present | Cards, detail pages, listings, search | domains/cms/components/ + domains/present/orama/ |
| Monetize | Featured slots, gated content, paywall, four Stripe types | domains/monetization/ |
| Monitor | Per-tenant observations, /operate dashboard | domains/monitor/ |
| Operate | Records, schemas, imports, moderation | Payload admin + CLI + MCP |
Three-level access model
| Level | Interface | Used by |
|---|---|---|
| 1 — Agent | MCP plugin (primary), CLI, REST | Claude Code, MCP-capable clients, scripted operators |
| 2 — Admin | Payload admin panel | Humans reviewing, approving, auditing |
| 3 — Code | Next.js + custom domain code | Bespoke blocks, integrations |
Agents create content as drafts by default; humans review and publish via the Payload admin. Every agent action is auditable and reversible through Payload’s version history.
Interface boundaries
All three Payload-native protocol surfaces (REST, MCP, GraphQL) run in-process on the ShipMore box. They are different protocols over the same service layer — not separate systems.
ShipMore box (Railway / Vercel)
├── REST API (Payload built-in) ─┐
├── MCP server (plugin-mcp) ─┼──→ service layer → Payload Local API → MongoDB
└── GraphQL (Payload built-in) ─┘
External clients
└── shipmore CLI binary ──→ REST API ──→ ShipMore boxTenant-facing public API (a tenant’s data product serving its own end users) lives in src/app/(frontend)/, completely separate from Payload’s /api/ namespace.
The agent boundary
ShipMore has no LLM. No model SDK, no API key, no inference calls inside the platform. The intelligence layer is the operator’s harness — Claude Code, Claude Desktop, any MCP client — driven by the SKILL.md shipped with the box.
The split:
- ShipMore — deterministic primitives. Commands, endpoints, heuristic inference, storage. Produces structured facts.
- Harness agent — all judgment. Reads raw data, interprets heuristic output, decides what to change.
SKILL.md teaches agents how to think, not what to decide. See Agents & CLI.
Architectural rules
Dependency injection. Domains expose pure services and utils. They do not import getPayload(), stripe, or other root-owned clients directly — those are instantiated at root (src/lib/) and passed in.
Server actions. All 'use server' actions live in src/actions/ only. Domains do not export action-generator functions.
Tenant scoping. The service layer receives tenantId as an argument. It does not resolve “which tenants can this user access?” — Payload RBAC handles that.
Logic lives once. Never put business logic inside MCP tool handlers, REST route handlers, or webhook handlers. Put it in the service layer.
Search boundary. Two separate search layers, kept isolated:
@payloadcms/plugin-search— global editorial search overposts. Do not addrecordsto it.- Orama (
src/domains/present/orama/) — per-tenant in-memory index overrecordsonly. Synchronous (v3+); registry is a Node module-level singleton — never useruntime = 'edge'on Present routes.
Operational constraints
The single-box, multi-tenant deployment model places several shared components on the same Node process:
- Per-tenant Orama indexes live in-process. Scales with records-per-tenant × indexed-field size.
- Import peak memory ≈ 2–3× file size. Capped by
IMPORT_MAX_FILE_SIZE_MB(default 100 MB). - Payload Local API + Next.js baseline ≈ 250 MB.
Recommended box sizes:
| Tenants | Reasonable baseline | Comfortable |
|---|---|---|
| 1 | 1 GB | 2 GB |
| 3 | 2 GB | 4 GB |
| 5+ | reconsider single-box model |