Asset Serving & Microfrontends
Easy AppServer provides secure, high-performance asset serving with integrity verification and support for multiple microfrontend integration modes including Module Federation, Web Components, and ES Modules.
Overview
Frontend assets (JavaScript, CSS, fonts, images) uploaded via app manifests are stored in PostgreSQL and served with aggressive caching, integrity checks (SRI), and optimized HTTP headers. The platform supports three microfrontend integration modes for building composable UIs.
Asset Storage
Assets are stored as part of the app manifest registration:
Code Reference: pkg/v2/domain/app/app.go:99
Asset Structure
type Asset struct {
Name string // e.g., "remoteEntry.js", "styles.css"
MimeType string // e.g., "application/javascript"
Contents []byte // Raw file data
Signature []byte // For integrity verification
SHA256 string // Checksum hash
}
Storage Pipeline
Manifest Registration:
├─ Extract assets from manifest
├─ Calculate SHA-256 checksums
├─ Store in PostgreSQL (appserver.app_assets table)
├─ Associate with app version
└─ Ready for serving
Asset Serving
The AssetServer provides high-performance asset delivery:
Code Reference: pkg/v2/application/ui/asset_server.go:18
Serving Flow
HTTP Request: GET /apps/{appName}/assets/{assetName}
↓
Check in-memory cache
├─ Cache Hit: Return immediately (< 1ms)
└─ Cache Miss: Query database
↓
Validate checksum
↓
Generate SRI hash
↓
Store in cache
↓
Return with headers
Response Headers
Content-Type: application/javascript
ETag: "a1b2c3d4e5f67890"
Cache-Control: public, max-age=31536000, immutable
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self'
Subresource Integrity (SRI)
Assets include SRI hashes for browser verification:
<script
src="/apps/de.easy-m.todos/assets/remoteEntry.js"
integrity="sha256-AbCdEf1234567890..."
crossorigin="anonymous">
</script>
Generated with:
func GenerateSRIHash(data []byte) string {
hash := sha256.Sum256(data)
encoded := base64.StdEncoding.EncodeToString(hash[:])
return fmt.Sprintf("sha256-%s", encoded)
}
Cache Manager
In-memory caching minimizes database queries:
Code Reference: pkg/v2/application/ui/cache_manager.go:8
Cache Configuration
type CacheManager struct {
cache map[string]*CachedAsset // Key: "appName:assetName"
ttl time.Duration // Default: 1 hour
maxSize int64 // Max cache size in bytes
currSize int64 // Current usage
}
type CachedAsset struct {
Data []byte
MimeType string
SHA256 string
ETag string
SRI string
CachedAt int64 // Unix timestamp (ms)
LastModified int64
}
Cache Behavior
Get Operation:
func (cm *CacheManager) Get(key string) *CachedAsset {
// Check if exists and not expired
if asset, exists := cm.cache[key]; exists {
if time.Since(time.UnixMilli(asset.CachedAt)) <= cm.ttl {
return asset // Cache hit
}
}
return nil // Cache miss
}
Set Operation:
func (cm *CacheManager) Set(key string, asset *CachedAsset) {
// Evict oldest entries if over limit
if cm.currSize + len(asset.Data) > cm.maxSize {
cm.evictOldest(len(asset.Data))
}
cm.cache[key] = asset
cm.currSize += len(asset.Data)
}
Automatic Cleanup
Cache automatically removes expired entries:
func (cm *CacheManager) cleanupLoop() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
cm.cleanup() // Remove expired entries
}
}
Event-Based Invalidation
Cache invalidation on app lifecycle events (when properly configured):
App Installed/Updated/Uninstalled:
↓
Publish app.* event
↓
UI Service receives event (if Start() called)
↓
Invalidate cache for app: cache.DeleteByPrefix("appName:")
↓
Next request fetches fresh assets
The UI service defines a Start() method (pkg/v2/application/ui/ui_service.go:200-239) that subscribes to app.* events for cache invalidation. However, this method is not currently called during server startup (pkg/v2/server/services.go:330-357).
Current behavior: Asset caches are not automatically invalidated when apps are installed, updated, or uninstalled. Caches persist until manual invalidation or server restart.
To enable automatic cache invalidation: Call uiService.Start(ctx) during server initialization, or manually clear caches after app lifecycle changes.
Code Reference: pkg/v2/application/ui/ui_service.go:19
Microfrontend Integration Modes
The platform supports three integration modes:
Code Reference: pkg/v2/domain/app/app.go:116
Mode 1: Module Federation
Webpack Module Federation for React/Vue/Angular apps.
Manifest Declaration:
defineManifest()
.webApp(web => web
.mode('federation')
.entryAsset('remoteEntry.js')
.exposedModule('./App')
.routeBase('/apps/todos')
.navigation(nav => nav
.item('Todos', '/todos', 'list-icon')
)
)
.build();
Runtime Configuration:
// GET /apps/de.easy-m.todos/mf-config
{
"remoteEntryURL": "/apps/de.easy-m.todos/assets/remoteEntry.js",
"exposedModule": "./App",
"moduleName": "de.easy-m.todos"
}
Shell Integration:
// Shell dynamically loads remote
const remoteTodos = await loadRemote('de.easy-m.todos/App');
Code Reference: pkg/v2/application/ui/ui_service.go:58
Mode 2: Web Components
Framework-agnostic Custom Elements.
Manifest Declaration:
defineManifest()
.webApp(web => web
.mode('web_component')
.entryAsset('component.js')
.routeBase('/apps/notes')
)
.build();
Runtime Configuration:
// GET /apps/de.easy-m.notes/wc-config
{
"tagName": "de-easy-m-notes-app",
"scriptURL": "/apps/de.easy-m.notes/assets/component.js"
}
Shell Integration:
<!-- Dynamically load script -->
<script src="/apps/de.easy-m.notes/assets/component.js"></script>
<!-- Use custom element -->
<de-easy-m-notes-app></de-easy-m-notes-app>
Code Reference: pkg/v2/application/ui/ui_service.go:88
Mode 3: ES Modules (ESM)
Lightweight ES Module imports.
Manifest Declaration:
defineManifest()
.webApp(web => web
.mode('esm_component')
.entryAsset('index.esm.js')
.routeBase('/apps/widget')
)
.build();
Runtime Configuration:
// GET /apps/de.easy-m.widget/esm-config
{
"importURL": "/apps/de.easy-m.widget/assets/index.esm.js",
"exports": ["default"]
}
Shell Integration:
// Dynamic ES Module import
const module = await import('/apps/de.easy-m.widget/assets/index.esm.js');
const Widget = module.default;
Code Reference: pkg/v2/application/ui/ui_service.go:117
Navigation Integration
Apps declare navigation items in their manifest:
.navigation(nav => nav
// Level 1: Rail navigation
.item('Dashboard', '/dashboard', 'dashboard-icon')
// Level 2: Drawer navigation
.item('Reports', '/reports', 'chart-icon', (sub) => sub
.item('Sales', '/reports/sales')
.item('Analytics', '/reports/analytics')
// Level 3: Collapsible sub-items
.item('Advanced', '/reports/advanced', (subsub) => subsub
.item('Custom', '/reports/advanced/custom')
)
)
)
The platform merges navigation from all installed apps into the shell's navigation structure.
Checksum Validation
All assets undergo integrity verification:
Code Reference: pkg/v2/application/ui/asset_server.go:109
Validation Process
func (s *AssetServer) validateChecksum(data []byte, expectedSHA256 string) error {
hash := sha256.Sum256(data)
actualSHA256 := hex.EncodeToString(hash[:])
if actualSHA256 != expectedSHA256 {
return &ChecksumMismatchError{
Expected: expectedSHA256,
Actual: actualSHA256,
}
}
return nil
}
Error Response
If validation fails:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "checksum mismatch",
"expected": "a1b2c3d4...",
"actual": "e5f67890..."
}
This prevents serving tampered or corrupted assets.
HTTP Caching Strategy
Asset responses use aggressive caching:
Cache Headers
Cache-Control: public, max-age=31536000, immutable
public: Can be cached by browsers and CDNsmax-age=31536000: Cache for 1 year (31,536,000 seconds)immutable: Hint that content will never change
ETag Support
ETag: "a1b2c3d4e5f67890"
If-None-Match: "a1b2c3d4e5f67890"
→ HTTP 304 Not Modified
Reduces bandwidth for unchanged assets.
Content Versioning
Assets are immutable—version changes require new app registration. This enables long cache lifetimes without staleness concerns.
Best Practices
For App Developers
Minify and Optimize Assets:
# Before upload
npm run build # Minifies JS/CSS
Use Content Hashing in Filenames:
main.a1b2c3d4.js
styles.e5f67890.css
Declare Correct MIME Types:
.asset('app.js', 'application/javascript')
.asset('styles.css', 'text/css')
.asset('logo.svg', 'image/svg+xml')
Keep Assets Under Limits:
Single asset: < 10 MB
Total per app: < 50 MB
For Platform Operators
Configure Adequate Cache Size:
cache := NewCacheManager(
1 * time.Hour, // TTL
500 * 1024 * 1024, // 500 MB max
)
Monitor Cache Hit Rate:
Target: > 95% hit rate
Alert if < 80%
Enable CDN (Optional):
Place CDN in front of /apps/*/assets/* routes
Cache based on ETag/Cache-Control headers
Security Considerations
Content Security Policy
Assets are served with CSP headers:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
MIME Type Sniffing Prevention
X-Content-Type-Options: nosniff
Prevents browsers from MIME-sniffing and executing non-JS files as scripts.
Subresource Integrity
SRI hashes ensure browser verifies integrity:
<script
src="..."
integrity="sha256-..."
crossorigin="anonymous">
</script>
If hash doesn't match, browser refuses to execute.
Performance Optimization
Asset Compression
Assets can be pre-compressed:
app.js → app.js.br (Brotli)
app.js → app.js.gz (Gzip)
Server serves based on Accept-Encoding header.
Lazy Loading
Module Federation supports code splitting:
const TodosApp = lazy(() => import('de.easy-m.todos/App'));
Only loads remote when needed.
Parallel Asset Loading
Browser can parallelize asset fetches:
<link rel="preload" href="/apps/todos/assets/main.js" as="script">
<link rel="preload" href="/apps/todos/assets/styles.css" as="style">
Troubleshooting
Asset Not Found
Problem: GET /apps/myapp/assets/main.js → 404
Causes:
- App not installed
- Asset name mismatch in manifest
- Asset not uploaded during registration
Solution:
- Verify app installation: SELECT * FROM appserver.apps WHERE name = 'myapp'
- Check asset table: SELECT * FROM appserver.app_assets WHERE app_id = ...
- Re-register app with correct assets
Checksum Mismatch
Problem: Asset fails integrity check
Causes:
- Database corruption
- Concurrent update during serving
- Incorrect checksum in manifest
Solution:
- Verify database integrity
- Re-upload app assets
- Recalculate checksums
Cache Thrashing
Problem: Low cache hit rate, high database load
Causes:
- Cache size too small
- TTL too short
- Too many unique apps/assets
Solution:
- Increase cache max size
- Increase TTL (if acceptable staleness)
- Monitor cache evictions
Related Concepts
- Application Manifest - Declaring assets in manifest
- Platform Architecture - HTTP serving layer
- Caching Strategy - Multi-level caching details
- Developer Platform & SDK - Asset bundling with SDK
- GraphQL API & Subscriptions - Querying app UI config
Further Reading
- Getting Started: Building Apps - Creating microfrontends
- Frontend SDK: Module Federation - MF setup and best practices