Goal & when
Use bundled assets for templates, seed JSON, logos, and other read-only files that travel with the plugin package. Use the plugin data folder for anything created or updated while the app runs.
- Backend read-only — plugins/<name>/backend/assets/
- Frontend URLs — PluginAssets.network(metadata, relativePath)
- Runtime writes — AppFolder.getPluginDataFolder(metadata.name), not the asset folder
1. Prerequisites
You need a plugin with matching metadata.name / pluginVersion and access to the shared AppFolder / PluginAssets helpers.
- module.metadata.json — name and pluginVersion align with the installed bundle
- Backend — @quan-erp/shared-backend-core (AppFolder)
- Frontend — @quan-erp/shared-frontend-core (PluginAssets)
- Rebuild + reinstall (or watch refresh) after changing packed assets
2. Backend assets
Put read-only files under plugins/<name>/backend/assets/. The CLI packs them into available/installed plugins. Resolve paths with AppFolder — never hardcode APP_DATA_FOLDER.
- Source — plugins/<name>/backend/assets/…
- Runtime path — AppFolder.getPluginAssetFolder(metadata.name, metadata.pluginVersion)
- Writable runtime files — AppFolder.getPluginDataFolder(metadata.name) instead
- Do not write into the asset folder after install — it is versioned and replaced on upgrade
1plugins/<plugin-name>/
2└── backend/
3 ├── assets/
4 │ ├── invoice-template.html
5 │ └── seed/
6 │ └── defaults.json
7 └── src/1import { AppFolder } from "@quan-erp/shared-backend-core";
2import metadata from "../../module.metadata.json" with { type: "json" };
3import fs from "node:fs/promises";
4import path from "node:path";
5
6const assetDir = AppFolder.getPluginAssetFolder(
7 metadata.name,
8 metadata.pluginVersion,
9);
10const template = await fs.readFile(
11 path.join(assetDir, "invoice-template.html"),
12 "utf8",
13);const dataDir = AppFolder.getPluginDataFolder(metadata.name);
await fs.mkdir(dataDir, { recursive: true });
await fs.writeFile(path.join(dataDir, "export.csv"), csv, "utf8");3. Frontend assets
Use PluginAssets.network(metadata, path) from @quan-erp/shared-frontend-core to build absolute URLs for bundled plugin assets in the UI. Keep the relative path stable and aligned with what you ship in the package.
- Import PluginAssets from @quan-erp/shared-frontend-core
- Pass the same module.metadata.json the plugin uses elsewhere
- path is relative to the plugin’s packed frontend / asset tree your stack exposes
1import { PluginAssets } from "@quan-erp/shared-frontend-core";
2import metadata from "../module.metadata.json" with { type: "json" };
3
4export function PluginLogo() {
5 const src = PluginAssets.network(metadata, "images/logo.png");
6 return <img src={src} alt={metadata.name} />;
7}4. Verify & rules
- After watch / build, assets exist under available-plugins (and installed-plugins after Install)
- getPluginAssetFolder resolves and readFile succeeds for a known relative path
- PluginAssets.network returns a loadable URL in the browser
- Assets are versioned with the plugin package — rebuild + reinstall after changes
- Durable generated files belong in getPluginDataFolder, not assets
- Keep relative paths stable so backend AppFolder and frontend PluginAssets stay aligned
- Isolation — only write under getPluginDataFolder(yourPluginName); never hardcode /app-data/…
Common mistakes
- Hardcoding APP_DATA_FOLDER or /app-data/... paths instead of AppFolder
- Writing exports / reports into the asset folder at runtime
- Passing the wrong pluginVersion into getPluginAssetFolder after an upgrade
- Changing asset files without rebuild / reinstall (or watch refresh)
- Frontend path that does not match the packed asset relative path
- Storing durable data in the app temp folder