Seller portal — monorepo membership & release flow¶
The seller-portal Vue 3 SPA lives in this monorepo at portals/seller-portal
and ships inside the FlyokaiMarketplace plugin: the compiled bundle is
served same-origin by the plugin, so a buyer/seller hits /seller-portal on the
Shopware host and the SPA talks back to /flyok-app, /flyok-portal and
/store-api on that same origin (no CORS, no separate deploy).
Why portals/ and not packages/ or plugins/¶
monorepo-builder.php lists only packages/ + plugins/ as package
directories, and bin/flyokai-split.sh / .github/workflows/split.yml mirror
each packages/* and plugins/* leaf (keyed by its composer.json name) out
to a read-only flyokai/* repo. portals/seller-portal is an npm package
(no composer.json) and a deliverable, not a consumable library — so it
sits under portals/, is excluded from the split, and has no read-only mirror.
The SPA <-> plugin /flyok-portal/* request/response DTO contract now versions
atomically in one repo.
Importing it (one-time, git subtree)¶
# from a CLEAN monorepo working tree:
git subtree add --prefix=portals/seller-portal git@github.fly:flyokai/seller-portal.git main
After this the standalone flyokai/seller-portal repo can be retired (or kept
as an archived mirror). Future SPA changes are normal monorepo commits under
portals/seller-portal.
How the bundle is served (runtime)¶
bin/build-portal.shrunsvite build --base=/bundles/flyokaimarketplace/portal/and copiesdist/intoplugins/FlyokaiMarketplace/src/Resources/public/portal(gitignored).bin/console assets:installpublishes that topublic/bundles/flyokaimarketplace/portal. Hashed/assets/*are served statically by the webserver.SellerPortalController(route scopeflyok-portal,auth_required=false) serves the HTML shell at/seller-portaland/seller-portal/{path}(deep links / refresh), injecting per-install runtime config:
<script>window.__PORTAL_CONFIG__ = {
"apiBase": "", // same-origin
"routerBase": "/seller-portal/", // Vue Router history base
"oauthClientId": "…", // FlyokaiMarketplace.config.sellerOauthClientId
"oauthClientSecret": "…", // FlyokaiMarketplace.config.sellerOauthClientSecret (public by design)
"salesChannelAccessKey": "…", // first active storefront sales channel
"marketplacePrefix": "/flyok-portal" // where the Base plugin serves the marketplace API
};</script>
- The SPA reads these via
src/config.ts(portalConfig), falling back toimport.meta.env.VITE_*sonpm run devagainst the Vite proxy still works unchanged. One built bundle works on any install — credentials are not baked in.
Asset base vs router base¶
Two independent prefixes:
- Vite base = /bundles/flyokaimarketplace/portal/ — where /assets/*
load from (set via the --base build flag; vite.config.ts is untouched).
- Router base = /seller-portal/ — where the app is mounted
(createWebHistory(portalConfig.routerBase)).
Marketplace API prefix (/api/marketplace → /flyok-portal)¶
Service files keep calling the stable cluster-era path /api/marketplace/*. In
dev, Vite's proxy rewrites that to /flyok-portal. In the bundled build there
is no proxy, so the http client rewrites the prefix at request time using
portalConfig.marketplacePrefix (the host injects /flyok-portal). Without this
every API call would 404 against the same-origin host (which only serves
/flyok-portal/*). /flyok-app, /store-api, /flyok-portal paths are passed
through unchanged.
Operator setup on a fresh install¶
bin/console plugin:install --activate FlyokaiMarketplace
bin/console marketplace:oauth:client:create # stores client id + secret (+ plaintext for injection)
bin/console marketplace:user:create --role=seller …
bin/console assets:install
# → browse to https://<shop>/seller-portal
PHP dependency vendoring (bin/vendor-plugin.sh)¶
The plugin's PHP deps — the private flyokai/* packages, the flyokai forks
(revolt-event-loop, laminas-db) and the third-party closure (amphp/*,
laminas/*, league/uri, commerceguys/addressing, cuyz/valinor) — are
not resolvable from Packagist on a clean install, and executeComposerCommands()
returns false, so Shopware never runs composer. The zip therefore ships a
self-contained vendor/ that FlyokaiMarketplace::patchAutoloader() registers
(PSR-4 prefixes + amphp function files) against the Shopware kernel classloader
at boot.
⚠️ Assembled in the build stage, never the live plugin. The live
plugins/FlyokaiMarketplaceis symlinked into running installs (vendor/flyokai/sw-marketplace→ monorepo). Avendor/there makespatchAutoloaderdouble-load classes the install already resolves from root vendor → fatal "Cannot redeclare". Sovendor-plugin.shwrites into a destination plugin dir (defaultbuild/FlyokaiMarketplace) and refuses to target the live source.
composer install is not viable here: the flyokai packages require each other
via self.version (monorepo-only) and patchAutoloader expects a specific
on-disk layout. So the source of truth for which packages + layout is a
working symlinked plugin vendor (the dev setup). bin/vendor-plugin.sh [DEST]:
- dereferences
REF_VENDOR(default/www/shopware/main/custom/plugins/FlyokaiMarketplace/vendor) into real copies underDEST/vendor— capturing the exact set the dev plugin loads; - overlays the fresher flyokai/* straight from the monorepo
packages/; - verifies every key
patchAutoloaderpath resolves.
flyokai/applicationis trimmed, not shipped whole. The full package is the cluster runtime (~250 files, 1.2 MB), butapplication-coreholds the real impls of the leaf classes the embedded plugin uses, and consumers reference\Flyokai\ApplicationCore\*directly (the legacyclass_aliasshims were removed in 546342e3). Fromapplicationthe embedded path now loads only 6 files:Base/ExtensionRepositoryTrait.php(real bridge trait —SellerRepositoryImplstilluses the legacy namespace; traits can't beclass_alias'd) and 5 custom Laminas Ddl Column classes (json/longtext/mediumtext/smallint/timestamp) thatdb-schema'sColumnFactoryinstantiates duringapplyMarketplaceSchema()on install/update. Everything else (Acl, Bootstrap, Console, Cluster, Setup, …) is cluster-only and never autoloaded in embedded mode, sovendor-plugin.shcopies just the keep-list and the build checks fail loudly if a keep-list file goes missing. The clean activate in the smoke test — which runs schema apply — is the proof the trimmed set is sufficient.
doctrine/collections(+doctrine/deprecations) must be inREF_VENDOR.commerceguys/addressing'sSubdivision::getChildren()returns aDoctrine\Common\Collections\ArrayCollection, and a clean Shopware install has nodoctrine/collections(core ships dbal, not the ORM).patchAutoloaderregisters both prefixes from the bundled vendor; the smoke test's/locations/countries/US/subdivisionscheck is the one that exercises this path.
Shopware-provided libraries (symfony/*, twig/*) are intentionally not
bundled — they come from the host's root vendor on any Shopware install.
Override the source when the default reference isn't present:
REF_VENDOR=/path/to/working/plugin/vendor bin/vendor-plugin.sh.
Deploy + smoke (clean-room validation)¶
Two scripts — both take the target Shopware install as a required positional arg (no env-var defaults; you say where it goes):
bin/deploy-plugin.sh <SW_DIR> # build zip, install, activate, build admin
bin/smoke-portal.sh <SW_DIR> [BASE_URL] # HTTP checks against a deployed install
bin/smoke-portal.sh --deploy <SW_DIR> # deploy + check in one command
The canonical target is /www/sw6710st/sw6710st (→ https://sw6710st.loc) — a
Shopware install that does NOT symlink the monorepo and has NO flyokai/* in its
root vendor, so the plugin runs purely off its bundled vendor/ +
patchAutoloader. Both scripts refuse any install that fails the clean-room
guards (symlinked plugin, or flyokai/* in root vendor would mask gaps).
(/www/sw6710/sw6710 stays a dev box — the portal runs there as a Vite dev
server from portals/seller-portal, not the bundled route.)
deploy-plugin.sh does: package-plugin.sh → unzip to custom/plugins/ →
plugin:refresh → plugin:install --activate (or :update on re-runs) — a
clean activate proves the shipped vendor is complete — → build-administration.sh
(compiles per-plugin admin into Resources/public/administration/, else /admin
shows no FlyokaiMarketplace menu items) → assets:install → cache:clear.
smoke-portal.sh HTTP-checks /seller-portal (shell + injected
window.__PORTAL_CONFIG__ incl. marketplacePrefix=/flyok-portal), the
marketplace API (/flyok-portal/locations/countries), the doctrine-bundled
path (/locations/countries/US/subdivisions), a hashed portal asset, and the
compiled admin bundle (/bundles/flyokaimarketplace/administration/assets/
flyokai-marketplace-*.js) — the last one is the proof that admin built and the
sidebar entries will render.
TODO — ship pre-built admin in the zip.
deploy-plugin.shrunsbuild-administration.shon the install (~30 s, needs Node + the install's npm deps). The Shopware Store recommends shipping pre-built admin so customer installs don't need a JS build toolchain. Tooling option:shopware-cli extension buildbuilds a single extension in isolation, suitable for packaging into the zip.Login prerequisite: the bundled portal injects the OAuth
client_secretfrom system config, persisted only for clients created after the plaintext-secret change. On a fresh/pre-existing install runbin/console marketplace:oauth:client:createonce so login works (the smoke test warns when it's empty).TODO before actual submission: fix the cosmetic favicon 404 below.
Versioning & cutting a release¶
Shopware reads the plugin version from the zip's composer.json version.
plugin:refresh records it as the available version; when it exceeds the
installed version the plugin shows "upgradeable" and plugin:update runs
Plugin::update() + new migrations. The Store requires each upload to have a
higher version than the last.
The version lives in plugins/FlyokaiMarketplace/VERSION — not in the live
composer.json, which deliberately has no version so the monorepo
self.version deps resolve as dev-main on the symlinked dev install. The
packaging step stamps VERSION into the shipped composer.json and the zip name.
bin/release-plugin.sh patch # 1.0.0 -> 1.0.1, then package
bin/release-plugin.sh minor # -> 1.1.0
bin/release-plugin.sh 2.3.0 # set explicitly
bin/release-plugin.sh # no bump — repackage at current version
It prints the suggested git commit/git tag v<version> (it does not touch git
itself). Then run bin/smoke-portal.sh --deploy /www/sw6710st/sw6710st to validate
the new build on the clean-room install (it plugin:updates an already-installed
version, rebuilds admin, then HTTP-checks).
Packaging the Store zip¶
It stages the plugin source, stamps the VERSION into the shipped
composer.json and strips the private flyokai/* requires (they ship in
vendor/ and load via patchAutoloader, not composer — Shopware's plugin
requirement check otherwise rejects the self.version entries as "missing or not
installed"), runs build-portal.sh (SPA) + vendor-plugin.sh (PHP vendor) into
the stage, and zips it. CI equivalent: .github/workflows/release.yml.
Known cosmetic gap¶
The built index.html keeps a root-absolute <link rel="icon" href="/favicon.svg">
(Vite does not rewrite absolute URLs in index.html). It 404s under the bundle
path — harmless. Fix later by making the favicon relative or dropping it.