Skip to content

Internal admin

Who runs MerchOS

The internal admin (internal.yourcustommerch.ca) is the surface YCM’s two cofounders use every day:

  • Jess — runs the business side: customer acquisition, billing, design queue oversight, brand voice.
  • Joy — runs the catalog: curation, vertical decks, supplier relationships.

Plus INTERNAL_DESIGNER users (junior team members) who work the design queue without business-side access.

The internal admin is the only portal with all roles. The other three apps (storefront, company-admin, marketing) are role-specific.

The left rail — every surface at a glance

Pipeline
└── (CRM-style leads → opportunities → customers funnel)
Companies
├── List + filter + search
├── /companies/[id] — full company detail + onboarding state
└── /companies/[id]/billing — tier swap, invoices, refunds
Orders
├── All orders (every company)
├── Per-company drill-in
└── Refund interface (SUPER_ADMIN only)
Design queue
├── PRINT_FILE tickets (INTERNAL_DESIGNER works this)
└── LOGO tickets (logo add-on customers)
Catalog
├── Raw — supplier mirror, triage entry point
├── Master — the canonical Skus
├── Verticals — per-vertical published catalogs
├── Categories — YcmCategory editor
├── Audit trail — every catalog mutation
├── Vertical curation health — orphan detection
├── Cost alerts — supplier price-drift alarms
└── Digest — Monday 9am weekly email preview
Fundraising markup
└── Outstanding balances + monthly payout batches
Tickets
├── Refund requests
└── Issue reports (employee-submitted)
Launch readiness
└── Pre-go-live checklist per Company
Tasks
└── Operator todo list (cross-cutting)

Every page is brand-themed via the internal admin’s theme picker (default tidewater; six alt-themes — three cosmic, three beach). Theme switcher in the topbar.

The catalog pipeline — Joy’s daily loop

The four modes from Chapter 6 each have a dedicated page:

Raw — /catalog/raw

Joy’s entry point most mornings. Lists the supplier-mirror as ~5K style-group rows (matview-backed). Filters: vertical, CA-fulfillable, category. Each row carries a representative image, variant count, price range, CA-fulfillable count, suppliers list.

Hover a row → quick-action drawer with:

  • Promote — creates a DRAFT Sku from the RawStyle. Auto-prices at 2.5× supplier cost; copies sizes/colors/image; carries over ≥ MID-confidence AI vertical suggestions into SkuVerticalDraft.
  • Defer — flags as MAYBE.
  • Reject — structured RejectReason enum (NOT_MERCH / POOR_QUALITY / DUPLICATE / UNSHIPPABLE / WRONG_PRICE_POINT / WRONG_AUDIENCE / OTHER) + required note for OTHER.
  • Pull back — REJECTED/MAYBE → NEW.

All triage actions are bulk + idempotent — Joy multi-selects rows, action applies to all, partial failure doesn’t kill the batch.

Master — /catalog/master

The canonical Sku list. Three-pane drawer per Sku:

  • Identity tab — name, code, category, brand, supplier, status (DRAFT / ACTIVE / DISCONTINUED).
  • Colors / Sizes / Print Options tabs — axis editing. Add a color → it’s available across every size + print automatically (per the axis-model rationale in Chapter 6).
  • Fulfillment tab — supplier coverage, per-(color × size × print) cell-state, manual exclusions. Refresh button re-runs the supplier-sync hook on demand.
  • Curation tab — per-vertical print-option curation (ADR-0071) — Joy picks which print methods to recommend per vertical.
  • Audit tab — every mutation on this Sku since promote, with actor + timestamp.

Demote (Phase 0155, 2026-05-19) — the “drop accidentally-promoted Sku back to Raw” escape hatch. Visible only on DRAFT Skus. Two-step modal:

  1. previewDemoteImpact(skuId) lists any Company Products that would block the demote (NoAction FK gate).
  2. Confirm → CASCADE wipes the Sku + all child rows; CuratedStyle.status flips back to NEW so the underlying RawStyle re-appears in Joy’s Raw queue.

Audit-event written for forensics. Integration-tested against real Postgres (master.service.int.spec.ts, 7 tests).

Verticals — /catalog/verticals

One row per active vertical. Each carries:

  • Published Sku count
  • Draft Sku count
  • “Has unpublished changes” flag
  • Last published date
  • Published-vs-draft diff summary {added, removed, reordered}

Drill into /catalog/verticals/[code] for the per-vertical editor:

  • Draft layer — drag-to-reorder + per-Sku in/out + per-print-option curation.
  • Publish button — runs the VerticalsService.publish atomic-replace tx (Chapter 6 for the load-bearing details). Storefronts pick up the new order on the next request.
  • Revert button — copies published rows back into the draft layer.
  • Snapshot history — Phase 0122 / ADR-0073 added auto-snapshots on every publish; revert against any past snapshot.

Vertical curation health — /catalog/vertical-curation-health

Cross-vertical orphan scan. A SkuVertical row with zero current SkuVerticalPrintOption recommendations + a Sku that has at least one active SkuPrintOption is flagged as a probable orphan (per the heuristic in isOrphanedCuration).

Each row links back to /catalog/master?skuId=… for in-place re-curation. Heartbeat icon next to the page link in the left rail when count > 0.

Audit trail — /catalog/audit-trail

Every catalog mutation since system inception: promotes, rejects, demotes, merges, vertical publishes, axis edits, mapping inserts. SUPER_ADMIN-only.

Filters: by Sku, by actor, by action type, by date range. CSV export.

The audit trail is the ground truth when something goes wrong — Joy can ask “who reordered this vertical last week?” and the answer is one query away.

Companies — /companies

Per-Company view with:

  • Identity — slug, displayName, country, billingCurrency, vertical, pricingModel.
  • Onboarding state — current OnboardingStatus + transition history via OnboardingService.getHistory(companyId).
  • Billing — tier, subscription state, next invoice, refund history, SUBSCRIPTION_WAIVED toggle.
  • Employees — invited / active / churned counts + roster.
  • Products — every Product in this company’s catalog.
  • Orders — every order, paginated.

Lifecycle actions (SUPER_ADMIN only):

  • Pause / Resume the storefront.
  • Force a state-machine transition (with audit trail).
  • Activate manually (override activatedAt if Stripe drift).
  • Issue a setup-fee credit-back (when SUBSCRIPTION_WAIVED isn’t enough).

Design queue — /design-queue

DesignTicket rows grouped by status:

  • PENDING_DESIGN
  • IN_REVIEW
  • APPROVED
  • REJECTED (needs rework)

INTERNAL_DESIGNER users see only their assigned tickets by default; SUPER_ADMIN sees all. Per-ticket page has upload + revision history + approve/reject buttons.

When all DesignTicket rows for a Company reach APPROVED, the activation cron flips the Company to LIVE. The activation is automatic but requires the human to approve the last ticket.

Orders — /orders

All orders across all companies + a per-company filter. Each row shows the multi-package status (FulfillmentTask aggregation) and links to the full order detail.

Refunds (SUPER_ADMIN only) — /orders/[id]/refund lets the operator issue:

  • A Stripe refund on anonymous / MEMBER_PAYS / split-tender orders (full or partial).
  • A RefundRequest row for gated company-paid orders (manual operator follow-up — credit on next invoice).

Split-tender orders refund LIFO (employee portion first via Stripe, then company rail via SpendRecord restore — see Pricing models).

Fundraising markup — /fundraising-markup

The custodial-liability ledger. Three views:

  • Outstanding balances — per-Company FundraisingMarkupEntry rows where status != PAID. Aggregate balance per Company.
  • Cut a payout batch — multi-select unpaid entries + create a FundraisingMarkupPayoutBatch. Marks entries ASSIGNED_TO_BATCH.
  • Mark batch paid — flip batch + all child entries to PAID. Audit-logged.

CSV export at every level for the bookkeeper’s monthly close.

Tickets — /refund-requests + /tickets

RefundRequest rows: anonymous shoppers / employees flagging a problem on a delivered order. Each carries status, reason, conversation thread.

The Postmark Inbound integration (apps/api/src/modules/tickets/) creates ticket rows from inbound emails to support@yourcustommerch.ca — the operator can reply from inside the internal admin and the customer sees a normal email thread.

Launch readiness — /launch-readiness

Per-Company pre-go-live checklist (Phase 4 / ADR-0089). 12 checks per Product covering:

  • Has approved print file?
  • Has zone coverage matching the print method?
  • Has color + size axis populated?
  • Has at least one fulfillable variant per supplier link?
  • (more — see ADR-0089 §Checks)

Failing checks gate Sku.activate() server-side. Launch readiness is the surface where Joy + the customer’s admin walk the failures together pre-go-live.

Tasks — /tasks

A cross-cutting operator todo list (Phase 0144). Auto-populated by system events (e.g. “Company Acme — print files for SKU-1234 missing”, “Customer-photo upload from Beta Co needs approval”) + manually-added items.

Search — ⌘K

SearchService cross-mode palette (Chapter 6). Searches Skus, RawStyles, CuratedStyles, SupplierProducts, Companies simultaneously. Ranked by relevance.

The most-used operator surface after Raw + Master — Joy can search “gildan 64000” and jump to whichever mode is most useful.

Themes — /settings/themes

The internal admin has seven themes:

  • tidewater (default — Stripe/Notion-style light)
  • 3 cosmic (twilight, aurora, nova)
  • 3 beach (coastline, lagoon, sunset)

Topbar picker switches live. Theme choice is per-user (stored in User.preferences), not per-company.

Guidance system (Phase 0121)

ADR-0072. Every internal-admin page uses <PageHeader> with both title and description props. Help-popovers (<HelpPopover>) explain non-obvious surfaces in plain English so the next operator (or a returning operator after months away) can self-onboard.

<EmptyState>, <NextStepCallout>, <DisabledReason> are the three other reusable guidance components. Custom ESLint rule to enforce <PageHeader> description is in POST_DEPLOY_BACKLOG.md.

What’s next


Canonical sources

Triggers for update

Update this chapter if you:

  • Add or retire a left-rail surface.
  • Add a new drawer tab to the Master Sku detail view.
  • Change the demote, promote, or vertical-publish flow shape.
  • Add a new theme (and update the seven listed here).
  • Add or change a role in the role-to-surface matrix.
  • Move RefundRequest / Tickets / Fundraising markup to a different module.
  • Add a new guidance component beyond the five named above.