Context
A module ships files with its code: images, icons, fonts, language files, mail and PDF templates, scaffold stubs and seed data. Those files are read-only and versioned with the package. Runtime files, such as uploads and generated PDFs, are different: a tenant owns them and they change. The owner accepted the proposal instackra-notes/resources-discovery-proposal.md (2026-10-20), with the placements below. The owner then renamed the
concept from “resources” to “assets”, so that the word “resource” stays with @Resource.
Decision
- One pipeline. A module’s assets follow the same path as its other contributions: declare, collect, publish, merge, consume (0144 for the registry, 0158 for the manifest).
- Declare by location. A module’s files sit in an
assets/folder beside its module file. For a package’s root module that is the package’sassets/folder. The namespace is the module’sname(@Module({ name })). There is nodefineAssets. Folder name = kind:public/: anything with a URL, published to the CDN;lang/: goes into the i18n registry under the namespace;templates/: mail, PDF and SMS templates, going into the templates registry;stubs/: used by the scaffolder (pnpm newandstackra make);data/: private seed or reference data, read withread().
- Packages.
@stackra/assetsis in thedatacategory. Its entries:./contracts: theAssetsinterface, theASSETStoken and theAssetKeytype;.: theAssetsservice, a read-only view over a filesystem disk with namespace and override resolution, and a manifest loader;./node: the Explorer that registers module assets at boot, and the dev server that serves them;./reactand./native: theuseAsset()hook;./testing: a fake.
@stackra/filesystemis in thedatacategory: disks and drivers (S3, R2, MinIO, local and database).@stackra/containermodules carryname, which is the namespace.- A package exports its assets as
./assets/*. Its folder isassets/, notresources/.
- Keys use
::. An asset key is<module>::<path>, for exampleflags::public/images/eg.svg. The same form is used for i18n keys (billing::invoice.refund_confirm), template keys (billing::mail/invoice-paid) and stub keys (ADR 0231, decision 3). - Collection. The collector lives in the codegen compiler (
stackra gen,developercategory). It walks the module graph and writes theassetssection ofapp-manifest.json, with namespace, kind, path, content hash, public flag and owning module. It emits theAssetKeyunion into the gitignored.stackra/(0162). The collector and the Explorer agree, and a conformance test checks it (0144). - Publish. Publishing public assets to the CDN bucket is a deploy step, run by
stackra buildor CI. Files are content-hashed. - Storage.
@stackra/filesystemis the only storage API. Disks are injected, never reached through a facade. Usage isdisk('public').url()anddisk('private').temporaryUrl(). Static helpers exist only for pure utilities (Str,Date,Arr, ADR 0145). - Consumers.
@stackra/i18nconsumeslang/. The templates registry in the communication category consumestemplates/. The scaffolder consumesstubs/. - Overrides. A member replaces a module file at
assets/overrides/<namespace>/<same path>. The override wins, and gen records which one won. Language JSON files merge key by key. Other files are replaced whole. - Bundler imports.
#assets/public/...?urlimports are allowed only inside the owning package, for tree-shaking. Overrides do not apply to them. The docs say so. - Runtime tenant files belong to the Figentra OS files service, not to assets. Tenant branding stays in the runtime brand service.
- Why two places. Laravel also separates
resources/(shipped with code) fromstorage/(created at runtime). This ADR keeps that split, and names the firstassets/here, because Stackra reserves “resource” for@Resource.
Consequences
- An agent can predict an asset’s path from its kind and namespace, without reading code.
- Every asset is in
app-manifest.json, so it is reviewed and diffed in a pull request. - Public assets are immutable after publish (content-hashed), so caches never serve a stale file.
- Any text that calls a package folder
resources/is stale. ADR 0126 is amended by a forward note in the same change.
Enforcement
planned — gen fails on an asset whose kind folder is unknown, and the Explorer and collector conformance test covers the manifest section. The validator rejects a../ import into assets/ (0233). The scaffold templates gain assets/ with public/ and lang/.
Alternatives
- Assets as runtime uploads in the same store: rejected; the two have different owners and mutability.
- A
defineAssetscall per module: rejected; the folder is the declaration. - “Resources” for shipped files: rejected by the owner; the word is reserved for
@Resource.
Supersedes and sources
Supersedes: none. Amends: 0127; forward note on 0126. Builds on: 0144, 0158, 0162. Sources:stackra-notes/resources-discovery-proposal.md (owner
accepted, 2026-10-20); the owner’s placements and the rename to assets (2026-10-20); the owner’s naming convention (stackra-decisions-2026-10-19.md,
end of file: :: for namespace lookups); DA3 (stackra-decisions-2026-10-19.md, public CDN bucket).