Skip to main content

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
Implementation Status

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

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 CDNs
  • max-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

Further Reading