Status: decided (P0). Verified against Harper 5.2.12.
This repo is an Example under the org repository taxonomy: it must be clonable and deployable as-is, and it tracks the latest Harper release. But it also wants shared packages — a platform-neutral spec, a seed generator, and a portable e2e suite that runs against competitor implementations too.
Those two pull against each other. The alternative resolution is a vendor/ directory plus a bundle step, which works and costs a build stage plus a copy of every shared file. Plain workspaces avoid both.
The repo root is the Harper component root. Not a preference — a hard constraint, from two independent places:
- Harper does not implement npm's
#path:git extension.deploy_componentwithpackage=github:owner/reporesolves throughparseGitReference(components/Application.ts), which returnsnullfor a#path:committish. The caller treats that as "recognized as git, but not safely handleable" and fails loudly rather than falling back. Sopackage=github:HarperFast/ecommerce-store#path:apps/storeis not deployable. A single directory tree is the unit of deployment. - Harper framework plugins that wrap an application (
@harperfast/nextjs, for example) likewise expect their config at the component root.
Consequence: config.yaml, resources/ and schemas/ live at the top level, and packages/* are subdirectories of the deployed tree.
Note on (2): P0 has no web framework — it is two JSON endpoints, and Next.js, React and a shared component package were removed rather than carried as inert dependencies that ship ~300 MB to every node. Constraint (1) is sufficient on its own, so reintroducing a framework with the storefront reopens nothing here.
Verified empirically against the exact commands Harper runs, not inferred:
| Step | What Harper runs | Result |
|---|---|---|
| Pack | npm pack --json --ignore-scripts <clone> |
packages/** included intact; node_modules, .next excluded via .gitignore |
| Extract | tar to components/<project>, flattening one wrapper dir |
workspace dirs are real directories |
| Install | npm install --force --omit=dev --no-audit --no-fund (cwd = component root) |
node_modules/@ecommerce-store/{spec,ui,seed} → relative symlinks into packages/; 26 packages, no dev tooling |
| Runtime | import { … } from '@ecommerce-store/spec' |
resolves |
--install-links is added only on win32 for candidate builds; on POSIX (Fabric) npm links file:/workspace dependencies relatively, which survives the staging→live rename.
--omit=devat the root does not protect you from a workspace package's owndependencies. They are installed on the node regardless of how the root references that workspace.
Measured: a workspace listed in the root's devDependencies, carrying an external package in its own dependencies, was installed under --omit=dev. Moving that package to the workspace's devDependencies omitted it correctly.
So:
packages/spec— runtime. Rootdependencies. Keep its deps minimal.packages/seed— dev-only. Everything heavy goes in that workspace'sdevDependencies, never itsdependencies.
A dev-only workspace is still symlinked onto the node — that is harmless (source files only) and it is what lets the deployment double as an E2E target for Harper releases.
@ecommerce-store/e2e lives at the repo root, outside the packages/* glob, with its own package.json and its own node_modules. It consumes the spec via "@ecommerce-store/spec": "file:../packages/spec".
That looks like an inconsistency. Two reasons it is not.
Optional peer dependencies defeat --omit=dev. npm installs an optional peer as soon as anything in the resolution tree makes it satisfiable. A package that declares @playwright/test as an optional peer — web frameworks do — will therefore pull Playwright onto a deployed node merely because some workspace in the tree declares it, even in that workspace's devDependencies, even under --omit=dev. That is tens of megabytes of browser tooling on a production node, and nothing in the manifest shows it coming.
Keeping Playwright out of the workspace graph entirely is the only form of this that does not depend on which packages happen to be installed.
The e2e suite is not part of this application. It is the executable spec: it runs against any implementation via BASE_URL — Harper, Vercel, Supabase — so it does not belong in the dependency graph of one of them.
npm pack still ships e2e/** as source (kilobytes), so a deployed node remains a self-describing E2E target; its dependencies are simply never installed there.
If you add a dev tool anywhere under packages/*, re-run the deploy simulation in scripts/verify-deploy.sh and check the installed package count. Optional peer dependencies make this class of leak invisible in the manifest.
- If
node_modulesalready exists in the payload, install is skipped entirely and the runtime is treated as opaque for redeploy comparison. Never shipnode_modules. .npmignoreREPLACES.gitignorefor packing — it does not merge with it. Anything.gitignoreexcludes must be repeated there or it lands in the deployed component. It exists because the ~800 MB dataset is committed to git (it is the benchmark's contract) but must not ship in a deploy payload: it is loaded separately via the operations API, outside the measured run.devEngines.packageManagercan select pnpm/yarn, which then run with their own install defaults (dev dependencies included). We stay on npm deliberately: the zero-config path is the one a customer will clone, and it is the one Harper exercises by default.- A custom
install.commandmakes the whole installation opaque to Harper. Avoided.
ecommerce-store/ repo root = Harper component root
config.yaml component config; plugin ORDER is load-bearing (see file)
schemas/store.graphql the eight tables, @export'd, plus the derived ProductView cache
resources/*.js the two endpoints, the admin write surface, and their lib/
packages/
spec/ route contract + requirement ids — stack-neutral, runtime
seed/ deterministic dataset generator — dev-only
e2e/ Playwright suite, the executable spec — NOT a workspace
dataset/ the committed, checksummed dataset
scripts/ dataset load/verify, deploy simulation, run-record validation
bench/ the load harness
containers/ the containerized run topology and its orchestrator
docs/
SPEC.md numbered, stack-neutral requirements
bench/ and containers/ are measurement scaffolding, and scaffolding shared across comparisons belongs in the benchmarks repo. It is here because there is one implementation, so there is nothing yet to share it with, and a harness developed apart from the only target it runs against is a harness nobody runs.
It moves to the benchmarks repo when a second target makes it genuinely shared — which is the same point at which "containerized identically for every target" stops being vacuous.
Until then the separation is by directory, not by repo: nothing under resources/ or packages/ imports from bench/, and .npmignore keeps both out of a deployed component. A customer reading this reference should see an ecommerce application, not a benchmark rig.
server-timingis emitted inline, not by a plugin. Both endpoints set the header directly, which satisfiesOBS-001/OBS-003and needs no plugin ordering. The commented block inconfig.yamlrecords the ordering constraint for the day a plugin does the decomposition — earlier-registered http listeners are outer middleware layers, so it would have to be registered before any handler whose time it decomposes. Left commented rather than deleted because the constraint is the expensive part to rediscover.