@latimer-woods-tech/operator
v0.8.0
Published
The shared **operator platform** — the portfolio "vending machine" (Factory#1947, [`docs/PORTFOLIO.md`](../../docs/PORTFOLIO.md) layer L2). Built once; every product mounts it instead of hand-rolling a client hierarchy or billing.
Readme
@latimer-woods-tech/operator
The shared operator platform — the portfolio "vending machine" (Factory#1947,
docs/PORTFOLIO.md layer L2). Built once; every product
mounts it instead of hand-rolling a client hierarchy or billing.
Operators (training consultancies, practitioners, barbers, agencies, developers)
buy wholesale, sell at their own retail under their own brand, and the platform
provisions, meters, and collects automatically on the shared Stripe Connect
platform (acct_1SlCcFAW1229TZte, Express connected accounts). The platform
never holds a balance — money moves via direct/destination charges with an
application fee, and the ledger is the source of truth for earnings and payouts.
What this package owns
- Operator identity + white-label config (name/logo/colors/support/domain)
- Operator→client hierarchy (
operator_clients, mapped to each app's native org id) - Wholesale/retail price split (
price_books; supports a no-rake mode for selfprime's practitioner network —platform_fee_bps = 0) - Usage metering hooks → ledger
- Append-only rev-share ledger (
ledger_entries, idempotent) + payouts
Status — Phase 2 (money, test-mode)
Phase 0 shipped the data model and API surface; Phase 1 the identity +
hierarchy + white-label slice. Phase 2 completes the money surface, test-mode
throughout: the merchant-of-record-independent pricing core (wholesale/retail
price books, effective-price resolution, rev-share split), metering + the
derived ledger (recordUsage, the derived balance, ledger listing, the
raw-entry escape hatch), and now the live-money Connect surface
(OperatorMoneyService: Express onboarding, direct-charge collection with the
rev-share split, payout settlement of the derived balance, and payout-status
reconciliation). Charges use operator direct charges + an application fee
(operator is merchant of record per the Factory#1947 V1 directive + ADR-002), so
the platform never holds a balance. Everything runs in Stripe test mode; the
live-mode switch stays founder-gated.
| Phase | Scope |
|-------|-------|
| 0 ✅ | Schema, migration, API surface, ADR |
| 1 ✅ | OperatorIdentityService (identity + hierarchy + white-label), OperatorStore port + in-memory adapter, validation + lifecycle rules, tests |
| 2 ✅ | Money (test-mode). Pricing core (OperatorPricingService) · Metering + ledger (OperatorMeteringService) · Connect surface (OperatorMoneyService: startConnectOnboarding, collectRetailCharge, schedulePayout, markPayoutStatus, listPayouts). Live-mode + npm publish founder-gated |
| 3 | npm publish + integration examples (XPElevator #16 Phase 4, selfprime migration) |
import { OperatorIdentityService, InMemoryOperatorStore } from '@latimer-woods-tech/operator';
const platform = new OperatorIdentityService({ store: new InMemoryOperatorStore() });
const operator = await platform.createOperator({ slug: 'acme-barbers', displayName: 'Acme Barbers' });
await platform.updateWhiteLabel(operator.id, { brandName: 'Acme', primaryColor: '#1a1a2e' });
const client = await platform.createClientOrg({ operatorId: operator.id, name: 'Downtown', externalOrgId: 'org-42' });Pricing (Phase 2) mounts the same store — the platform sets a wholesale
catalogue, the operator sets their retail, and getEffectivePrice resolves the
split (no money moves; see ADR-002 for the merchant-of-record decision that
gates live charges):
import { OperatorPricingService, InMemoryOperatorStore } from '@latimer-woods-tech/operator';
const store = new InMemoryOperatorStore();
const pricing = new OperatorPricingService({ store });
await pricing.setPlatformWholesale({ sku: 'seat.voice', priceModel: 'seat', wholesaleAmountCents: 7000 });
await pricing.setOperatorRetail({ operatorId: operator.id, sku: 'seat.voice', retailAmountCents: 10_000 });
const quote = await pricing.getEffectivePrice(operator.id, 'seat.voice');
// → { retailAmountCents: 10000, wholesaleAmountCents: 7000, operatorMarginCents: 3000, ... }
const split = pricing.computeRevShare(quote!); // pure: gross / platformFee / operatorEarningMetering (Phase 2) accrues what the operator has earned from metered usage onto the append-only ledger and derives their balance from it — still no money moves (that's the ADR-002-gated charge/payout slice). Each event carries an idempotency key, so a metering retry can never double-post:
import { OperatorMeteringService, InMemoryOperatorStore } from '@latimer-woods-tech/operator';
const metering = new OperatorMeteringService({ store });
// margin 3000/seat (retail 10000 − platform cut 7000) × 2 seats = 6000 accrued
await metering.recordUsage({ operatorId: operator.id, sku: 'seat.voice', quantity: 2, idempotencyKey: 'inv-2026-07:seat.voice' });
const balance = await metering.getOperatorBalance(operator.id);
// → { availableCents: 6000, lifetimeEarnedCents: 6000, lifetimePaidCents: 0, ... } (Σ earning − Σ payout)
const entries = await metering.listLedgerEntries(operator.id, { entryType: 'operator_earning' });The money surface (Phase 2, test-mode) mounts the same store plus a
StripeConnectPort. The operator onboards to Stripe Express, a buyer is charged
on the operator's connected account with the platform's application fee, and a
payout settles the derived balance. A charge posts one balance-affecting
operator_earning row plus audit-only charge/platform_fee rows, so the
Σ operator_earning − Σ payout balance invariant holds:
import { OperatorMoneyService, InMemoryOperatorStore } from '@latimer-woods-tech/operator';
const money = new OperatorMoneyService({ store, connect }); // connect = a StripeConnectPort
await money.startConnectOnboarding(operator.id, { refreshUrl, returnUrl }); // Express account + link
const split = await money.collectRetailCharge({
operatorId: operator.id,
sku: 'seat.voice',
quote: quote!, // from pricing.getEffectivePrice
idempotencyKey: 'order-42', // retry-safe: a repeat rejects at the ledger
});
// → { grossCents: 10000, platformFeeCents: 7000, operatorEarningCents: 3000, currency: 'usd' }
const payout = await money.schedulePayout(operator.id, { periodStart, periodEnd }); // pays the balance
await money.markPayoutStatus(payout.id, 'paid'); // or 'failed' → posts a compensating reversalSwap InMemoryOperatorStore for a @latimer-woods-tech/neon FactoryDb-backed
adapter (Phase 3) — the services are unchanged.
Vending protocol (Phase 3)
When a buyer purchases through an operator's page, the hub tells the consuming app to provision the tenant and to grant/revoke entitlement — over a signed webhook, never a shared database (Severability law; Factory#1947 V1 directive D1). This package owns the wire format so no product hand-rolls it.
import { signVendingEvent, verifyVendingWebhook } from '@latimer-woods-tech/operator';
// hub side — emit a provisioning event
const { payload, signature } = await signVendingEvent(
{ id: 'evt_1', type: 'provisioning.requested', createdAt: Math.floor(Date.now() / 1000),
data: { operatorId: 'op_1', sku: 'xpelevator.simulator', purchaseRef: 'cs_test_123' } },
env.VENDING_SIGNING_SECRET,
);
// app side — verify before acting (throws on bad signature / stale replay / malformed body)
const event = await verifyVendingWebhook(rawBody, req.headers.get('Operator-Signature')!, env.VENDING_SIGNING_SECRET);
if (event.type === 'provisioning.requested') await provisionTenant(event.data); // typedHMAC-SHA256 via Web Crypto (crypto.subtle), a t=<unix>,v1=<hmac> header, a
constant-time signature check, and a ±tolerance replay window. Design +
alternatives in docs/ADR-003-vending-protocol.md.
App-side entitlement client
The other half of D1: an app answers "may this tenant use this SKU?" by asking
the hub's API, cached for a short TTL, with entitlement.* webhooks used as
cache invalidation (entitlement is state, not a stream — a webhook is a
signal, the hub API is the truth).
import { createFetchHubClient, CachedEntitlementChecker } from '@latimer-woods-tech/operator';
// once per Worker — a hub client wrapped in a short-TTL cache
const checker = new CachedEntitlementChecker({
port: createFetchHubClient({ baseUrl: env.HUB_BASE_URL, apiKey: env.HUB_API_KEY }),
ttlMs: 60_000,
});
// on each gated request
if (!(await checker.isEntitled({ operatorId, externalOrgId, sku }))) return forbidden();
// on an entitlement webhook — invalidate, then the next check re-reads the hub
const event = await verifyVendingWebhook(rawBody, sigHeader, env.VENDING_SIGNING_SECRET);
checker.invalidateFromEvent(event);createFetchHubClient uses error-handled fetch (a rejected call or 5xx →
InternalError, 401/403 → AuthError, malformed body → ValidationError)
against a branded-HTTPS base URL only. The API key is injected from a Worker
binding, never read from the environment here.
Persistence — NeonOperatorStore (Phase 3)
Every service (OperatorIdentityService, OperatorPricingService,
OperatorMeteringService, OperatorMoneyService) depends only on the narrow
OperatorStore port. InMemoryOperatorStore is the reference for tests and
local flows; NeonOperatorStore is the durable adapter — a drop-in behind the
same port, so swapping it in changes nothing at the service layer.
import { createDb } from '@latimer-woods-tech/neon';
import { NeonOperatorStore, OperatorIdentityService } from '@latimer-woods-tech/operator';
// A FactoryDb (Hyperdrive-bound Neon) satisfies the OperatorDb port directly.
const store = new NeonOperatorStore(createDb(env.DB));
const identity = new OperatorIdentityService(store);It composes parameterized sql statements (identifiers come from a fixed
allow-list, never caller data), lets the database fill column defaults via
RETURNING *, and maps the driver's 23505 unique violations back to the
ValidationError the port promises — so a duplicate slug, connect-account, or
ledger idempotency key rejects exactly as the in-memory reference does. It
depends only on a structural OperatorDb port (the subset of
@latimer-woods-tech/neon's FactoryDb it calls), keeping the published bundle
free of the postgres driver's Node built-ins. Apply migrations/0001_operator_core.sql
to provision the tables.
Consumers
Derived from real requirements, not speculation:
- XPElevator #16 Phase 4 — self-serve operator onboarding, white-label workspace, client orgs, wholesale seat billing with operator-set retail.
- selfprime practitioner network (HumanDesign ADR-002/003) — practitioner as operator, clients/leads beneath them, no-rake display-only payments.
See docs/ADR-001-operator-hierarchy-and-ledger.md,
docs/API_SURFACE.md, and per-app integration specs in
docs/integrations/ — e.g.
XPElevator, with an executable contract test at
src/integrations/xpelevator.integration.test.ts.
Usage (once implemented)
import type { OperatorPlatform } from '@latimer-woods-tech/operator';
import { operators, ledgerEntries } from '@latimer-woods-tech/operator';
// Schema tables plug into a @latimer-woods-tech/neon FactoryDb.
// The OperatorPlatform service (Phase 1+) is the one surface products mount.