Our Tech estate Context and architecture

Technical briefing

Read this before you change a Worker, a theme file, or a P&L formula. It is the context of the running estate — not a sign-in tutorial.

1

What the estate is

A UK Shopify merchant. Customers see the shop, the store mailbox, and track. Everything else is back office.

2

Two repositories

profit-admin is the desk product. business-ops is the Our Tech instance and the agent workspace.

3

Invariants

P&L stays in Python. Decisions stay in D1. Access is not membership.

4

Then zoom

C4 diagrams: estate context, then the desk containers and components.

What this estate is

Our Tech Accessories is a UK-only Shopify shop (ourtechaccessories.com). It sells everyday tech accessories (chargers, MagSafe banks, mounts, cases). There is no high-street stock. A partner packs the box — mostly CJ Dropshipping, with leftover AliExpress / DSers fulfilments. James is still the merchant: the card charge, the brand, and “where is my order?” sit here.

The public face is three things: the storefront, the store mailbox, and /pages/track. Profit Admin, Meta Ads Manager, supplier consoles, and this git workspace are operator surfaces. Customers never log into those.

flowchart TB
  subgraph public [Customer-visible]
    store["Shopify storefront"]
    hello["Store mailbox"]
    track["/pages/track"]
  end
  subgraph edge [Edge]
    mx["Mail forwarding"]
    proxy["track-proxy Worker"]
    access["Cloudflare Access"]
  end
  subgraph back [Back office]
    desk["Profit Admin"]
    gmail["Store Gmail + desk Assist"]
    meta["Meta ads + Pixel"]
    cj["CJ / DSers / Alibaba"]
    agent["Cursor in business-ops"]
  end
  store --> meta
  store --> cj
  hello --> mx --> gmail
  track --> proxy --> cj
  access --> desk
  desk --> store
  desk --> meta
  desk --> cj
  agent --> desk
  agent --> store
  agent --> meta
        

Money, parcels, and mail each have one owner. Do not add a second path that the customer or the P&L will see twice.

Two repositories

Repo Job Do not
JamesMMiller/profit-admin The desk product: Worker gateway, FastAPI engine, React UI, D1. Submodule at apps/profit-admin. Put Our Tech secrets, theme edits, or agent skills here.
JamesMMiller/business-ops The Our Tech instance: local configuration, skills, storefront ops, track-proxy, inbox, estate docs, and the submodule pin. Fork P&L into a script in this repo. Call pnl.py.

Canonical skill edits live in skills/ and shared/skills/, then ./scripts/sync-skill.sh. Copies under .cursor/skills/ are generated. Apps Script is paste-deployed: a git change does nothing until someone pastes it into the store Google account.

The public briefing at jamesmmiller.github.io/profit-admin-docs is a copy of apps/profit-admin/site/. After you change those files, run ./scripts/publish-docs.sh. Do not edit the Pages repo by hand.

Live surfaces

Surface Where Notes
Store ourtechaccessories.com Apex primary. Campaign PDP handles are URL-locked.
Track /pages/track apps/track-proxy — not the ops desk. Keeps the supplier credential off the storefront.
Mail Store mailbox → forwarding → store Gmail Desk Mail Assist drafts pitch/FAQ/deal and tracking; you send. The old inbox bot is retired once desk drafts have proven themselves.
Desk production desk.ourtechaccessories.com Access, then D1 membership. Container cold-start can take a minute.
Desk staging Separate staging desk (address kept in the private operator notes) Desk development only. Own Container. Shared D1, namespaced by environment.
Laptop engine http://127.0.0.1:8787 No Worker. Seed owner from the business-ops local configuration.

The Android Profit Admin PWA is retired. Phone and laptop use the hosted desk. Worker ota-profit-admin only redirects there.

Hosted desk — Ads

Ads → Performance defaults to the whole Meta ad account. Split by campaign, ad set, or ad overlays that level on the daily totals. Intra-day lists every ad that delivered in the current scope, with a day sparkline plus spend, share, purchases, ROAS, and CTR. Click a row to plot that ad on the hourly chart (up to six; four when comparing two periods). Dashed lines are the comparison set. Views, named subsets, SKUs, and object pickers live under Advanced — they are not the first screen.

Empty object ids on Insights means account-level. The same rule applies to the machine API split field (campaign / adset / ad).

Hosted desk — Mail

Mail shows the store inbox for an owned address such as the store mailbox. Genuine customer mail that still needs a reply is listed first. Filters include Assist drafts and Needs you. Assist is two-tier triage: a cheap path drafts pitch, FAQ, and volume-deal replies from built-in seed templates; a tool-using agent drafts tracking replies from Shopify and track-proxy facts; refunds, returns, and damage escalate with no draft by default. Mail → Assist has presets (safe drafts, auto cheap, human only, paused), per-category action chips, and an effective-routing summary (auto-send needs an explicit master switch; refund never auto-sends). You still send through the Gmail API unless a category is set to auto-send. A scheduled poll drafts replies in the background. A saved reply can also be added through the API. Edit a template under Mail → Templates (built-ins save as an org override with the same id). The mailbox is still Gmail, with the store address forwarded to it. Once desk drafts have proven themselves, the old inbox bot is retired; fulfilment “on its way” mail goes track-proxy → Profit Admin. The Gmail connection is stored in D1, so a Container replace does not drop the mailbox connection. Empty saves never clear the refresh token or owned addresses; only Disconnect deletes the row.

Components, sequence diagrams, the draft lifecycle, and a “no drafts appearing” checklist are on the Mail system page.

How a sale becomes a desk number

  1. Meta (or organic) lands on a locked PDP handle. Do not rename live campaign URLs; hot-swap the product under the handle.
  2. Shopify checkout. Payments Basic UK — the desk assumes 1.5% + £0.25.
  3. Fulfilment: CJ app or leftover DSers. Tracking lands on the Shopify fulfilment; track-proxy verifies the webhook, caches CJ, and can fire branded “on its way” mail.
  4. Profit Admin collectors pull Shopify orders, Meta Insights, and landed COGS (DSers ledger → CJ API → Shopify unitCost). pnl.py is the only formula.
  5. Hosted: Shopify webhooks bump the orders clock; a 15-minute cron bumps the ads clock and a short tracking batch for Delivery times. The desk polls clocks and reloads. Full Refresh still owns CJ freight and catalog.

Meta has no spend webhook — Ads can lag 15 minutes. A new webhook order may sit on unitCost until the next full Refresh quotes freight. Never invent cost on a Worker-only path.

Finance on the desk is that same P&L, summed for a period (all time, this month, last 7 days, last 30 days) against the matching prior window. It is a management view, not a tax computation. Bank readings, and any large debtor or creditor, are figures an owner types in. A reading can be removed, and it can store the all-time profit beside the balance. There is no bank feed. Customers pay at checkout, so the desk does not keep an aged debtor ledger.

Who owns which data

Data System of record Ephemeral / derived
Users, orgs, roles, API keys, decisions D1 on the Worker Engine disk is not a decision log. MASTER_CONTEXT.md is not either.
P&L, snapshots, clocks, views, watches state Engine org files (/data/… hosted, outputs/profit-admin/org/ laptop) The engine sleeps after 2 minutes of quiet. A long Refresh stays up until it finishes. Production's 15-minute cron wakes it; staging runs only the mail automation, hourly. Opening the desk does not wake it — the page is a Worker asset. The first API call after sleep can take about a minute. The engine disk is a cache and is empty after sleep: the last good copy of each org's desk files lives in that org's DeskHub Durable Object and is seeded back. R2 is not required; it is optional snapshot history only if a bucket is bound.
Recorded bank readings and debtor / creditor notes D1 finance_bank_entries and finance_balances Not a bank feed. Sales and profit on the Finance tab are summed from the engine P&L.
Org membership vs “can reach the hostname” D1 membership + usable subscription Cloudflare Access only answers “is this email allowed in?”
Store catalog, checkout, theme Shopify Git has theme sources and skills; live MAIN is a deploy.
Ad objects and spend Meta Desk Insights are a pull. New ads stay PAUSED.
Brand voice, dated ops notes Local MASTER_CONTEXT.md Changelog Read headings; do not ingest the whole file. Decisions that matter on the desk go to D1.

Invariants

P&L is Python. apps/profit-admin/pnl.py is the only formula. Do not reimplement contribution, MER, or ROAS in Worker JS.
Decisions are D1. POST /api/v1/decisions only. Do not prepend MASTER_CONTEXT.md. Staging and production share one D1; rows carry environment.
Access ≠ tenancy. Access is the human gate. No D1 membership → Under construction. A usable invite or beta invite can create an org by connecting Shopify (paste the store, approve, land on orders). Agents never use Access sessions; they use OAuth or an API key against /api/v1.
Default agent env is production for live Our Tech ops. Staging only while developing the desk. After a material desk write, commit and push profit-admin then business-ops. If the public briefing is now wrong, update site/ and run ./scripts/publish-docs.sh.
Campaign URLs are locked. Live ad landings keep their handle. Fulfilment changes move the product under that handle.
Meta ads stay PAUSED when created from the repo. James unpauses in Ads Manager. Default video for IG + Facebook feed is 1:1.
Shopify webhooks are not behind Access. They are verified by Shopify’s signature instead. Same idea on track-proxy fulfilments.
These do not run in the Container: Improve / Cursor jobs (business-ops cwd), DSers .xlsx watch, Apps Script, theme paste.

Trust boundary on the desk

Humans sign in through Cloudflare Access (Google). A separate operator-only fallback exists and is documented privately. The Worker passes the verified identity, role and organisation on to the engine, and in the hosted setup the engine refuses requests that did not come through the Worker (except public paths). Viewer POST to Refresh / Settings is 403.

Machines: POST /api/v1/oauth/token then Bearer access token (or a long-lived API key for scripts). OpenAPI is at $BASE/docs. Cloudflare admin credentials are for deployment and Access administration only — never the desk Bearer.

Where to change what

You want… Change…
P&L, collectors, Refresh, desk UI apps/profit-admin/ — then pin the submodule in business-ops. If the public briefing should describe the change, edit site/ and run ./scripts/publish-docs.sh
Public estate briefing / C4 apps/profit-admin/site/ then ./scripts/publish-docs.sh (pushes JamesMMiller/profit-admin-docs)
Track page / fulfilment mail page.track.liquid + apps/track-proxy + paste the mailer if HTML changed
PDP / homepage shopify-store skill, dry-run first
Inbox FAQ / deal JSON store-inbox skill, then paste Apps Script / upsert the page
Paused Meta ads meta-ad-builder into an existing ad set
Supplier listing research cj-dropshipping, tvc-mall, aliexpress-ds, alibaba-com — do not auto-publish

Read next

  1. C4 diagrams on this site — estate, then desk.
  2. docs/estate/technical.md — operator reference, purchase sequence, fulfilment lanes.
  3. profit-admin docs/architecture.md — request path, clocks, Refresh, storage layout.
  4. docs/api-agents.md + the profit-admin skill — only when you are calling the machine API.

Longer product tutorials stay in docs/tutorial. They assume you already have this map.